API Overview
Patterns shared across every API route.
All API routes live under app/api/ and follow the Next.js App Router convention — each folder with a route.ts file is an endpoint. Most routes follow a common pattern. This page covers the shared building blocks.
Route categories
| Path | Purpose |
|---|---|
/api/projects/* | Project CRUD and listing |
/api/categories/* | Category management |
/api/user/* | Current user's profile and settings |
/api/admin/* | Admin-only operations |
/api/payments/* | Checkout creation |
/api/webhooks/stripe | Stripe webhook handler |
/api/ai/generate | AI description generator (feature-flagged) |
/api/analytics/* | Event tracking endpoints |
/api/cron/* | Scheduled jobs (protected by CRON_SECRET) |
Shared patterns
1. Auth check
Always verify with getUser(), never getSession():
import { getSupabaseAdmin } from '@/lib/supabase/client';
const supabase = getSupabaseAdmin();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}2. Feature guard
Return 404 if the whole feature is disabled:
import { featureGuard } from '@/lib/features';
const guard = featureGuard('comments');
if (guard) return guard; // 404 response if comments are off3. Rate limit
Every write endpoint should rate-limit to prevent abuse:
import { checkRateLimit, createRateLimitResponse } from '@/lib/rate-limit';
const limit = await checkRateLimit(request, 'submission');
if (!limit.success) return createRateLimitResponse(limit);Tiers available:
| Tier | Use for |
|---|---|
general | Most reads, safe writes |
voting | Upvotes, ratings |
auth | Login-adjacent endpoints |
submission | Creating projects |
admin | Admin actions |
analytics | Tracking events |
4. Input validation (Zod)
Schemas live in lib/validations/schemas.ts:
import { ProjectSubmissionSchema } from '@/lib/validations/schemas';
const body = await request.json();
const parsed = ProjectSubmissionSchema.safeParse(body);
if (!parsed.success) {
return Response.json(
{ error: 'Invalid input', details: parsed.error.format() },
{ status: 400 }
);
}
// parsed.data is fully typed5. Database access
Use the db singleton for most things:
import { db } from '@/lib/supabase/database';
const project = await db.insertOne('apps', { ... });See Database Layer API.
6. Webhook/external notifications
Fire outbound webhooks after important events:
import { webhookEvents } from '@/lib/webhooks';
await webhookEvents.projectSubmitted(project);Response shape
Successful responses:
{ "data": { ... } }Error responses:
{ "error": "Something went wrong", "code": "INVALID_INPUT" }Status codes to expect:
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 400 | Bad request (validation failed) |
| 401 | Unauthorized (not logged in) |
| 403 | Forbidden (logged in but no permission) |
| 404 | Not found or feature disabled |
| 429 | Rate-limited |
| 500 | Unhandled error |
Example route skeleton
import { NextRequest } from 'next/server';
import { getSupabaseAdmin } from '@/lib/supabase/client';
import { checkRateLimit, createRateLimitResponse } from '@/lib/rate-limit';
import { featureGuard } from '@/lib/features';
import { db } from '@/lib/supabase/database';
import { z } from 'zod';
const BodySchema = z.object({ name: z.string().min(1) });
export async function POST(req: NextRequest) {
const guard = featureGuard('comments');
if (guard) return guard;
const limit = await checkRateLimit(req, 'general');
if (!limit.success) return createRateLimitResponse(limit);
const { data: { user } } = await getSupabaseAdmin().auth.getUser();
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
const body = BodySchema.safeParse(await req.json());
if (!body.success) return Response.json({ error: 'Invalid input' }, { status: 400 });
const { insertedId } = await db.insertOne('apps', {
...body.data,
user_id: user.id,
status: 'pending',
});
return Response.json({ data: { id: insertedId } }, { status: 201 });
}With AI
Add a new API route to this DirectoryKit project.
Endpoint: {METHOD} {/api/your-path} Purpose: {what the route does, 1 sentence} Auth: {public | logged-in | admin} Feature flag to guard (if any): {flag name from features.config.ts, or "none"} Rate limit tier: {general | voting | auth | submission | admin | analytics}
Request body (if any) — define a Zod schema. Fields:
- {field name: type: constraints}
Database writes (if any): which table, which fields. Response shape: {what gets returned}
Follow the template in app/api/ and CLAUDE.md. Use:
getSupabaseAdmin().auth.getUser()for auth check (never getSession)featureGuard(...)before anything elsecheckRateLimit(request, '<tier>')right after- Zod
.safeParse()for body validation with 400 on failure db.findOne/insertOne/updateOnefor DB ops- Response.json with consistent { data } / { error } shape