nextjs-module-builder

Complete workflow for building new modules in Next.js 15 (App Router) with Supabase backend. Use when creating CRUD features, data management modules, or any new feature module following the 5-layer architecture (Types → Services → Hooks → Components → Pages). Covers TypeScript types, Supabase services, React hooks, Shadcn/UI components, and permission-based routing.

$ インストール

git clone https://github.com/majiayu000/claude-skill-registry /tmp/claude-skill-registry && cp -r /tmp/claude-skill-registry/skills/development/nextjs-module-builder ~/.claude/skills/claude-skill-registry

// tip: Run this command in your terminal to install the skill


name: nextjs-module-builder description: Complete workflow for building new modules in Next.js 15 (App Router) with Supabase backend. Use when creating CRUD features, data management modules, or any new feature module following the 5-layer architecture (Types → Services → Hooks → Components → Pages). Covers TypeScript types, Supabase services, React hooks, Shadcn/UI components, and permission-based routing.

Next.js Module Builder

Build complete feature modules following MyJKKN's standardized 5-layer architecture for Next.js 15 + Supabase applications.

Architecture Overview

Every module follows a strict 5-layer pattern:

  1. Types Layer (types/) - TypeScript interfaces and DTOs
  2. Service Layer (lib/services/) - Supabase database operations and business logic
  3. Hooks Layer (hooks/) - React state management and data fetching
  4. Components Layer (_components/) - Reusable UI components with Shadcn/UI
  5. Pages Layer (app/(routes)/) - Route handlers with Server Components

Workflow Decision Tree

Start here for every new module:

  1. Is database schema defined?

    • NO → Read references/database-patterns.md and create schema first
    • YES → Continue to step 2
  2. Are TypeScript types needed?

    • YES → Follow Step 1: Types Layer below
    • NO → Skip to step 3
  3. Need database operations?

    • YES → Follow Step 2: Service Layer below
    • NO → Skip to step 4
  4. Need state management?

    • YES → Follow Step 3: Hooks Layer below
    • NO → Skip to step 5
  5. Need UI components?

    • YES → Follow Step 4: Components Layer below
    • NO → Skip to step 6
  6. Need pages/routes?

    • YES → Follow Step 5: Pages Layer below
    • Then proceed to step 7
  7. Configure permissions

    • Follow Step 6: Permissions & Navigation

Step 1: Types Layer (20-30 min)

Create types/[module-name].ts with these interfaces:

// Main entity interface - include ALL database fields
export interface Entity {
  id: string;
  institution_id: string;
  name: string;
  // ... your entity fields
  is_active: boolean;
  created_at: string;
  updated_at: string;
  created_by?: string;
  updated_by?: string;
}

// Create DTO - only fields user provides
export interface CreateEntityDto {
  institution_id: string;
  name: string;
  // ... only user-provided fields
}

// Update DTO - all fields optional except id
export interface UpdateEntityDto {
  id: string;
  name?: string;
  // ... optional update fields
  is_active?: boolean;
}

// Filter interface - for search and filtering
export interface EntityFilters {
  institution_id?: string;
  search?: string;
  is_active?: boolean;
  // ... filter fields
}

// Response interface - for paginated lists
export interface EntityResponse {
  data: Entity[];
  total: number;
  page: number;
  pageSize: number;
}

Detailed patterns: See references/typescript-patterns.md

Step 2: Service Layer (45-60 min)

Create lib/services/[module]/[entity]-service.ts:

import { createClientSupabaseClient } from '@/lib/supabase/client';
import type { CreateEntityDto, UpdateEntityDto, EntityFilters } from '@/types/[module]';

export class EntityService {
  private static supabase = createClientSupabaseClient();

  // GET with pagination and filters
  static async getEntities(filters: EntityFilters = {}, page = 1, pageSize = 10) {
    try {
      let query = this.supabase.from('entities').select('*', { count: 'exact' });

      // Apply filters
      if (filters.institution_id) {
        query = query.eq('institution_id', filters.institution_id);
      }
      if (filters.search) {
        query = query.ilike('name', `%${filters.search}%`);
      }
      if (filters.is_active !== undefined) {
        query = query.eq('is_active', filters.is_active);
      }

      // Pagination
      const from = (page - 1) * pageSize;
      const to = from + pageSize - 1;
      query = query.range(from, to);

      const { data, error, count } = await query;
      if (error) throw error;

      return { data: data || [], total: count || 0, page, pageSize };
    } catch (error) {
      console.error('[module/entity] Error fetching:', error);
      throw error;
    }
  }

