API Routes and Middleware
API Routes and Middleware
Section titled “API Routes and Middleware”Introduction
Section titled “Introduction”API Routes in Next.js allow you to build your API endpoints as part of your Next.js application, enabling full-stack development within a single project. This topic covers how to create API routes, handle different HTTP methods, and use middleware for common functionality.
Why do we need this?
Section titled “Why do we need this?”Building a full-stack application often requires a backend to handle data operations, authentication, and business logic. Instead of setting up a separate server or service, Next.js API routes let you keep your frontend and backend code together, simplifying development and deployment.
Problem Statement
Section titled “Problem Statement”Managing a separate backend service introduces complexity in deployment, versioning, and communication between frontend and backend. Developers need to maintain two codebases, handle CORS issues, and manage separate hosting configurations.
Real World Story
Section titled “Real World Story”You’re building a blog platform and need endpoints to create, read, update, and delete posts. Instead of setting up a separate Node.js/Express server, you create API routes in pages/api/posts/ that handle these operations directly within your Next.js project.
Real World Analogy
Section titled “Real World Analogy”Think of API routes like adding a kitchen to your apartment:
- Instead of going to a restaurant (separate backend) every time you want to cook, you have a kitchen (API routes) right in your home (Next.js app)
- You can prepare meals (handle requests) whenever you need without leaving your building
- You still have the option to order out (use external services) when it makes sense
- Your grocery shopping (data fetching) can be done locally or from external sources
Visual Explanation
Section titled “Visual Explanation”pages/└── api/ ├── hello.js → GET /api/hello ├── users/ │ ├── index.js → GET/POST /api/users │ └── [id].js → GET/PUT/DELETE /api/users/:id ├── posts/ │ ├── [id].js → GET/PUT/DELETE /api/posts/:id │ └── comments/ │ └── [id].js → POST /api/posts/:id/comments ├── auth/ │ ├── login.js → POST /api/auth/login │ └── logout.js → POST /api/auth/logout └── _middleware.js → Applies to all API routesInternal Working
Section titled “Internal Working”When a request arrives at /api/*:
- Next.js routes it to the API routes handler (instead of the page renderer)
- The request is forwarded to the corresponding file in
pages/api/ - The file should export a default function that handles the request
- The handler function receives
req(IncomingMessage) andres(ServerResponse) objects - You can read request data (query, body, headers) and write response data (status, headers, body)
- Middleware can be applied to run logic before the handler
Technical Explanation
Section titled “Technical Explanation”API Route Structure
Section titled “API Route Structure”- All files inside
pages/api/are treated as API endpoints - The file path determines the endpoint URL (similar to pages routing)
- Only files with
.js,.jsx,.ts,.tsxextensions are considered - API routes are server-only - they never send code to the browser
Request Handling
Section titled “Request Handling”API route handlers follow this pattern:
export default function handler(req, res) { // req: IncomingMessage (http module) // res: ServerResponse (http module)
// Get query parameters: req.query // Get body (with body parsing): req.body // Get headers: req.headers // Get HTTP method: req.method
// Set response: res.statusCode, res.setHeader(), res.write(), res.end() // Or use helpers: res.status(200).json(data)}HTTP Methods
Section titled “HTTP Methods”Handle different methods in the same handler:
export default function handler(req, res) { if (req.method === 'GET') { // Handle GET request } else if (req.method === 'POST') { // Handle POST request } else if (req.method === 'PUT') { // Handle PUT request } else if (req.method === 'DELETE') { // Handle DELETE request } else { // Handle unsupported methods res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']); res.status(405).end('Method Not Allowed'); }}Body Parsing
Section titled “Body Parsing”Next.js automatically parses:
application/json(viareq.body)application/x-www-form-urlencoded(viareq.body)multipart/form-data(viareq.body- but files need special handling)
For raw bodies or custom parsing, you need to disable body parsing:
export const config = { api: { bodyParser: false }};Response Helpers
Section titled “Response Helpers”// Send JSONres.status(200).json({ success: true, data });
// Send textres.status(200).send('Hello World');
// Send status onlyres.sendStatus(204);
// Redirectres.redirect(301, '/new-location');
// Set headerres.setHeader('Content-Type', 'application/json');
// Set cookieres.setHeader('Set-Cookie', 'cookieName=value; Max-Age=3600; Path=/');
// End responseres.end();Mermaid Diagram 1: API Route Request Flow
Section titled “Mermaid Diagram 1: API Route Request Flow”sequenceDiagram participant Browser participant NextJS participant APIRoute participant Database
Browser->>NextJS: POST /api/users Note over Browser: Includes JSON body NextJS->>APIRoute: Route to pages/api/users.js APIRoute->>Database: Insert user Database-->>APIRoute: Success APIRoute->>NextJS: Return JSON response NextJS-->>Browser: 201 Created with user dataMermaid Diagram 2: API Route Middleware Flow
Section titled “Mermaid Diagram 2: API Route Middleware Flow”flowchart TD A[Incoming Request] --> B{API Route?} B -->|Yes| C[Run Middleware] C --> D{Match Route?} D -->|Yes| E[Execute Handler] D -->|No| F[Return 404] B -->|No| G[Continue to Page Routing] E --> H{Send Response?} H -->|Yes| I[Return Response] H -->|No| J[Next Middleware/Handler]Example: Simple API Route
Section titled “Example: Simple API Route”- Create
pages/api/hello.js:
export default function handler(req, res) { res.status(200).json({ message: 'Hello World' });}Example: Handling Different Methods
Section titled “Example: Handling Different Methods”- Create
pages/api/tasks.js:
export default function handler(req, res) { if (req.method === 'GET') { // Get all tasks res.status(200).json({ tasks: [] }); } else if (req.method === 'POST') { // Create new task const { title = req.body.title; // Save to database res.status(201).json({ id: 1, title, completed: false }); } else { res.setHeader('Allow', ['GET', 'POST']); res.status(405).end('Method Not Allowed'); }}Example: Dynamic API Route
Section titled “Example: Dynamic API Route”- Create
pages/api/posts/[id].js:
export default function handler(req, res) { const { id } = req.query;
if (req.method === 'GET') { // Get post by ID // In real app: fetch from database if (id === '1') { res.status(200).json({ id: 1, title: 'First Post', content: 'Hello' }); } else { res.status(404).json({ error: 'Post not found' }); } } else if (req.method === 'PUT') { // Update post by ID // In real app: validate and update in database res.status(200).json({ id, ...req.body }); } else if (req.method === 'DELETE') { // Delete post by ID // In real app: delete from database res.status(204).end(); } else { res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); res.status(405).end('Method Not Allowed'); }}Example: Middleware with _middleware.js
Section titled “Example: Middleware with _middleware.js”- Create
pages/api/_middleware.js:
export default function middleware(req, res) { // This runs for every API request
// Example: Logging console.log(`${req.method} ${req.url}`);
// Example: Authentication check const authHeader = req.headers.authorization; if (!authHeader || authHeader !== 'Bearer valid-token') { res.status(401).json({ error: 'Unauthorized' }); return false; // Stop request from proceeding to handler }
// Return true to continue to the next middleware/handler return true;}- Create
pages/api/protected.js:
export default function handler(req, res) { // This will only run if middleware returns true res.status(200).json({ message: 'This is protected data' });}Example: Connecting to Database
Section titled “Example: Connecting to Database”- Create
pages/api/users/[id].js:
import { connectToDatabase } from '../../lib/mongodb';
export default function handler(req, res) { const { id } = req.query;
switch (req.method) { case 'GET': // Get user by ID return getUser(req, res, id); case 'PUT': // Update user return updateUser(req, res, id); case 'DELETE': // Delete user return deleteUser(req, res, id); default: res.setHeader('Allow', ['GET', 'PUT', 'DELETE']); return res.status(405).end('Method Not Allowed'); }}
async function getUser(req, res, id) { try { const { db } = await connectToDatabase(); const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
if (!user) { return res.status(404).json({ error: 'User not found' }); }
return res.status(200).json(user); } catch (error) { console.error('Error fetching user:', error); return res.status(500).json({ error: 'Internal Server Error' }); }}
async function updateUser(req, res, id) { try { const { db } = await connectToDatabase(); const { name, email } = req.body;
const result = await db.collection('users').updateOne( { _id: new ObjectId(id) }, { $set: { name, email } } );
if (result.matchedCount === 0) { return res.status(404).json({ error: 'User not found' }); }
return res.status(200).json({ message: 'User updated' }); } catch (error) { console.error('Error updating user:', error); return res.status(500).json({ error: 'Internal Server Error' }); }}
async function deleteUser(req, res, id) { try { const { db } = await connectToDatabase(); const result = await db.collection('users').deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 0) { return res.status(404).json({ error: 'User not found' }); }
return res.status(200).json({ message: 'User deleted' }); } catch (error) { console.error('Error deleting user:', error); return res.status(500).json({ error: 'Internal Server Error' }); }}Example: Using External Libraries
Section titled “Example: Using External Libraries”- Install required packages:
npm install axios sqlite3 - Create
pages/api/weather.js:
import axios from 'axios';
export default function handler(req, res) { const { city } = req.query;
if (!city) { return res.status(400).json({ error: 'City parameter is required' }); }
try { const response = await axios.get( `https://api.openweathermap.org/data/2.5/weather`, { params: { q: city, appid: process.env.OPENWEATHER_API_KEY, units: 'metric' } } );
return res.status(200).json(response.data); } catch (error) { if (error.response) { return res.status(error.response.status).json({ error: error.response.data.message }); } return res.status(500).json({ error: 'Failed to fetch weather data' }); }}Example: File Upload Handling
Section titled “Example: File Upload Handling”- Create
pages/api/upload.js:
import { promisify } from 'util';import { pipeline } from 'stream';import fs from 'fs';import path from 'path';
// Disable body parsing for file uploadsexport const config = { api: { bodyParser: false }};
export default async function handler(req, res) { if (req.method !== 'POST') { res.setHeader('Allow', ['POST']); return res.status(405).end('Method Not Allowed'); }
// Prepare to save file const uploadDir = './public/uploads'; if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); }
const fileName = `upload-${Date.now()}-${Math.round(Math.random() * 1E9)}`; const filePath = path.join(uploadDir, fileName);
// Create write stream const writeStream = fs.createWriteStream(filePath);
// Parse multipart/form-data and pipe to file try { await new Promise((resolve, reject) => { pipeline( req, // In a real app, you'd use a proper multipart parser like busboy // For demo, we'll assume raw binary data (not recommended for production) writeStream, (err) => { if (err) { console.error('File upload failed:', error); reject(err); } else { resolve(); } } ); });
res.status(200).json({ message: 'File uploaded successfully', fileName, filePath: `/uploads/${fileName}` }); } catch (error) { console.error('File upload error:', error); res.status(500).json({ error: 'File upload failed' }); }}Production Example
Section titled “Production Example”In production, API routes are optimized:
- Serverless Functions: On Vercel, each API route becomes a serverless function
- Server Mode: On self-hosted Node.js servers, API routes are handled by the same server
- Edge Functions: On Vercel Edge, API routes can run at the edge for lower latency
- **Automatic scaling based on traffic
- Scaling: Automatic scaling based on traffic (serverless) or configurable (server mode)
- Cold Starts: Consider initialization time for serverless functions
- Execution Limits: Be aware of execution time limits (e.g., 10s on Vercel free tier)
- Environment Variables: Use
process.envfor configuration and secrets - Logging: Use console.log/error - logs are captured by the platform
- Timeouts: Configure via
vercel.jsonor platform-specific settings - Dependencies: Bundle only what’s needed to minimize cold start time
Best Practices
Section titled “Best Practices”- Keep API routes focused: Each endpoint should have a single responsibility
- Use proper HTTP status codes: 200 for success, 201 for created, 400 for bad request, 401 for unauthorized, 403 for forbidden, 404 for not found, 409 for conflict, 500 for server error
- Validate input: Always validate and sanitize incoming data
- Handle errors gracefully: Don’t leak stack traces to clients
- Use asynchronous operations: Avoid blocking the event loop
- Implement rate limiting: Especially for public endpoints
- Secure sensitive endpoints: Use authentication and authorization
- Log appropriately: Use different levels (info, warn, error) for different situations
- Cache when appropriate: Use caching headers for GET requests
- Version your API: Consider
/api/v1/prefix for breaking changes - Use environment variables: For configuration, secrets, and feature flags
- Test thoroughly: Include unit tests for your API handlers
- Document your API: Use tools like Swagger or write documentation
- Handle CORS: If needed, set appropriate headers (though same-origin requests don’t need it)
- Optimize dependencies: Only import what you need in each API route
Common Mistakes
Section titled “Common Mistakes”- Forgetting to return/responses: Leads to hanging requests
- Not handling all HTTP methods: Results in 405 errors for unsupported methods
- Blocking the event loop: Long-running operations without async/await
- Exposing sensitive data: Errors or logs that include secrets
- Ignoring request size limits: Large payloads can cause issues
- Not validating input: Leads to security vulnerabilities (injection, etc.)
- Forgetting to set Content-Type: Clients may misinterpret response format
- Using synchronous file operations: Blocks the event loop
- Not handling connection errors: Especially important for database operations
- Using inappropriate status codes: Misleading clients about operation results
- Not setting appropriate headers: Missing caching, security, or CORS headers
- Hardcoding configuration: Should use environment variables
- Ignoring rate limits: Can lead to service abuse or crashes
- Not cleaning up resources: Especially important for file handles or database connections
- Over-engineering simple endpoints: Sometimes a simple handler is best
Performance Notes
Section titled “Performance Notes”- Response time: Critical for user experience; aim for <200ms for simple operations
- Database connections: Use connection pooling to avoid overhead
- Memory usage: Be cautious with large file uploads or data processing
- Concurrency: Handle multiple requests efficiently with async/await
- Caching: Implement caching strategies for repetitive read operations
- Payload size: Limit request/response sizes to prevent abuse
- Compression: Consider enabling gzip compression for responses
- CDN integration: For static assets, consider serving via CDN
- Monitoring: Track response times, error rates, and throughput
- Scaling: Understand how your hosting platform scales API routes
- Cold starts: For serverless, minimize initialization code to reduce latency
- Batching: Combine operations when possible to reduce database round trips
Security Notes
Section titled “Security Notes”- Authentication: Verify user identity before granting access to sensitive data
- Authorization: Check if authenticated user has permission for the requested action
- Input validation: Validate all inputs (query, body, params) against expected schema
- Output encoding: If returning user-generated content, prevent XSS
- SQL/NoSQL injection: Use parameterized queries or ORM protections
- Request size limits: Configure maximum body size to prevent DoS
- Rate limiting: Implement to prevent abuse and brute force attacks
- CORS: Configure appropriately if serving to different origins
- Security headers: Set headers like:
Content-Security-PolicyX-Frame-OptionsX-Content-Type-OptionsStrict-Transport-Security(HTTPS only)Referrer-PolicyX-XSS-Protection
- Dependencies: Keep updated to avoid known vulnerabilities
- Secrets management: Never hardcode secrets; use environment variables
- Error handling: Don’t leak internal details in error responses
- File uploads: Validate file types, scan for malware, store securely
- Timeouts: Set appropriate timeouts to prevent resource exhaustion
SEO Considerations
Section titled “SEO Considerations”While API routes don’t directly affect SEO, they impact:
- Performance: Fast API responses improve page load times (Core Web Vitals)
- Content availability: Ensures data is available for server-side rendering
- Reliability: Reduces errors that could lead to incomplete page rendering
- Security: Prevents vulnerabilities that could compromise site integrity
- Accessibility: Ensures all users can access content regardless of device or network
Interview Questions
Section titled “Interview Questions”- How do you create an API route in Next.js?
- How do you handle different HTTP methods in a single API route?
- How do you access query parameters, request body, and headers in an API route?
- What is the purpose of the
configobject in an API route? - How would you handle file uploads in an API route?
- How do you connect to a database from an API route?
- What middleware options exist for API routes in Next.js?
- How would you implement authentication for API routes?
- How do you handle errors in API routes?
- How can you disable body parsing in an API route?
- What is the difference between
res.send(),res.json(), andres.end()? - How would you set cookies in an API route response?
- How do you handle CORS in Next.js API routes?
- What are the best practices for securing API routes?
- How do you test API routes in Next.js?
-
Which file creates the API endpoint
/api/users? a)pages/api/users.jsb)pages/api/users/index.jsc) Both a and b d) Neither a nor bAnswer
-
How do you access the request body in an API route (by default)? a)
req.bodyb)req.queryc)req.paramsd)req.dataAnswer
-
Which method disables automatic body parsing in an API route? a)
export const config = { api: { bodyParser: false } }b)export const config = { bodyParser: false }c)export const config = { api: { parseBody: false } }d)export const config = { parseBody: false }]]>Answer
a) `export const config = { api: { bodyParser: false } }`