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/:id

Route 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:

HelperStatus Code
ok(data)200
created(data)201
notFound(message)404
fail(code, message, status)Custom

HTTP Status Codes

CodeMeaning
200Success
201Created
400Validation error
401Unauthenticated
403Forbidden
404Not found
500Server 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

  1. Always use requireUser() first
  2. Always use withAuthDb(jwt, ...) for database access
  3. Always get tenantId from getActiveTenantId() (cookie)
  4. Always pass tenantId to tenant-scoped queries
  5. Never use Supabase for data queries
  6. Never accept tenant_id or user_id from the client request body
  7. Never implement authorization in code (RLS handles it)
  8. Always use response helpers from responses.ts

On this page