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.
Build complete feature modules following MyJKKN's standardized 5-layer architecture for Next.js 15 + Supabase applications.
Every module follows a strict 5-layer pattern:
types/) - TypeScript interfaces and DTOslib/services/) - Supabase database operations and business logichooks/) - React state management and data fetching_components/) - Reusable UI components with Shadcn/UIapp/(routes)/) - Route handlers with Server ComponentsStart here for every new module:
Is database schema defined?
references/database-patterns.md and create schema firstAre TypeScript types needed?
Need database operations?
Need state management?
Need UI components?
Need pages/routes?
Configure permissions
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
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
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
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 menuComplete component code examples: See references/component-patterns.md
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:
ContentLayout wrapperBreadcrumb navigationPermissionGuard for access controlComplete page code examples: See references/page-patterns.md
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'],
};
{
groupLabel: 'Module',
menus: [
{
label: 'Entities',
href: '/[module]/entities',
permission: '[module].[entity].view'
}
]
}
<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
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
kebab-case (entity-name.tsx)PascalCase (EntityForm)camelCase (fetchEntities)PascalCase (CreateEntityDto)use prefix (useEntities)Service suffix (EntityService)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);
any types - use unknown with type guardsBefore marking module as complete:
any)Total: 5-7 hours for a complete module
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