  // GET by ID
  static async getEntityById(id: string) {
    try {
      const { data, error } = await this.supabase
        .from('entities')
        .select('*')
        .eq('id', id)
        .single();

      if (error) throw error;
      return data;
    } catch (error) {
      console.error('[module/entity] Error fetching by ID:', error);
      throw error;
    }
  }

  // CREATE
  static async createEntity(dto: CreateEntityDto) {
    try {
      const { data, error } = await this.supabase
        .from('entities')
        .insert([dto])
        .select()
        .single();

      if (error) throw error;
      console.log('[module/entity] Created:', data.id);
      return data;
    } catch (error) {
      console.error('[module/entity] Error creating:', error);
      throw error;
    }
  }

  // UPDATE
  static async updateEntity(dto: UpdateEntityDto) {
    try {
      const { id, ...updates } = dto;
      const { data, error } = await this.supabase
        .from('entities')
        .update(updates)
        .eq('id', id)
        .select()
        .single();

      if (error) throw error;
      console.log('[module/entity] Updated:', id);
      return data;
    } catch (error) {
      console.error('[module/entity] Error updating:', error);
      throw error;
    }
  }

  // DELETE (soft delete recommended)
  static async deleteEntity(id: string) {
    try {
      // Soft delete: set is_active = false
      const { error } = await this.supabase
        .from('entities')
        .update({ is_active: false })
        .eq('id', id);

      if (error) throw error;
      console.log('[module/entity] Deleted:', id);
      return true;
    } catch (error) {
      console.error('[module/entity] Error deleting:', error);
      throw error;
    }
  }
}

Detailed patterns: See references/service-patterns.md

Step 3: Hooks Layer (30-45 min)

Create hooks/[module]/use-[entity].ts:

'use client';

import { useState, useEffect, useCallback } from 'react';
import { EntityService } from '@/lib/services/[module]/[entity]-service';
import type { Entity, EntityFilters } from '@/types/[module]';
import { usePermissions } from '@/hooks/use-permissions';

export function useEntities(filters: EntityFilters = {}) {
  const [entities, setEntities] = useState<Entity[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [total, setTotal] = useState(0);
  const [page, setPage] = useState(1);
  const pageSize = 10;

  const { userProfile } = usePermissions();

  const fetchEntities = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);

      // Auto-apply institution filter
      const effectiveFilters = {
        ...filters,
        institution_id: filters.institution_id || userProfile?.institution_id
      };

      const response = await EntityService.getEntities(
        effectiveFilters,
        page,
        pageSize
      );

      setEntities(response.data);
      setTotal(response.total);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Failed to fetch';
      setError(message);
      console.error('[hooks/entity] Fetch error:', err);
    } finally {
      setLoading(false);
    }
  }, [filters, page, userProfile?.institution_id]);

  useEffect(() => {
    fetchEntities();
  }, [fetchEntities]);

  return {
    entities,
    loading,
    error,
    total,
    page,
    setPage,
    pageSize,
    refetch: fetchEntities
  };
}

Detailed patterns: See references/hooks-patterns.md

Step 4: Components Layer (90-120 min)

Required components in app/(routes)/[module]/_components/:

  1. data-table-schema.ts - Zod validation schema
  2. columns.tsx - TanStack Table column definitions
  3. [entity]-data-table.tsx - Data table wrapper component
  4. [entity]-form.tsx - Create/Edit form with React Hook Form
  5. [entity]-filters.tsx - Search and filter controls
  6. row-actions.tsx - Edit/Delete action menu

Complete component code examples: See references/component-patterns.md

Step 5: Pages Layer (45-60 min)

Required pages:

  1. page.tsx - List view with filters and data table
  2. new/page.tsx - Create form page
  3. [id]/edit/page.tsx - Edit form page
  4. [id]/page.tsx - Detail view page (optional)

