API Routes
Bài 14 – Cách xây dựng API Routes trong Pages Router và App Router
1. Giới thiệu về API Routes
API Routes cho phép bạn xây dựng backend ngay trong Next.js, phù hợp cho các tác vụ như authentication, xử lý form, upload file nhẹ hoặc tạo endpoint cho client. Next.js hỗ trợ API Routes trong cả Pages Router (/pages/api/*) và App Router (/app/api/*).
Lợi ích của API Routes:
- Không cần setup server riêng biệt
- Tích hợp hoàn hảo với frontend
- Tự động scaling với Next.js
- Hỗ trợ TypeScript out-of-the-box
- Chạy trên Edge Runtime hoặc Node.js
Use cases phổ biến:
- Authentication và authorization
- Form submission và validation
- Webhook handlers
- File upload/download
- API proxy và aggregation
- Database operations
2. Nội dung chính
2.1 API Routes trong Pages Router
Pages Router sử dụng cấu trúc thư mục /pages/api/* với default export là handler function:
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = {
message: string;
users?: any[];
error?: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
// Xử lý theo HTTP method
switch (req.method) {
case 'GET':
// Logic lấy danh sách users
const users = getUsers();
return res.status(200).json({ message: 'Success', users });
case 'POST':
// Logic tạo user mới
const { name, email } = req.body;
const newUser = createUser({ name, email });
return res.status(201).json({ message: 'User created' });
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).json({ error: `Method ${req.method} not allowed` });
}
}
2.2 API Routes trong App Router
App Router sử dụng file route.ts với named exports cho từng HTTP method:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
try {
const users = await getUsers();
return NextResponse.json({
success: true,
data: users,
count: users.length
});
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
// POST /api/users
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, email } = body;
// Validation
if (!name || !email) {
return NextResponse.json(
{ success: false, error: 'Name and email are required' },
{ status: 400 }
);
}
const newUser = await createUser({ name, email });
return NextResponse.json(
{ success: true, data: newUser },
{ status: 201 }
);
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Failed to create user' },
{ status: 500 }
);
}
}
2.3 HTTP Methods và Request Handling
App Router hỗ trợ tất cả HTTP methods:
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/products/[id]
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const product = await getProduct(params.id);
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
}
// PUT /api/products/[id]
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const updatedProduct = await updateProduct(params.id, body);
return NextResponse.json(updatedProduct);
}
// DELETE /api/products/[id]
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await deleteProduct(params.id);
return NextResponse.json(
{ message: 'Product deleted successfully' },
{ status: 200 }
);
}
2.4 Request/Response Processing
Xử lý các loại request body và custom response headers:
// app/api/upload/route.ts
export async function POST(request: NextRequest) {
// Handle multipart/form-data
const formData = await request.formData();
const file = formData.get('file');
if (!file || !(file instanceof File)) {
return NextResponse.json(
{ error: 'No file uploaded' },
{ status: 400 }
);
}
// Process file upload
const uploadResult = await uploadFile(file);
// Custom response headers
return NextResponse.json(
{ success: true, file: uploadResult },
{
status: 201,
headers: {
'X-Upload-ID': uploadResult.id,
'X-Upload-Size': file.size.toString()
}
}
);
}
// app/api/download/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const file = await getFile(params.id);
// Return file as stream
return new NextResponse(file.stream, {
headers: {
'Content-Type': file.contentType,
'Content-Disposition': `attachment; filename="${file.name}"`,
'Content-Length': file.size.toString()
}
});
}
2.5 Ứng dụng thực tế
Authentication middleware:
// app/api/auth/me/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function GET(request: NextRequest) {
// Get token from header
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return NextResponse.json(
{ error: 'No token provided' },
{ status: 401 }
);
}
try {
const user = await verifyToken(token);
return NextResponse.json({ user });
} catch (error) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
}
Webhook handler:
// app/api/webhooks/stripe/route.ts
export async function POST(request: NextRequest) {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
try {
// Verify webhook signature
const event = verifyStripeWebhook(body, signature);
// Process webhook event
await processStripeEvent(event);
return NextResponse.json({ received: true });
} catch (error) {
return NextResponse.json(
{ error: 'Webhook verification failed' },
{ status: 400 }
);
}
}
3.1 RESTful API với Pages Router
Basic GET endpoint:
// pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from 'next';
type Data = {
name: string;
timestamp: string;
method: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({
name: 'John Doe',
timestamp: new Date().toISOString(),
method: req.method || 'GET'
});
}
User management API với validation:
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
interface User {
id: number;
name: string;
email: string;
}
type ErrorResponse = {
error: string;
details?: string;
};
type SuccessResponse = {
message: string;
user?: User;
users?: User[];
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<SuccessResponse | ErrorResponse>
) {
try {
switch (req.method) {
case 'GET':
const users = await getUsers();
return res.status(200).json({
message: 'Users retrieved successfully',
users
});
case 'POST':
const { name, email } = req.body;
// Validation
if (!name || !email) {
return res.status(400).json({
error: 'Missing required fields',
details: 'Name and email are required'
});
}
if (!isValidEmail(email)) {
return res.status(400).json({
error: 'Invalid email format'
});
}
const newUser = await createUser({ name, email });
return res.status(201).json({
message: 'User created successfully',
user: newUser
});
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).json({
error: `Method ${req.method} not allowed`
});
}
} catch (error) {
console.error('API Error:', error);
return res.status(500).json({
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}
// Helper functions
async function getUsers(): Promise<User[]> {
// Database logic here
return [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
}
async function createUser(data: { name: string; email: string }): Promise<User> {
// Database logic here
return { id: Date.now(), name: data.name, email: data.email };
}
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
Contact form API với rate limiting:
// pages/api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import rateLimit from 'express-rate-limit';
// Rate limiting middleware
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: 'Too many requests from this IP'
});
interface ContactForm {
name: string;
email: string;
subject: string;
message: string;
}
interface ApiResponse {
success: boolean;
message?: string;
error?: string;
}
async function handler(
req: NextApiRequest,
res: NextApiResponse<ApiResponse>
) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).json({
success: false,
error: `Method ${req.method} not allowed`
});
}
try {
const { name, email, subject, message }: ContactForm = req.body;
// Validation
if (!name || !email || !subject || !message) {
return res.status(400).json({
success: false,
error: 'All fields are required'
});
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
error: 'Invalid email format'
});
}
// Validate message length
if (message.length < 10 || message.length > 1000) {
return res.status(400).json({
success: false,
error: 'Message must be between 10 and 1000 characters'
});
}
// Send email notification
await sendContactEmail({ name, email, subject, message });
return res.status(200).json({
success: true,
message: 'Your message has been sent successfully!'
});
} catch (error) {
console.error('Contact form error:', error);
return res.status(500).json({
success: false,
error: 'Failed to send message. Please try again later.'
});
}
}
// Apply rate limiting
export default function rateLimitedHandler(
req: NextApiRequest,
res: NextApiResponse<ApiResponse>
) {
return new Promise((resolve, reject) => {
limiter(req as any, res as any, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(handler(req, res));
});
});
}
// Helper function to send email
async function sendContactEmail(data: ContactForm): Promise<void> {
// Email service integration (SendGrid, Nodemailer, etc.)
console.log('Sending contact email:', data);
}
3.2 API Proxy và External Services
API Proxy với error handling:
// pages/api/proxy.ts
import type { NextApiRequest, NextApiResponse } from 'next';
interface ProxyResponse {
success: boolean;
data?: any;
error?: string;
status?: number;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ProxyResponse>
) {
// Only allow GET requests
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET']);
return res.status(405).json({
success: false,
error: `Method ${req.method} not allowed`
});
}
try {
const { url } = req.query;
if (!url || typeof url !== 'string') {
return res.status(400).json({
success: false,
error: 'URL parameter is required'
});
}
// Validate URL
let targetUrl: URL;
try {
targetUrl = new URL(url);
} catch {
return res.status(400).json({
success: false,
error: 'Invalid URL format'
});
}
// Only allow specific domains
const allowedDomains = ['api.example.com', 'jsonplaceholder.typicode.com'];
if (!allowedDomains.includes(targetUrl.hostname)) {
return res.status(403).json({
success: false,
error: 'Domain not allowed'
});
}
// Make external API call
const response = await fetch(targetUrl.toString(), {
method: 'GET',
headers: {
'User-Agent': 'Next.js Proxy',
'Accept': 'application/json'
},
timeout: 10000 // 10 second timeout
});
if (!response.ok) {
return res.status(response.status).json({
success: false,
error: `External API error: ${response.statusText}`,
status: response.status
});
}
const data = await response.json();
// Add CORS headers for client-side usage
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
return res.status(200).json({
success: true,
data
});
} catch (error) {
console.error('Proxy error:', error);
if (error instanceof Error && error.name === 'AbortError') {
return res.status(504).json({
success: false,
error: 'Request timeout'
});
}
return res.status(500).json({
success: false,
error: 'Internal proxy error'
});
}
}
3.3 Advanced App Router Examples
GraphQL API với Server Actions:
// app/api/graphql/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { graphqlHTTP } from 'express-graphql';
import { buildSchema } from 'graphql';
// GraphQL schema
const schema = buildSchema(`
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User
deleteUser(id: ID!): Boolean!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
`);
// Resolvers
const rootValue = {
users: async () => {
return await getUsers();
},
user: async ({ id }: { id: string }) => {
return await getUserById(id);
},
posts: async () => {
return await getPosts();
},
createUser: async ({ name, email }: { name: string; email: string }) => {
return await createUser({ name, email });
},
updateUser: async ({ id, name, email }: { id: string; name?: string; email?: string }) => {
return await updateUser(id, { name, email });
},
deleteUser: async ({ id }: { id: string }) => {
return await deleteUser(id);
}
};
export async function POST(request: NextRequest) {
try {
const { query, variables } = await request.json();
const result = await graphqlHTTP({
schema,
rootValue,
graphiql: process.env.NODE_ENV === 'development'
})({
body: { query, variables }
} as any);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: 'GraphQL execution failed' },
{ status: 500 }
);
}
}
// Helper functions (implement your database logic)
async function getUsers() {
return [];
}
async function getUserById(id: string) {
return null;
}
async function getPosts() {
return [];
}
async function createUser(data: { name: string; email: string }) {
return { id: '1', ...data };
}
async function updateUser(id: string, data: any) {
return { id, ...data };
}
async function deleteUser(id: string) {
return true;
}
Real-time WebSocket API (Server-Sent Events):
// app/api/events/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// Send initial connection message
controller.enqueue(encoder.encode('data: Connected to event stream\n\n'));
// Send periodic updates
const interval = setInterval(() => {
const data = {
timestamp: new Date().toISOString(),
message: 'Server heartbeat',
random: Math.random()
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
}, 5000);
// Cleanup on client disconnect
request.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
}
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
}
});
}
4. Kiến thức trọng tâm
4.1 Khác biệt giữa Pages Router và App Router
| Tính năng | Pages Router | App Router |
|---|---|---|
| Cấu trúc file | /pages/api/*.ts | /app/api/*/route.ts |
| HTTP Methods | Switch trong handler | Named exports riêng biệt |
| Request object | NextApiRequest | NextRequest |
| Response object | NextApiResponse | NextResponse |
| Middleware | Manual implementation | Built-in support |
| Streaming | Limited | Full support |
| TypeScript | Manual typing | Better type inference |
4.2 Xử lý lỗi và Validation
Global error handler cho App Router:
// app/api/error-handler.ts
import { NextResponse } from 'next/server';
export class ApiError extends Error {
constructor(
public statusCode: number,
public message: string,
public details?: any
) {
super(message);
this.name = 'ApiError';
}
}
export function handleApiError(error: unknown) {
console.error('API Error:', error);
if (error instanceof ApiError) {
return NextResponse.json(
{
success: false,
error: error.message,
details: error.details
},
{ status: error.statusCode }
);
}
if (error instanceof Error) {
return NextResponse.json(
{
success: false,
error: error.message || 'Internal server error'
},
{ status: 500 }
);
}
return NextResponse.json(
{
success: false,
error: 'An unknown error occurred'
},
{ status: 500 }
);
}
// Usage in API routes
export async function GET(request: NextRequest) {
try {
const result = await someOperation();
return NextResponse.json({ success: true, data: result });
} catch (error) {
return handleApiError(error);
}
}
Validation middleware:
// app/api/middleware/validation.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
export function validateRequest(schema: z.ZodSchema) {
return async (request: NextRequest) => {
try {
const body = await request.json();
const validatedData = schema.parse(body);
// Create new request with validated data
const newRequest = request.clone();
(newRequest as any).validatedData = validatedData;
return { success: true, data: validatedData, request: newRequest };
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
error: 'Validation failed',
details: error.errors,
response: NextResponse.json(
{
success: false,
error: 'Validation failed',
details: error.errors
},
{ status: 400 }
)
};
}
return {
success: false,
error: 'Invalid request body',
response: NextResponse.json(
{ success: false, error: 'Invalid request body' },
{ status: 400 }
)
};
}
};
}
// Usage example
const createUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).max(120).optional()
});
export async function POST(request: NextRequest) {
const validation = await validateRequest(createUserSchema)(request);
if (!validation.success) {
return validation.response;
}
const { name, email, age } = validation.data;
// Process validated data...
}
4.3 CORS và Middleware
CORS configuration:
// app/api/middleware/cors.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const allowedOrigins = [
'http://localhost:3000',
'https://yourdomain.com',
'https://app.yourdomain.com'
];
export function corsMiddleware(request: NextRequest) {
const origin = request.headers.get('origin');
const response = NextResponse.next();
// Handle preflight requests
if (request.method === 'OPTIONS') {
const preflightHeaders = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400' // 24 hours
};
return NextResponse.json({}, { headers: preflightHeaders });
}
// Set CORS headers for actual requests
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Credentials', 'true');
}
return response;
}
// Apply to specific routes
export async function middleware(request: NextRequest) {
return corsMiddleware(request);
}
export const config = {
matcher: '/api/:path*'
};
4.4 Bảo mật và Best Practices
Environment variables và secrets:
// app/api/config/security.ts
export const securityConfig = {
// Rate limiting
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
},
// JWT configuration
jwt: {
secret: process.env.JWT_SECRET || 'fallback-secret',
expiresIn: '24h',
algorithm: 'HS256'
},
// API keys
apiKeys: {
stripe: process.env.STRIPE_SECRET_KEY,
sendgrid: process.env.SENDGRID_API_KEY,
weather: process.env.WEATHER_API_KEY
},
// CORS
cors: {
allowedOrigins: [
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
]
}
};
// Input sanitization
export function sanitizeInput(input: string): string {
return input
.replace(/[<>]/g, '') // Remove potential HTML tags
.trim()
.slice(0, 1000); // Limit length
}
// API key validation
export function validateApiKey(request: NextRequest): boolean {
const apiKey = request.headers.get('X-API-Key');
const validApiKey = process.env.API_SECRET_KEY;
return apiKey === validApiKey;
}
Rate limiting implementation:
// app/api/middleware/rate-limit.ts
import { NextRequest, NextResponse } from 'next/server';
interface RateLimitStore {
[key: string]: {
count: number;
resetTime: number;
};
}
class RateLimiter {
private store: RateLimitStore = {};
private windowMs: number;
private max: number;
constructor(windowMs: number, max: number) {
this.windowMs = windowMs;
this.max = max;
}
isAllowed(identifier: string): boolean {
const now = Date.now();
const key = identifier;
// Clean up expired entries
if (this.store[key] && now > this.store[key].resetTime) {
delete this.store[key];
}
// Initialize or increment counter
if (!this.store[key]) {
this.store[key] = {
count: 1,
resetTime: now + this.windowMs
};
return true;
}
this.store[key].count++;
// Check if limit exceeded
return this.store[key].count <= this.max;
}
getRemaining(identifier: string): number {
const key = identifier;
if (!this.store[key]) return this.max;
return Math.max(0, this.max - this.store[key].count);
}
getResetTime(identifier: string): number {
const key = identifier;
if (!this.store[key]) return Date.now() + this.windowMs;
return this.store[key].resetTime;
}
}
// Create rate limiter instance
const rateLimiter = new RateLimiter(15 * 60 * 1000, 100); // 100 requests per 15 minutes
export function rateLimitMiddleware(request: NextRequest) {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
if (!rateLimiter.isAllowed(ip)) {
const resetTime = rateLimiter.getResetTime(ip);
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
return NextResponse.json(
{
success: false,
error: 'Too many requests',
retryAfter
},
{
status: 429,
headers: {
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': resetTime.toString(),
'Retry-After': retryAfter.toString()
}
}
);
}
const remaining = rateLimiter.getRemaining(ip);
const response = NextResponse.next();
response.headers.set('X-RateLimit-Remaining', remaining.toString());
response.headers.set('X-RateLimit-Reset', rateLimiter.getResetTime(ip).toString());
return response;
}
4.5 Deploy và Performance
Serverless considerations:
// app/api/health/route.ts
import { NextResponse } from 'next/server';
// Lightweight health check endpoint
export async function GET() {
return NextResponse.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
}
Cold start optimization:
// app/api/optimized/route.ts
import { NextResponse } from 'next/server';
// Keep dependencies minimal
// Avoid heavy computations at module level
// Use dynamic imports for heavy libraries
export async function GET() {
// Lazy load heavy dependencies
const { heavyComputation } = await import('@/lib/heavy-computation');
// Cache frequently used data
const cacheKey = 'api-cache-key';
let data = await getFromCache(cacheKey);
if (!data) {
data = await heavyComputation();
await setCache(cacheKey, data, 300); // Cache for 5 minutes
}
return NextResponse.json({ data });
}
App Router sử dụng file route.ts thay cho pages/api, giúp đồng nhất kiến trúc.
API Route chạy trong môi trường server edge hoặc serverless tùy nền tảng deploy.
Không nên dùng API Routes cho tác vụ nặng: dùng external backend service sẽ bền vững hơn.
5. Bài tập nhanh
Bài 1: Tạo API đăng ký user với validation
Tạo endpoint /api/auth/register với:
- Validation dữ liệu đầu vào (username, email, password)
- Mã hóa password với bcrypt
- Kiểm tra user tồn tại
- Gửi email xác thực
Bài 2: Tạo API CRUD cho bài viết với authorization
Tạo các endpoint cho /api/posts với:
- Authentication middleware
- Authorization (chỉ owner mới được sửa/xóa)
- Validation dữ liệu
- Error handling đầy đủ
Bài 3: Tạo API webhook nhận dữ liệu từ Stripe
Tạo endpoint /api/webhooks/stripe với:
- Xác thực webhook signature
- Xử lý các event types khác nhau
- Cập nhật database theo trạng thái thanh toán
- Error handling và retry logic
6. Kết luận
API Routes mang lại sự tiện lợi khi xây dựng backend tích hợp trong dự án Next.js. Việc hiểu đúng cách tổ chức và giới hạn của API Routes giúp bạn triển khai ứng dụng hiệu quả và bền vững. Key takeaways:
- Chọn đúng Router: Pages Router cho legacy projects, App Router cho projects mới
- Security first: Luôn validate input, implement rate limiting, và protect sensitive data
- Performance matters: Optimize cho serverless environment, minimize cold starts
- Error handling: Implement proper error handling với meaningful error messages
- Testing: Test API routes với various scenarios và edge cases