Add a Frontend Module

Step-by-step guide to creating a new frontend module following SOLID View/ViewModel pattern.

This tutorial walks you through creating a complete frontend module from scratch, following the SOLID View/ViewModel pattern. You'll set up the module structure, create domain entities, build the data layer, implement ViewModels, and wire everything together.

Prerequisites

  • Node.js 18+ and pnpm installed
  • Backend API running (see Quick Start guide)
  • Understanding of SOLID View/ViewModel pattern

Step-by-Step Guide

  1. 1. Create Module Structure

    Set up the standard module directory structure with domain, data, and presentation layers.
  2. 2. Define Domain Entity

    Create a Zod schema for your entity with validation rules.
  3. 3. Create Repository

    Implement the data layer with API calls and response mapping.
  4. 4. Set Up DI Container

    Register your repository in the module's dependency injection container.
  5. 5. Build ViewModel

    Create the main ViewModel that orchestrates CRUD operations using useCrudViewModel.
  6. 6. Create View

    Build the pure UI View component that consumes the ViewModel (max ~200 lines).
  7. 7. Add Route & Navigation

    Create the Next.js page connector and add navigation entries.

Module Structure

src/modules/inventory/directory.structure.txtText
src/modules/inventory/
├── di.ts                        # Module DI container (wires all sub-modules)
├── index.ts                     # Public exports
└── products/                    # Products sub-module
    ├── index.ts                 # Sub-module barrel
    ├── locales/                 # Sub-module-owned translations
    │   ├── products.en.ts
    │   ├── products.ar.ts
    │   └── index.ts
    └── src/
        ├── domain/
        │   ├── entities/
        │   │   └── Product.ts   # Zod schema/Entity
        │   └── interfaces/
        │       └── IProductRepository.ts
        ├── data/
        │   ├── models/
        │   │   └── ProductModel.ts  # API DTO
        │   ├── mappers/
        │   │   └── ProductMapper.ts # DTO ↔ Entity
        │   └── repositories/
        │       └── ProductRepository.ts
        └── presentation/
            ├── viewmodels/
            │   └── useProductListViewModel.ts # Hook
            ├── views/
            │   └── ProductListView.tsx        # Pure UI view
            └── components/
                └── ProductStats.tsx

Domain Entity

products/src/domain/entities/Product.tsTypeScript
import { z } from 'zod';

/**
 * Exported constant defining parameters and fields for product schema configurations.
 */
export const ProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  sku: z.string(),
  price: z.number().positive(),
  quantity: z.number().int().min(0),
  category: z.string(),
  isActive: z.boolean(),
  tenantId: z.string().uuid(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime().optional(),
});

/**
 * Exported type defining parameters and fields for product configurations.
 */
export type Product = z.infer<typeof ProductSchema>;

/**
 * Exported constant defining parameters and fields for create product schema configurations.
 */
export const CreateProductSchema = z.object({
  name: z.string().min(1, "Name is required"),
  sku: z.string().min(1, "SKU is required"),
  price: z.number().positive("Price must be positive"),
  quantity: z.number().int().min(0),
  category: z.string(),
});

/**
 * Exported type defining parameters and fields for create product input configurations.
 */
export type CreateProductInput = z.infer<typeof CreateProductSchema>;

Repository

products/src/domain/interfaces/IProductRepository.tsTypeScript
import { Result } from '@core/common/Result';

/**
 * Repository layer implementing client request queries for i product.
 * Calls base API service routines and resolves DTO objects mapping to domain entities.
 */
export interface IProductRepository {
  getAll(params: PaginationParams): Promise<Result<PaginatedResult<Product>>>;
  getById(id: string): Promise<Result<Product>>;
  create(data: CreateProductInput): Promise<Result<Product>>;
  update(id: string, data: UpdateProductInput): Promise<Result<Product>>;
  delete(id: string): Promise<Result<void>>;
}

DI Container

di.ts — Module DI ContainerTypeScript
import { getModuleApiService } from '@core/services/api-factory';
import { ProductRepository } from './products/src/data/repositories/ProductRepository';
import type { IProductRepository } from './products/src/domain/interfaces/IProductRepository';

interface InventoryContainer {
  productRepository: IProductRepository;
}

let _instance: InventoryContainer | null = null;

function createContainer(): InventoryContainer {
  if (typeof window === 'undefined') {
    const dummyProxy = new Proxy({} as any, {
      get() { return () => Promise.resolve({}); }
    });
    return { productRepository: dummyProxy };
  }
  const apiService = getModuleApiService('INVENTORY');
  return {
    productRepository: new ProductRepository(apiService)
  };
}

/**
 * Documentation for module export
 */
export const container: InventoryContainer = new Proxy({} as InventoryContainer, {
  get(_target, prop: keyof InventoryContainer) {
    if (!_instance) {
      _instance = createContainer();
    }
    return _instance[prop];
  }
});

ViewModel

products/src/presentation/viewmodels/useProductListViewModel.ts — OrchestratorTypeScript
'use client';
import { useCrudViewModel } from '@core/crud';
import { container } from '../../../../di';

/**
 * React hook/ViewModel orchestrating state and data flows for product list view model.
 * Handles active states updates, form fields validations, and browser navigation controllers.
 */
export function useProductListViewModel() {
  const repo = container.productRepository;

  const table = useCrudViewModel({
    queryKey: 'products',
    fetchFn: (params) => repo.getAll(params),
    createFn: (data) => repo.create(data),
    updateFn: (id, data) => repo.update(id, data),
    deleteFn: (id) => repo.delete(id),
    enableSelection: true,
  });

  const columns = [
    column.index('No'),
    column.text('name', t('products.name')),
    column.text('sku', t('products.sku')),
    column.text('price', t('products.price')),
    column.status('isActive', t('status'), statusMap),
  ];

  return { table, columns };
}

View Component

products/src/presentation/views/ProductListView.tsx — Pure UI (~20 lines)TSX
'use client';
import { GenericCrudView } from '@core/crud';
import { useProductListViewModel } from '../viewmodels/useProductListViewModel';

/**
 * Presentation UI component rendering the product list 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 ProductListView() {
  const { table, columns } = useProductListViewModel();

  return (
    <GenericCrudView
      crud={table}
      columns={columns}
      title={t('products.title')}
      createButtonLabel={t('products.create')}
    />
  );
}

Route & Navigation

src/app/(modules)/inventory/page.tsx — Server ConnectorTSX
import { Metadata } from 'next';
import { ProductListView } from '@modules/inventory';

export const metadata: Metadata = {
  title: 'Inventory | SCRIPE',
  description: 'Manage products and inventory',
};

export default function InventoryPage() {
  return <ProductListView />;
}