API Route Performance
Optimize Next.js API routes and server functions for faster response times
API route performance directly impacts TTFB and user experience. This guide covers techniques to make your API routes faster and more efficient.
Route Handler Basics
export async function GET(request: Request) {
const users = await getUsers();
return Response.json(users);
}Response Caching
Cache-Control Headers
export async function GET() {
const products = await getProducts();
return Response.json(products, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
});
}| Directive | Meaning |
|---|---|
public | CDN can cache |
private | Browser only |
s-maxage=60 | CDN caches for 60s |
max-age=60 | Browser caches for 60s |
stale-while-revalidate=300 | Serve stale while refreshing |
Dynamic Caching
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const products = await getProductsByCategory(category);
// Cache based on category
const cacheTime = category === 'featured' ? 300 : 60;
return Response.json(products, {
headers: {
'Cache-Control': `public, s-maxage=${cacheTime}`,
},
});
}Database Query Optimization
Connection Pooling
import { Pool } from 'pg';
// Reuse connection pool across requests
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30000,
});
export async function query(text: string, params?: any[]) {
return pool.query(text, params);
}Avoid N+1 Queries
// ❌ Bad: N+1 queries
export async function GET() {
const posts = await db.query('SELECT * FROM posts');
for (const post of posts) {
post.author = await db.query(
'SELECT * FROM users WHERE id = $1',
[post.authorId]
);
}
return Response.json(posts);
}
// ✅ Good: Single query with JOIN
export async function GET() {
const posts = await db.query(`
SELECT posts.*, users.name as authorName, users.avatar as authorAvatar
FROM posts
JOIN users ON posts.author_id = users.id
ORDER BY posts.created_at DESC
LIMIT 50
`);
return Response.json(posts);
}Query Result Caching
import { unstable_cache } from 'next/cache';
const getCachedProducts = unstable_cache(
async (category: string) => {
return db.query('SELECT * FROM products WHERE category = $1', [category]);
},
['products'],
{ revalidate: 60 }
);
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category') || 'all';
const products = await getCachedProducts(category);
return Response.json(products);
}Streaming Responses
Streaming JSON
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode('['));
let first = true;
for await (const item of fetchItemsGenerator()) {
if (!first) controller.enqueue(encoder.encode(','));
controller.enqueue(encoder.encode(JSON.stringify(item)));
first = false;
}
controller.enqueue(encoder.encode(']'));
controller.close();
},
});
return new Response(stream, {
headers: { 'Content-Type': 'application/json' },
});
}Server-Sent Events
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const subscription = dataSource.subscribe((data) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
});
// Cleanup on close
return () => subscription.unsubscribe();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}Edge Runtime
// Runs at edge for lower latency
export const runtime = 'edge';
export async function GET(request: Request) {
// Edge-compatible code only
const data = await fetch('https://api.example.com/data');
return Response.json(await data.json());
}Edge Limitations
| Not Available | Edge-Compatible Alternative |
|---|---|
fs module | Fetch from object storage (S3, R2) |
Traditional SQL pools (pg, mysql2) | Edge databases (PlanetScale, Turso, Neon) |
| Long-running processes | KV stores (Vercel KV, Upstash) |
| Node.js-specific APIs | Web standard APIs (fetch, crypto, Headers) |
Compression
Automatic with Vercel
Vercel automatically compresses responses with gzip/brotli.
Manual Compression
import { gzip } from 'zlib';
import { promisify } from 'util';
const gzipAsync = promisify(gzip);
export async function GET() {
const data = await getLargeDataset();
const json = JSON.stringify(data);
const compressed = await gzipAsync(json);
return new Response(compressed, {
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
},
});
}Request Handling
Parse Body Once
// ❌ Bad: Parsing body multiple times
export async function POST(request: Request) {
const body1 = await request.json(); // First parse
validateBody(await request.json()); // Error! Stream already consumed
}
// ✅ Good: Parse once, reuse
export async function POST(request: Request) {
const body = await request.json();
validateBody(body);
processData(body);
}Early Validation
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
export async function POST(request: Request) {
const body = await request.json();
// Validate early, fail fast
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: result.error.issues },
{ status: 400 }
);
}
// Proceed with validated data
const user = await createUser(result.data);
return Response.json(user, { status: 201 });
}Parallel Processing
export async function GET(request: Request) {
// ❌ Sequential: 300ms + 200ms + 100ms = 600ms
const users = await getUsers();
const products = await getProducts();
const stats = await getStats();
// ✅ Parallel: max(300ms, 200ms, 100ms) = 300ms
const [users, products, stats] = await Promise.all([
getUsers(),
getProducts(),
getStats(),
]);
return Response.json({ users, products, stats });
}Error Handling
export async function GET(request: Request) {
try {
const data = await fetchData();
return Response.json(data);
} catch (error) {
console.error('API Error:', error);
if (error instanceof DatabaseError) {
return Response.json(
{ error: 'Database unavailable' },
{ status: 503 }
);
}
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Monitoring
export async function GET(request: Request) {
const start = performance.now();
try {
const result = await processRequest(request);
const duration = performance.now() - start;
console.log(`API response time: ${duration.toFixed(2)}ms`);
return Response.json(result, {
headers: {
'Server-Timing': `total;dur=${duration.toFixed(2)}`,
},
});
} catch (error) {
const duration = performance.now() - start;
console.error(`API error after ${duration.toFixed(2)}ms:`, error);
throw error;
}
}Checklist
Caching
Set appropriate Cache-Control headers Tell CDN and browsers how to cache responses.
Use unstable_cache for DB queries Cache expensive database operations.
Implement stale-while-revalidate Serve fast stale content while refreshing.
Consider CDN caching Cache at edge for global performance.
Database
Use connection pooling Don't create new connections per request.
Avoid N+1 queries Use JOINs or batch queries instead of loops.
Add appropriate indexes Query performance depends on proper indexing.
Cache frequent queries Don't hit the database for the same data repeatedly.
Response
Enable compression Gzip or Brotli reduces transfer size significantly.
Stream large responses Don't wait to collect all data before responding.
Use pagination Return manageable chunks instead of everything.
Return only needed fields Don't send entire records when clients need three fields.
Architecture
Use Edge Runtime when possible Lower latency for simple operations.
Parallelize independent operations Use Promise.all for concurrent fetches.
Validate early Fail fast on bad input before doing expensive work.
Handle errors gracefully Return appropriate status codes and messages.
Performance Targets
| Metric | Target | Action if Exceeded |
|---|---|---|
| Response time | < 200ms | Add caching, optimize queries |
| TTFB | < 100ms | Use Edge Runtime |
| Payload size | < 50KB | Paginate, compress |
| Error rate | < 1% | Add retry logic, better error handling |