API Conventions
API Conventions
Every API route in the foundation follows a strict, consistent pattern. This makes it easy for both developers and AI tools to add new endpoints without breaking conventions.
File Structure
app/api/
tenants/
route.ts # GET /api/tenants
switch/
route.ts # POST /api/tenants/switch
habits/
route.ts # GET, POST /api/habits
[id]/
route.ts # GET, PUT, DELETE /api/habits/:idRoute Pattern
Every API route must follow this pattern:
import { requireUser } from '@/lib/auth/requireUser';
import { withAuthDb } from '@/lib/db/withAuthDb';
import { ok, fail, created, notFound } from '@/lib/api/responses';
export async function GET() {
const { user, session } = await requireUser();
if (!user || !session) {
return fail('UNAUTHENTICATED', 'Authentication required', 401);
}
try {
const data = await withAuthDb(session.access_token, (db) =>
queryFunction(db)
);
return ok(data);
} catch (error) {
console.error('[GET /api/resource]', error);
return fail('INTERNAL_SERVER_ERROR', 'Failed to fetch', 500);
}
}Response Format
Success
{ "data": {} }Error
{ "error": { "code": "ERROR_CODE", "message": "Human readable message" } }Response Helpers
Located in lib/api/responses.ts:
| Helper | Status Code |
|---|---|
ok(data) | 200 |
created(data) | 201 |
notFound(message) | 404 |
fail(code, message, status) | Custom |
HTTP Status Codes
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
400 | Validation error |
401 | Unauthenticated |
403 | Forbidden |
404 | Not found |
500 | Server error |
Validation
Use Zod for input validation at the API boundary. Return 400 with VALIDATION_ERROR on failure:
const parsed = schema.safeParse(body);
if (!parsed.success) {
return fail('VALIDATION_ERROR', 'Invalid input', 400);
}Tenant-Scoped Routes
For routes that operate on tenant data, always get the tenantId from the server-side cookie:
import { getActiveTenantId } from '@/lib/tenants/activeTenant';
export async function GET() {
const { user, session } = await requireUser();
if (!user || !session) return fail('UNAUTHENTICATED', 'Auth required', 401);
const tenantId = await getActiveTenantId();
if (!tenantId) return fail('BAD_REQUEST', 'No active tenant', 400);
const data = await withAuthDb(session.access_token, (db) =>
getHabits(db, tenantId)
);
return ok(data);
}Rules
- Always use
requireUser()first - Always use
withAuthDb(jwt, ...)for database access - Always get
tenantIdfromgetActiveTenantId()(cookie) - Always pass
tenantIdto tenant-scoped queries - Never use Supabase for data queries
- Never accept
tenant_idoruser_idfrom the client request body - Never implement authorization in code (RLS handles it)
- Always use response helpers from
responses.ts