Codapult provides four complementary ways to expose server logic: REST API routes, tRPC, GraphQL, and server actions. All share the same auth, validation, and rate-limiting primitives.
API Routes
All routes live in src/app/api/ and must follow a strict five-step pattern:
import { NextResponse } from 'next/server';
import { getAppSession } from '@/lib/auth';
import { checkRateLimit } from '@/lib/rate-limit';
import { someSchema } from '@/lib/validation';
export async function POST(req: Request) {
// 1. Auth check
const session = await getAppSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Rate limiting
const { allowed, remaining, resetAt } = checkRateLimit(`scope:${session.user.id}`, {
limit: 30,
windowSeconds: 60,
});
if (!allowed) {
return NextResponse.json(
{ error: 'Too many requests. Please wait a moment.' },
{
status: 429,
headers: {
'Retry-After': String(Math.ceil((resetAt - Date.now()) / 1000)),
},
},
);
}
// 3. Input validation (Zod)
const body = await req.json();
const data = someSchema.parse(body);
// 4. Business logic
const result = await doSomething(data);
// 5. Response
return NextResponse.json({ result }, { headers: { 'X-RateLimit-Remaining': String(remaining) } });
}
Rules:
- Always return
{ error: string }for errors — never expose stack traces or internal details. - Attach rate-limit headers:
X-RateLimit-RemainingandRetry-After. - Use Zod schemas from
@/lib/validation.tsfor input validation.
tRPC
Codapult includes a full tRPC v11 setup with TanStack React Query for end-to-end type-safe API calls. See the dedicated tRPC documentation for router creation, prefetching, and client usage.
GraphQL
An optional graphql-yoga server is available at /api/graphql. See the dedicated GraphQL documentation for setup, schema definition, and when to use it vs tRPC.
Server Actions
All mutations go through server actions in src/lib/actions/. Every action follows this pattern:
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { getAppSession } from '@/lib/auth';
import { checkRateLimit } from '@/lib/rate-limit';
import { updateProfileSchema } from '@/lib/validation';
export async function updateProfile(formData: FormData): Promise<void> {
const session = await getAppSession();
if (!session) redirect('/sign-in');
const { allowed } = checkRateLimit(`settings:${session.user.id}`, {
limit: 10,
windowSeconds: 60,
});
if (!allowed) throw new Error('Too many requests. Please wait a moment.');
const data = updateProfileSchema.parse({
name: formData.get('name'),
});
// ... DB update ...
revalidatePath('/dashboard/settings');
}
Available Actions
| Action | File | Description |
|---|---|---|
createCheckout | billing.ts | Create a Stripe/LS checkout session |
openCustomerPortal | billing.ts | Open the billing portal |
updateProfile | settings.ts | Update user name and preferences |
deleteAccount | settings.ts | Delete the current user account |
completeOnboarding | onboarding.ts | Mark onboarding as complete |
changeUserRole | admin.ts | Admin: change a user's role |
deleteUser | admin.ts | Admin: delete a user |
createApiKeyAction | api-keys.ts | Generate a new API key |
deleteApiKeyAction | api-keys.ts | Revoke an API key |
API Versioning
Codapult supports URL-prefix versioning via a proxy layer in src/proxy.ts.
How It Works
Requests to /api/v1/users are rewritten to /api/users internally. The version is extracted and tracked through the request lifecycle.
GET /api/v1/users → internally routed to → GET /api/users
Version Endpoint
GET /api/version returns the current API version status:
{
"current": "v1",
"supported": ["v1", "v2"],
"versions": {
"v1": { "deprecated": false },
"v2": { "deprecated": false }
}
}
Response Headers
Every versioned response includes:
| Header | Description |
|---|---|
X-API-Version | The resolved API version |
Deprecation | true if the version is deprecated |
Sunset | ISO date when the version will be removed |
Link | URL of the successor version |
Clients can send X-API-Version as a request header to opt into version-specific behavior.
Configuration
Version definitions live in src/lib/api-version.ts:
export const API_VERSIONS = ['v1', 'v2'] as const;
export const CURRENT_VERSION: ApiVersion = 'v1';
export const VERSION_INFO: Record<ApiVersion, { deprecated: boolean; sunset?: string }> = {
v1: { deprecated: false },
v2: { deprecated: false },
};
Rate Limiting
Codapult includes a sliding-window rate limiter in src/lib/rate-limit.ts.
Usage
import { checkRateLimit } from '@/lib/rate-limit';
const { allowed, remaining, resetAt } = checkRateLimit(`chat:${session.user.id}`, {
limit: 30,
windowSeconds: 60,
});
The checkRateLimit function returns:
| Field | Type | Description |
|---|---|---|
allowed | boolean | Whether the request is within limits |
remaining | number | Remaining requests in the window |
resetAt | number | Timestamp (ms) when the window resets |
Where It's Applied
| Scope | Limit | Window |
|---|---|---|
| Auth endpoints | Configured per route | 60s |
| AI chat | 30 requests | 60s |
| Billing actions | 5 requests | 60s |
| Admin actions | 10 requests | 60s |
| GraphQL | 60 requests | 60s |
| Plugin routes | 30 requests | 60s |
Note: The built-in limiter is in-memory and not shared across serverless instances. For production, swap it with a Redis-backed implementation (e.g. Upstash Rate Limit).