API Backend
Build a production-ready REST API with authentication, validation, rate limiting, and comprehensive error handling.
Features
Section titled “Features”- RESTful API design
- JWT authentication
- Input validation with Zod
- Rate limiting
- Error handling
- API documentation
- CORS support
Project Structure
Section titled “Project Structure”Directoryapp/
Directoryapi/
- middleware.ts # API-wide middleware
Directoryv1/
- middleware.ts # Version middleware
Directoryusers/
- route.ts # GET, POST /api/v1/users
Directory[id]/
- route.ts # GET, PUT, DELETE /api/v1/users/:id
Directoryposts/
- route.ts
Directory[id]/
- route.ts
Directoryauth/
Directorylogin/
- route.ts
Directoryregister/
- route.ts
Directoryrefresh/
- route.ts
API Middleware
Section titled “API Middleware”CORS and Rate Limiting
Section titled “CORS and Rate Limiting”// app/api/middleware.tsimport type { Middleware } from '@cloudwerk/core';
export const middleware: Middleware = async (request, next, context) => { // CORS headers const origin = request.headers.get('Origin'); const allowedOrigins = ['https://myapp.com', 'http://localhost:3000'];
const corsHeaders = { 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', };
if (origin && allowedOrigins.includes(origin)) { corsHeaders['Access-Control-Allow-Origin'] = origin; }
// Handle preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); }
// Rate limiting const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown'; const rateLimitKey = `rate_limit:api:${ip}`;
const requests = parseInt(await context.kv.get(rateLimitKey) ?? '0'); const limit = 100; // requests per minute
if (requests >= limit) { return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Retry-After': '60', }, }); }
await context.kv.put(rateLimitKey, String(requests + 1), { expirationTtl: 60, });
// Execute request const response = await next(request);
// Add CORS headers to response Object.entries(corsHeaders).forEach(([key, value]) => { response.headers.set(key, value); });
// Add rate limit headers response.headers.set('X-RateLimit-Limit', String(limit)); response.headers.set('X-RateLimit-Remaining', String(limit - requests - 1));
return response;};Authentication Middleware
Section titled “Authentication Middleware”// app/api/v1/middleware.tsimport type { Middleware } from '@cloudwerk/core';import { jwtVerify } from 'jose';
// Public routes that don't require authenticationconst publicRoutes = [ '/api/v1/auth/login', '/api/v1/auth/register', '/api/v1/auth/refresh',];
export const middleware: Middleware = async (request, next, context) => { const url = new URL(request.url);
// Skip auth for public routes if (publicRoutes.some(route => url.pathname.startsWith(route))) { return next(request); }
// Get token from Authorization header const authHeader = request.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return new Response(JSON.stringify({ error: 'Unauthorized', message: 'Missing or invalid Authorization header', }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); }
const token = authHeader.slice(7);
try { const secret = new TextEncoder().encode(context.env.JWT_SECRET); const { payload } = await jwtVerify(token, secret);
// Attach user to context context.auth.userId = payload.sub as string; context.auth.user = payload;
} catch (error) { return new Response(JSON.stringify({ error: 'Unauthorized', message: 'Invalid or expired token', }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); }
return next(request);};Authentication Endpoints
Section titled “Authentication Endpoints”// app/api/v1/auth/login/route.tsimport { json } from '@cloudwerk/core';import { z } from 'zod';import { SignJWT } from 'jose';import { verify } from '@cloudwerk/auth';
const LoginSchema = z.object({ email: z.string().email(), password: z.string().min(1),});
export async function POST(request: Request, { context }: CloudwerkHandlerContext) { const body = await request.json(); const result = LoginSchema.safeParse(body);
if (!result.success) { return json({ error: 'Validation failed', details: result.error.flatten().fieldErrors, }, { status: 400 }); }
const { email, password } = result.data;
// Find user const user = await context.db .selectFrom('users') .where('email', '=', email) .executeTakeFirst();
if (!user) { return json({ error: 'Invalid credentials' }, { status: 401 }); }
// Verify password const valid = await verify(password, user.password_hash); if (!valid) { return json({ error: 'Invalid credentials' }, { status: 401 }); }
// Generate tokens const secret = new TextEncoder().encode(context.env.JWT_SECRET);
const accessToken = await new SignJWT({ sub: user.id, email: user.email, }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') .sign(secret);
const refreshToken = await new SignJWT({ sub: user.id, type: 'refresh', }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(secret);
// Store refresh token await context.kv.put(`refresh_token:${user.id}`, refreshToken, { expirationTtl: 7 * 24 * 60 * 60, });
return json({ accessToken, refreshToken, expiresIn: 900, user: { id: user.id, email: user.email, name: user.name, }, });}Refresh Token
Section titled “Refresh Token”// app/api/v1/auth/refresh/route.tsimport { json } from '@cloudwerk/core';import { jwtVerify, SignJWT } from 'jose';
export async function POST(request: Request, { context }: CloudwerkHandlerContext) { const { refreshToken } = await request.json();
if (!refreshToken) { return json({ error: 'Refresh token required' }, { status: 400 }); }
const secret = new TextEncoder().encode(context.env.JWT_SECRET);
try { const { payload } = await jwtVerify(refreshToken, secret);
if (payload.type !== 'refresh') { return json({ error: 'Invalid token type' }, { status: 401 }); }
// Verify token is still valid in KV const storedToken = await context.kv.get(`refresh_token:${payload.sub}`); if (storedToken !== refreshToken) { return json({ error: 'Token revoked' }, { status: 401 }); }
// Get user const user = await context.db .selectFrom('users') .where('id', '=', payload.sub as string) .executeTakeFirst();
if (!user) { return json({ error: 'User not found' }, { status: 401 }); }
// Generate new access token const accessToken = await new SignJWT({ sub: user.id, email: user.email, }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') .sign(secret);
return json({ accessToken, expiresIn: 900, });
} catch (error) { return json({ error: 'Invalid refresh token' }, { status: 401 }); }}CRUD Endpoints
Section titled “CRUD Endpoints”Users Resource
Section titled “Users Resource”// app/api/v1/users/route.tsimport { json } from '@cloudwerk/core';import { z } from 'zod';
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(2).max(100), role: z.enum(['user', 'admin']).default('user'),});
const ListQuerySchema = z.object({ page: z.coerce.number().min(1).default(1), limit: z.coerce.number().min(1).max(100).default(20), sort: z.enum(['created_at', 'name', 'email']).default('created_at'), order: z.enum(['asc', 'desc']).default('desc'),});
export async function GET(request: Request, { context }: CloudwerkHandlerContext) { const url = new URL(request.url); const query = Object.fromEntries(url.searchParams);
const params = ListQuerySchema.safeParse(query); if (!params.success) { return json({ error: 'Invalid query parameters', details: params.error.flatten().fieldErrors, }, { status: 400 }); }
const { page, limit, sort, order } = params.data; const offset = (page - 1) * limit;
const [users, countResult] = await Promise.all([ context.db .selectFrom('users') .select(['id', 'email', 'name', 'role', 'created_at']) .orderBy(sort, order) .limit(limit) .offset(offset) .execute(), context.db .selectFrom('users') .select(context.db.fn.count('id').as('total')) .executeTakeFirst(), ]);
const total = countResult?.total ?? 0;
return json({ data: users, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, });}
export async function POST(request: Request, { context }: CloudwerkHandlerContext) { const body = await request.json(); const result = CreateUserSchema.safeParse(body);
if (!result.success) { return json({ error: 'Validation failed', details: result.error.flatten().fieldErrors, }, { status: 400 }); }
// Check if email already exists const existing = await context.db .selectFrom('users') .where('email', '=', result.data.email) .executeTakeFirst();
if (existing) { return json({ error: 'Email already in use' }, { status: 409 }); }
const user = await context.db .insertInto('users') .values({ id: crypto.randomUUID(), ...result.data, }) .returning(['id', 'email', 'name', 'role', 'created_at']) .executeTakeFirst();
return json({ data: user }, { status: 201 });}Single User Resource
Section titled “Single User Resource”// app/api/v1/users/[id]/route.tsimport { json } from '@cloudwerk/core';import { z } from 'zod';
const UpdateUserSchema = z.object({ name: z.string().min(2).max(100).optional(), role: z.enum(['user', 'admin']).optional(),});
export async function GET(request: Request, { params, context }: CloudwerkHandlerContext) { const user = await context.db .selectFrom('users') .select(['id', 'email', 'name', 'role', 'created_at']) .where('id', '=', params.id) .executeTakeFirst();
if (!user) { return json({ error: 'User not found' }, { status: 404 }); }
return json({ data: user });}
export async function PUT(request: Request, { params, context }: CloudwerkHandlerContext) { const body = await request.json(); const result = UpdateUserSchema.safeParse(body);
if (!result.success) { return json({ error: 'Validation failed', details: result.error.flatten().fieldErrors, }, { status: 400 }); }
const user = await context.db .updateTable('users') .set({ ...result.data, updated_at: new Date().toISOString(), }) .where('id', '=', params.id) .returning(['id', 'email', 'name', 'role', 'created_at']) .executeTakeFirst();
if (!user) { return json({ error: 'User not found' }, { status: 404 }); }
return json({ data: user });}
export async function DELETE(request: Request, { params, context }: CloudwerkHandlerContext) { const deleted = await context.db .deleteFrom('users') .where('id', '=', params.id) .returning(['id']) .executeTakeFirst();
if (!deleted) { return json({ error: 'User not found' }, { status: 404 }); }
return new Response(null, { status: 204 });}Error Handling
Section titled “Error Handling”Global Error Handler
Section titled “Global Error Handler”// app/api/error.tsexport function handleError(error: unknown): Response { console.error('API Error:', error);
if (error instanceof z.ZodError) { return new Response(JSON.stringify({ error: 'Validation failed', details: error.flatten().fieldErrors, }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); }
if (error instanceof NotFoundError) { return new Response(JSON.stringify({ error: 'Not found', message: error.message, }), { status: 404, headers: { 'Content-Type': 'application/json' }, }); }
// Generic error return new Response(JSON.stringify({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? error.message : undefined, }), { status: 500, headers: { 'Content-Type': 'application/json' }, });}Next Steps
Section titled “Next Steps”- Add API versioning strategy
- Implement webhook notifications
- Add request logging and analytics
- Create OpenAPI documentation
- Add integration tests
Related Examples
Section titled “Related Examples”- Blog Example - Full-stack application
- Real-time Chat - WebSocket API