Skip to main content

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

PathPurpose
/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/stripeStripe webhook handler
/api/ai/generateAI 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 off

3. 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:

TierUse for
generalMost reads, safe writes
votingUpvotes, ratings
authLogin-adjacent endpoints
submissionCreating projects
adminAdmin actions
analyticsTracking 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 typed

5. 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:

CodeMeaning
200OK
201Created
400Bad request (validation failed)
401Unauthorized (not logged in)
403Forbidden (logged in but no permission)
404Not found or feature disabled
429Rate-limited
500Unhandled error

Example route skeleton

app/api/example/route.ts
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

AI Prompt· Add a new API route

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 else
  • checkRateLimit(request, '<tier>') right after
  • Zod .safeParse() for body validation with 400 on failure
  • db.findOne/insertOne/updateOne for DB ops
  • Response.json with consistent { data } / { error } shape