Each page must include:

  • ContentLayout wrapper
  • Breadcrumb navigation
  • PermissionGuard for access control
  • Proper error handling and loading states

Complete page code examples: See references/page-patterns.md

Step 6: Permissions & Navigation (20-30 min)

1. Define Permissions

Add to lib/sidebarMenuLink.ts:

export const MENU_PERMISSIONS = {
  '[module].[entity].view': ['super_admin', 'admin', 'faculty'],
  '[module].[entity].create': ['super_admin', 'admin'],
  '[module].[entity].edit': ['super_admin', 'admin'],
  '[module].[entity].delete': ['super_admin'],
};

2. Add Menu Item

{
  groupLabel: 'Module',
  menus: [
    {
      label: 'Entities',
      href: '/[module]/entities',
      permission: '[module].[entity].view'
    }
  ]
}

3. Apply Guards

<PermissionGuard module="[module].[entity]" action="create">
  <Button>Create</Button>
</PermissionGuard>

// Or use shorthand components
<CanCreate module="[module].[entity]">
  <Button>Create</Button>
</CanCreate>

Complete permission setup: See references/permission-patterns.md

File Structure

app/(routes)/[module]/
├── page.tsx                    # List view
├── new/page.tsx               # Create form
├── [id]/
│   ├── page.tsx              # Detail view (optional)
│   └── edit/page.tsx         # Edit form
└── _components/
    ├── data-table-schema.ts
    ├── columns.tsx
    ├── [entity]-data-table.tsx
    ├── [entity]-form.tsx
    ├── [entity]-filters.tsx
    └── row-actions.tsx

lib/services/[module]/
└── [entity]-service.ts

hooks/[module]/
└── use-[entity].ts

types/
└── [module].ts

Development Standards

Naming Conventions

  • Files: kebab-case (entity-name.tsx)
  • Components: PascalCase (EntityForm)
  • Functions: camelCase (fetchEntities)
  • Types: PascalCase (CreateEntityDto)
  • Hooks: use prefix (useEntities)
  • Services: Service suffix (EntityService)

Logging Format

Always use module prefix:

console.log('[module/entity] Action completed:', details);
console.warn('[module/entity] Validation warning:', data);
console.error('[module/entity] Error occurred:', error);

TypeScript Rules

  • Use strict mode (no implicit any)
  • No any types - use unknown with type guards
  • Explicit return types on service methods
  • Use DTOs for all API boundaries
  • Interfaces over types for extensibility

Error Handling

  • Try-catch in all service methods
  • User-friendly messages in UI (toast notifications)
  • Console errors with module prefix for debugging
  • Graceful degradation for failed operations

Quality Checklist

Before marking module as complete:

  • All TypeScript types are strict (no any)
  • Service methods have proper error handling
  • RLS policies tested in Supabase dashboard
  • All CRUD operations tested manually
  • Permissions applied to all pages and actions
  • Loading states display correctly
  • Error messages are user-friendly
  • Console logs use correct module prefix
  • Navigation flows logically between pages
  • Mobile responsive (test on small screens)
  • Forms validate inputs correctly
  • Data table pagination works
  • Search and filters function properly

Time Estimates

  • Database Setup: 30-45 minutes
  • Types Layer: 20-30 minutes
  • Service Layer: 45-60 minutes
  • Hooks Layer: 30-45 minutes
  • Components Layer: 90-120 minutes
  • Pages Layer: 45-60 minutes
  • Permissions: 20-30 minutes
  • Testing: 30-45 minutes

Total: 5-7 hours for a complete module

Reference Files

For detailed implementation patterns and complete code examples:

  • references/architecture-patterns.md - Overall architecture and design patterns
  • references/database-patterns.md - Database schema, indexes, and RLS policies
  • references/typescript-patterns.md - Type definitions and DTO patterns
  • references/service-patterns.md - Service layer with complex queries
  • references/hooks-patterns.md - Custom hooks with advanced patterns
  • references/component-patterns.md - Complete UI component examples
  • references/page-patterns.md - Page structure with all routing patterns
  • references/permission-patterns.md - Permission system integration