| name | api-organization |
| description | Explains the standardized API organization pattern for this codebase. Use when creating new API endpoints, API clients, or modifying existing API structure. Covers the 5-file system (endpoint-types, endpoints, api-client, admin-api-client, protected-endpoints), role-based access patterns (admin vs regular users), and TypeScript type safety across the API layer. All API code lives in src/lib/api/ following this exact pattern. |
API Organization Pattern
This skill defines the standardized API organization pattern used throughout this codebase. All external API integrations follow the same 5-file structure for consistency, type safety, and maintainability.
When to Use This Skill
Use this skill when:
- Creating new API endpoints or integrations
- Adding new API categories or domains
- Implementing role-based API access (admin vs regular users)
- Modifying existing API structure
- Setting up authentication for API calls
- Creating type-safe API wrappers
Core Principles
- Single Source of Truth - All API URLs defined once in
endpoints.ts - Type Safety - Full TypeScript coverage from params to responses
- Role-Based Access - Separate clients for regular vs admin endpoints
- Centralized Auth - Authentication handled automatically by clients
- DRY Code - Reusable client functions, no duplicate endpoint definitions
The 5-File System
All API code lives in src/lib/api/ with exactly these files:
src/lib/api/
├── endpoint-types.ts # TypeScript types for all endpoints
├── endpoints.ts # URL definitions organized by domain
├── api-client.ts # Generic authenticated API client
├── admin-api-client.ts # Admin-only API client with role checks
└── protected-endpoints.ts # Type-safe wrapper functions
File Purposes
1. endpoint-types.ts
- Define all TypeScript interfaces for API data
- Organize types by domain (e.g., audiences, instances, membership)
- Include param types, response types, and request body DTOs
- Provide utility types for extracting types
See references/endpoint-types-pattern.md for detailed structure.
2. endpoints.ts
- Define all API endpoint URLs in one place
- Organize by feature/domain matching types
- Use functions that accept parameters and return URLs
- Support environment variable base URLs
See references/endpoints-pattern.md for detailed structure.
3. api-client.ts
- Generic API client with automatic authentication
- Error parsing and handling
- Support for GET, POST, PUT, DELETE methods
- Server-side only ('use server')
See references/api-client-pattern.md for implementation.
4. admin-api-client.ts
- Admin-specific API client
- Role validation (Admin/SuperAdmin groups)
- Permission checking functions
- Throws AdminAuthError if unauthorized
See references/admin-api-client-pattern.md for implementation.
5. protected-endpoints.ts
- Type-safe wrapper functions combining URLs + types + clients
- Organized to match endpoint-types structure
- Provides clean import:
import { api } from '@/lib/api/protected-endpoints' - No 'use server' directive (exports objects)
See references/protected-endpoints-pattern.md for detailed structure.
Authentication System
This application uses Supabase Auth for authentication. All API requests require authentication via Supabase access tokens.
Supabase Client Structure
src/lib/supabase/
├── client.ts # Browser-side Supabase client
├── server.ts # Server-side Supabase client (RSC, Server Actions)
└── middleware.ts # Middleware helper for auth cookie refresh
Auth Flow
- User authenticates via Supabase (email/password, OAuth, etc.)
- Supabase stores session in httpOnly cookies
- Middleware refreshes session on every request
- Server components/actions use
createClient()fromserver.ts - API client extracts access token from session automatically
See references/supabase-auth-integration.md for detailed implementation.
Role-Based Access Pattern
Regular User Endpoints
Use api-client.ts functions with automatic Supabase auth:
import { apiGet, apiPost } from '@/lib/api/api-client';
// Access token extracted from Supabase session automatically
const data = await apiGet<ResponseType>(url);
Admin-Only Endpoints
Use admin-api-client.ts functions with role validation:
import { adminApiRequest, checkAdminPermission } from '@/lib/api/admin-api-client';
// Validate admin access first (checks user role from database)
await checkAdminPermission(); // Throws if not admin
// Make admin API request
const data = await adminApiRequest<ResponseType>(url, options);
Admin Roles
Admin roles are stored in the users table:
users.role='admin'- Standard admin accessusers.role='member'- Regular user access
First user in a family is automatically assigned admin role.
Adding New API Endpoints
Follow this exact order when integrating a new API into the application:
Step 1: Define Types in endpoint-types.ts
// 1. Define response type(s)
export interface ResourceItem {
id: string;
name: string;
// ... other fields from your API response
}
// 2. Define request DTO(s) for mutations
export interface CreateResourceDto {
name: string;
// ... fields required to create
}
// 3. Add parameter types to EndpointParams interface
export interface EndpointParams {
// ... existing domains
resources: {
list: void; // No params needed
get: { id: string }; // Requires ID
create: void; // Body in request, not params
update: { id: string };
delete: { id: string };
};
}
// 4. Add response types to EndpointResponses interface
export interface EndpointResponses {
// ... existing domains
resources: {
list: ResourceItem[];
get: ResourceItem;
create: ResourceItem;
update: ResourceItem;
delete: void;
};
}
// 5. Add request body types to EndpointBodies interface (if needed)
export interface EndpointBodies {
// ... existing domains
resources: {
create: CreateResourceDto;
update: CreateResourceDto;
};
}
Step 2: Define URLs in endpoints.ts
export const API_ENDPOINTS = {
// ... existing categories
resources: {
list: () => `${API_BASE}/api/resources`,
get: (id: string) => `${API_BASE}/api/resources/${id}`,
create: () => `${API_BASE}/api/resources`,
update: (id: string) => `${API_BASE}/api/resources/${id}`,
delete: (id: string) => `${API_BASE}/api/resources/${id}`,
},
};
Step 3: Add Wrapper in protected-endpoints.ts
export const api = {
// ... existing domains
resources: {
async list(): Promise<ResourceItem[]> {
return apiGet<ResourceItem[]>(
API_ENDPOINTS.resources.list()
);
},
async get(id: string): Promise<ResourceItem> {
return apiGet<ResourceItem>(
API_ENDPOINTS.resources.get(id)
);
},
async create(data: CreateResourceDto): Promise<ResourceItem> {
return apiPost<ResourceItem, CreateResourceDto>(
API_ENDPOINTS.resources.create(),
data
);
},
async update(id: string, data: CreateResourceDto): Promise<ResourceItem> {
return apiPut<ResourceItem, CreateResourceDto>(
API_ENDPOINTS.resources.update(id),
data
);
},
async delete(id: string): Promise<void> {
return apiDelete<void>(
API_ENDPOINTS.resources.delete(id)
);
},
},
};
Step 4: Use in Application Code
'use server';
import { api } from '@/lib/api/protected-endpoints';
// In a server component or server action
const resources = await api.resources.list();
const resource = await api.resources.get(id);
const newResource = await api.resources.create({
name: 'New Resource'
});
Naming Conventions
Endpoint Operations
list/listAll- GET multiple itemsget- GET single itemcreate- POST new itemupdate- PUT/PATCH existing itemdelete- DELETE item
Type Naming
- Response types: PascalCase describing entity (e.g.,
AudienceListItem) - DTOs: PascalCase with suffix (e.g.,
CreateUserDto,UpdateSettingsDto) - Interfaces match plural for collections, singular for items
Error Handling
All API clients handle errors automatically:
try {
const data = await api.myFeature.get(id);
} catch (error) {
// Error already parsed and formatted
console.error('API error:', error.message);
}
Common error types:
AdminAuthError- Admin permission deniedAdminApiError- Admin API request failed- Generic errors from
api-clientwith parsed messages
Authentication Flow
Regular Endpoints
- Client calls protected endpoint wrapper (e.g.,
api.resources.list()) - Wrapper calls apiGet/apiPost/etc from api-client
- api-client calls
getAuthHeaders() - getAuthHeaders uses Supabase server client to get session:
const supabase = await createClient(); const { data: { session } } = await supabase.auth.getSession(); const accessToken = session?.access_token; - Access token added to Authorization header
- Request sent with automatic authentication
Admin Endpoints
Same flow as above, but with additional role check:
- Call
checkAdminPermission()first - checkAdminPermission queries
userstable for current user's role - Throws
AdminAuthErrorif role is not 'admin' - Then proceeds with normal authentication flow
Best Practices
- Never hardcode URLs - Always use
API_ENDPOINTS - Always define types first - Types drive implementation
- One wrapper per endpoint - Keep protected-endpoints clean
- Group by domain - Match structure across all 5 files
- Use helper functions -
apiGet,apiPost, etc. handle auth - Validate admin access early - Call
checkAdminPermission()first - Document complex endpoints - Add JSDoc comments
- Handle errors gracefully - API clients provide good error messages
Environment Variables
Required for Supabase Auth
NEXT_PUBLIC_SUPABASE_URL- Supabase project URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY- Supabase anonymous public key
Required for API Clients
INSTANCE_API_URLorAPI_URL- Base URL for external API endpointsNEXT_PUBLIC_SITE_URL- Site URL for redirects (optional, defaults to localhost:3000)
Database
- Supabase connection is handled automatically via the Supabase client
- No manual connection strings needed
Migration from Other Patterns
If migrating existing API code to this pattern:
- Extract all endpoint URLs to
endpoints.ts - Create types in
endpoint-types.tsfor params/responses - Replace direct fetch calls with
apiGet/apiPost/etc - Add wrappers to
protected-endpoints.ts - Update imports to use
apiobject
Example migration:
// Before (scattered fetch calls)
const response = await fetch(`${API_BASE}/api/resources/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
const resource = await response.json();
// After (centralized pattern)
import { api } from '@/lib/api/protected-endpoints';
const resource = await api.resources.get(id); // Auth automatic, types included
References
See reference files for detailed implementation patterns:
references/supabase-auth-integration.md- Supabase auth setup and integrationreferences/endpoint-types-pattern.md- Type definition structurereferences/endpoints-pattern.md- URL organization patternreferences/api-client-pattern.md- Generic client implementation with Supabasereferences/admin-api-client-pattern.md- Admin client with role-based accessreferences/protected-endpoints-pattern.md- Wrapper function patterns