API Routes
Create custom backend endpoints to handle complex logic, external integrations, and server-side operations.
Overview
API routes are serverless functions that run on the server:
- Handle form submissions
- Integrate with third-party APIs
- Process payments
- Send emails
- Any server-side logic
Creating API Routes
Using AI
Ask the AI to create endpoints:
Create an API endpoint that sends a welcome email when a user signs up
Add an API route to process Stripe webhook events
File Structure
API routes live in app/api/:
app/
└── api/
├── contact/
│ └── route.ts → /api/contact
├── posts/
│ ├── route.ts → /api/posts
│ └── [id]/
│ └── route.ts → /api/posts/:id
└── webhook/
└── stripe/
└── route.ts → /api/webhook/stripe
Basic Example
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello World!' });
}
HTTP Methods
Handle different HTTP methods:
// app/api/posts/route.ts
// GET /api/posts - List posts
export async function GET(request: Request) {
const posts = await db.from('posts').select('*');
return Response.json(posts);
}
// POST /api/posts - Create post
export async function POST(request: Request) {
const body = await request.json();
const post = await db.from('posts').insert(body).select();
return Response.json(post, { status: 201 });
}
// app/api/posts/[id]/route.ts
// GET /api/posts/:id - Get single post
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db
.from('posts')
.select('*')
.eq('id', params.id)
.single();
if (!post) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(post);
}
// PUT /api/posts/:id - Update post
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const post = await db
.from('posts')
.update(body)
.eq('id', params.id)
.select();
return Response.json(post);
}
// DELETE /api/posts/:id - Delete post
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.from('posts').delete().eq('id', params.id);
return new Response(null, { status: 204 });
}
Request Handling
Query Parameters
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const limit = searchParams.get('limit') || '10';
const posts = await db
.from('posts')
.select('*')
.range((+page - 1) * +limit, +page * +limit - 1);
return Response.json(posts);
}
Request Body
export async function POST(request: Request) {
const body = await request.json();
const { name, email, message } = body;
// Validate
if (!email || !message) {
return Response.json(
{ error: 'Email and message required' },
{ status: 400 }
);
}
// Process...
}
Headers
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
const userAgent = request.headers.get('user-agent');
// Return custom headers
return Response.json(data, {
headers: {
'Cache-Control': 'max-age=3600',
'X-Custom-Header': 'value'
}
});
}
Authentication
Verify User
import { createClient } from '@/lib/supabase/server';
export async function GET(request: Request) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return Response.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// User is authenticated
return Response.json({ userId: user.id });
}
Error Handling
export async function POST(request: Request) {
try {
const body = await request.json();
// Validate input
if (!body.email) {
return Response.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
// Process
const result = await processData(body);
return Response.json(result);
} catch (error) {
console.error('API error:', error);
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Environment Variables
Access secrets in API routes:
export async function POST(request: Request) {
const apiKey = process.env.EXTERNAL_API_KEY;
const response = await fetch('https://external-api.com/data', {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
return Response.json(await response.json());
}
Webhooks
Handle external service webhooks:
// app/api/webhook/stripe/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error) {
return Response.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object);
break;
}
return Response.json({ received: true });
}
Calling API Routes
From Client Components
// Fetch data
const response = await fetch('/api/posts');
const posts = await response.json();
// Submit form
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, message })
});
With Server Actions
For forms and mutations, prefer Server Actions:
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
await db.from('posts').insert({ title, content });
revalidatePath('/posts');
}
Best Practices
Validate All Input
Never trust client data:
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1)
});
export async function POST(request: Request) {
const body = await request.json();
const validated = PostSchema.parse(body);
// Use validated data
}
Rate Limiting
Protect against abuse:
import { rateLimit } from '@/lib/rate-limit';
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for');
const { success } = await rateLimit(ip);
if (!success) {
return Response.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// Continue...
}
Keep Routes Small
Complex logic should be in separate utility files:
import { sendWelcomeEmail } from '@/lib/email';
import { createUserAccount } from '@/lib/users';
export async function POST(request: Request) {
const body = await request.json();
const user = await createUserAccount(body);
await sendWelcomeEmail(user.email);
return Response.json(user);
}