Next.js 14+ with App Router provides powerful tools for building production-ready REST APIs. Combined with TypeScript's type safety, you can create scalable, maintainable backend services with excellent developer experience.
Why Next.js for REST APIs?
While Next.js is primarily known as a React framework, its App Router introduces route handlers that make it an excellent choice for building APIs:
- Colocation: Keep your API routes alongside frontend code
- Type Safety: Share TypeScript types between frontend and backend
- Edge Runtime: Deploy API routes to the edge for low latency
- Built-in Middleware: Request/response handling without external frameworks
Setting Up Route Handlers
Route handlers in Next.js are defined using route.ts files in the app/api directory:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}Request Validation with Zod
Never trust user input. Use Zod for runtime validation and TypeScript type inference:
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = userSchema.parse(body);
// validated is now type-safe
const user = await createUser(validated);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Validation failed', details: error.issues },
{ status: 400 }
);
}
throw error;
}
}Error Handling Best Practices
Consistent error handling improves API reliability and developer experience:
class APIError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
}
}
export async function GET(request: NextRequest) {
try {
const data = await fetchData();
return NextResponse.json(data);
} catch (error) {
if (error instanceof APIError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
);
}
// Log unexpected errors
console.error('Unexpected API error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Rate Limiting
Protect your API from abuse with rate limiting. Here's a simple in-memory implementation:
const rateLimit = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(ip: string, limit = 100, window = 60000): boolean {
const now = Date.now();
const record = rateLimit.get(ip);
if (!record || now > record.resetAt) {
rateLimit.set(ip, { count: 1, resetAt: now + window });
return true;
}
if (record.count >= limit) {
return false;
}
record.count++;
return true;
}
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') || 'unknown';
if (!checkRateLimit(ip)) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// Process request...
}Authentication with JWT
Implement JWT-based authentication for secure API access:
import jwt from 'jsonwebtoken';
export async function POST(request: NextRequest) {
const token = request.headers.get('Authorization')?.split(' ')[1];
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
const userId = (payload as any).sub;
// Use userId for authorized request
const data = await fetchUserData(userId);
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
}Database Integration
Use Prisma for type-safe database access that integrates perfectly with TypeScript:
// app/api/posts/route.ts
import { prisma } from '@/lib/prisma';
import { z } from 'zod';
const postSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const validated = postSchema.parse(body);
const post = await prisma.post.create({
data: validated,
include: { author: true },
});
return NextResponse.json(post, { status: 201 });
}Testing Your API
Write integration tests to ensure API reliability:
import { POST } from './route';
import { NextRequest } from 'next/server';
describe('POST /api/users', () => {
it('creates user with valid data', async () => {
const request = new NextRequest('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({
email: 'test@example.com',
name: 'Test User',
}),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toHaveProperty('id');
expect(data.email).toBe('test@example.com');
});
it('rejects invalid email', async () => {
const request = new NextRequest('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({
email: 'invalid-email',
name: 'Test User',
}),
});
const response = await POST(request);
expect(response.status).toBe(400);
});
});Deployment Considerations
When deploying your Next.js API to production:
- Environment Variables: Use
.env.productionfor secrets - CORS: Configure allowed origins for cross-origin requests
- Logging: Implement structured logging (e.g., Pino, Winston)
- Monitoring: Use Sentry or similar for error tracking
- Caching: Implement Redis for session storage and caching
Performance Optimization
Optimize API performance with these strategies:
- Use Edge Runtime for globally distributed APIs
- Implement response caching with
next.revalidate - Use database connection pooling
- Minimize middleware overhead
- Consider API pagination for large datasets
Conclusion
Next.js provides a robust foundation for building production-ready REST APIs. By combining TypeScript's type safety, Zod validation, proper error handling, and modern security practices, you can create APIs that are both developer-friendly and production-ready.
At Aivoma, we run Build, Integration & Migration Kits that cover API platform engineering, observability, and rollout planning. If you need a senior team to harden your Next.js APIs or migrate legacy endpoints, we can plug in quickly.
Book a working session and we’ll send back a KPI-driven plan within 48 hours.
Cost planning
Software Project Estimate Calculator
Quickly map MVP scope, delivery window, and engineering cost with sliders tuned for AI-native builds.
Open the calculatorBrevo double opt-in delivers the PDF recap + assumptions.