| 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:
- Types Layer (
types/) - TypeScript interfaces and DTOs - Service Layer (
lib/services/) - Supabase database operations and business logic - Hooks Layer (
hooks/) - React state management and data fetching - Components Layer (
_components/) - Reusable UI components with Shadcn/UI - Pages Layer (
app/(routes)/) - Route handlers with Server Components
Workflow Decision Tree
Start here for every new module:
Is database schema defined?
- NO → Read
references/database-patterns.mdand create schema first - YES → Continue to step 2
- NO → Read
Are TypeScript types needed?
- YES → Follow Step 1: Types Layer below
- NO → Skip to step 3
Need database operations?
- YES → Follow Step 2: Service Layer below
- NO → Skip to step 4
Need state management?
- YES → Follow Step 3: Hooks Layer below
- NO → Skip to step 5
Need UI components?
- YES → Follow Step 4: Components Layer below
- NO → Skip to step 6
Need pages/routes?
- YES → Follow Step 5: Pages Layer below
- Then proceed to step 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/:
data-table-schema.ts- Zod validation schemacolumns.tsx- TanStack Table column definitions[entity]-data-table.tsx- Data table wrapper component[entity]-form.tsx- Create/Edit form with React Hook Form[entity]-filters.tsx- Search and filter controlsrow-actions.tsx- Edit/Delete action menu
Complete component code examples: See references/component-patterns.md
Step 5: Pages Layer (45-60 min)
Required pages:
page.tsx- List view with filters and data tablenew/page.tsx- Create form page[id]/edit/page.tsx- Edit form page[id]/page.tsx- Detail view page (optional)
Each page must include:
ContentLayoutwrapperBreadcrumbnavigationPermissionGuardfor 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:
useprefix (useEntities) - Services:
Servicesuffix (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
anytypes - useunknownwith 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 patternsreferences/database-patterns.md- Database schema, indexes, and RLS policiesreferences/typescript-patterns.md- Type definitions and DTO patternsreferences/service-patterns.md- Service layer with complex queriesreferences/hooks-patterns.md- Custom hooks with advanced patternsreferences/component-patterns.md- Complete UI component examplesreferences/page-patterns.md- Page structure with all routing patternsreferences/permission-patterns.md- Permission system integration