Frontend Architecture
SOLID View/ViewModel pattern, module structure, and the connector pattern for Next.js integration.
The SCRIPE frontend is built with Next.js 16 (App Router) following a strict SOLID View/ViewModel pattern. Every page is composed of a pure UI View that delegates all logic to ViewModel hooks. This separation ensures testability, reusability, and maintainability.
SOLID View/ViewModel Pattern
The SOLID pattern ensures each piece of the UI has a single responsibility. Views render JSX, ViewModels manage state and logic, and Components provide reusable UI sections.
| Principle | Application in SCRIPE |
|---|---|
| S — Single Responsibility | Each ViewModel handles ONE concern (filter, stats, table) |
| O — Open/Closed | Base hooks extended via composition, not modification |
| L — Liskov Substitution | All ViewModels return consistent typed interfaces |
| I — Interface Segregation | Components receive only the props they need |
| D — Dependency Inversion | Views depend on ViewModel interfaces, not implementations |
View Rules
View SHOULD
- Max ~200 lines of JSX
- Zero useState, zero useEffect
- Only destructures props from ViewModel
- Returns JSX with component composition
- Imports section components for layout
View SHOULD NOT
- No business logic in views
- No API calls or data transformations
- No direct state management
- No inline styles with logic
- No conditional data fetching
View Example
'use client';
/**
* Presentation UI component rendering the user management view.
* Arranges layout boundaries and accessibility targets (WCAG, tab index) using the core design library (@core/ui/*). Coordinates text fields, submit indicators, and validation warning messages.
*/
export function UserManagementView() {
const vm = useUserManagementViewModel();
return (
<div>
<h1>{vm.title}</h1>
<FilterSection {...vm.filters} />
<Spacer />
<StatisticsSection {...vm.statistics} />
<GenericCrudView {...vm.table} columns={vm.columns} />
</div>
);
}
// ~15 lines. Views are this short. Always.ViewModel Rules
ViewModels are React hooks that contain all business logic. They compose section-specific ViewModels (statistics, filters, table) and return typed interfaces consumed by Views.
export function useUserManagementViewModel() {
// Compose section ViewModels (Single Responsibility)
const statistics = useStatisticsViewModel();
const filters = useFilterViewModel();
const table = useCrudViewModel(crudConfig);
const blockAction = useBlockUserAction();
// Columns defined HERE, not in View or Component
const columns = useMemo(() => [
column.index("No"),
column.text("name", "Name"),
column.text("email", "Email"),
column.status("status", "Status", statusMap),
column.switch("block", "Block", blockAction),
column.date("createdAt", "Created", { locale: "en-GB" }),
], [blockAction]);
return {
title: t("userManagement.title"),
statistics,
filters,
table,
columns,
};
}Module File Structure
module/
├── di.ts # Module DI Container
├── index.ts # Public API exports
└── src/
├── domain/ # Business Logic (Pure TypeScript)
│ ├── entities/ # Zod schemas + business rules
│ └── interfaces/ # Repository contracts
│
├── data/ # Data Access Layer
│ ├── models/ # API DTOs (raw response shapes)
│ ├── mappers/ # DTO ↔ Entity transformations
│ └── repositories/ # Interface implementations
│
└── presentation/ # UI Layer (SOLID Pattern)
├── viewmodels/ # Section ViewModels + Orchestrator
├── views/ # Pure UI Pages (~200 lines max)
└── components/ # Section ComponentsConnector Pattern
The connector pattern separates Next.js App Router pages (Server Components) from module Views (Client Components). Pages in src/app/ are thin connectors that import and render module Views. They handle routing, metadata, and URL params only.
// src/app/(modules)/admin/user-management/page.tsx
import { Metadata } from 'next';
import { UserManagementView } from '@modules/admin/user-management';
export const metadata: Metadata = {
title: 'User Management | SCRIPE',
};
export default function Page() {
return <UserManagementView />;
}
// That's it. Server component. No logic. Just connects.