React & Next.js Topic
Next.js API Routes
Next.js API Routes allow you to build your backend API directly within your Next.js application. Each file in the pages/api directory becomes an API endpoint that serverless functions can handle.
Basic Structure
text
pages/
api/
users/
index.js # /api/users
[id].js # /api/users/123
auth/
login.js # /api/auth/login
middleware.js # Custom middleware
1. Creating Backend Endpoints
Basic API Route
jsx
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from API!' });
}
// Access at: /api/hello
HTTP Methods Handling
jsx
// pages/api/users.js
export default function handler(req, res) {
const { method } = req;
switch (method) {
case 'GET':
handleGet(req, res);
break;
case 'POST':
handlePost(req, res);
break;
case 'PUT':
handlePut(req, res);
break;
case 'DELETE':
handleDelete(req, res);
break;
default:
res.setHeader('Allow', ['GET', 'POST', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
async function handleGet(req, res) {
try {
// Fetch users from database
const users = await fetchUsersFromDB();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
}
async function handlePost(req, res) {
try {
const { name, email } = req.body;
// Validate input
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
// Create user in database
const newUser = await createUserInDB({ name, email });
res.status(201).json(newUser);
} catch (error) {
res.status(500).json({ error: 'Failed to create user' });
}
}
// Mock database functions
async function fetchUsersFromDB() {
return [{ id: 1, name: 'John', email: 'john@example.com' }];
}
async function createUserInDB(userData) {
return { id: Date.now(), ...userData, createdAt: new Date() };
}
Dynamic Routes
jsx
// pages/api/users/[id].js
export default function handler(req, res) {
const {
query: { id },
method
} = req;
switch (method) {
case 'GET':
getUserById(req, res, id);
break;
case 'PUT':
updateUser(req, res, id);
break;
case 'DELETE':
deleteUser(req, res, id);
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
async function getUserById(req, res, id) {
try {
const user = await findUserById(parseInt(id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json(user);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user' });
}
}
async function updateUser(req, res, id) {
try {
const updates = req.body;
const updatedUser = await updateUserInDB(parseInt(id), updates);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json(updatedUser);
} catch (error) {
res.status(500).json({ error: 'Failed to update user' });
}
}
// Mock database functions
async function findUserById(id) {
const users = [{ id: 1, name: 'John', email: 'john@example.com' }];
return users.find(user => user.id === id);
}
async function updateUserInDB(id, updates) {
const user = await findUserById(id);
return user ? { ...user, ...updates, updatedAt: new Date() } : null;
}
API Routes with App Router (Next.js 13+)
jsx
// app/api/users/route.js
import { NextResponse } from 'next/server';
export async function GET() {
const users = await fetchUsersFromDB();
return NextResponse.json(users);
}
export async function POST(request) {
try {
const body = await request.json();
const { name, email } = body;
if (!name || !email) {
return NextResponse.json(
{ error: 'Name and email are required' },
{ status: 400 }
);
}
const newUser = await createUserInDB({ name, email });
return NextResponse.json(newUser, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
}
// app/api/users/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const user = await findUserById(parseInt(id));
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(request, { params }) {
const { id } = params;
const updates = await request.json();
const updatedUser = await updateUserInDB(parseInt(id), updates);
if (!updatedUser) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(updatedUser);
}
2. Middleware for Authentication & Logging
Next.js Middleware allows you to run code before a request is completed. It's perfect for authentication, logging, and request modification.
Basic Middleware Setup
jsx
// middleware.js (root of your project)
import { NextResponse } from 'next/server';
export function middleware(request) {
// Clone the request headers
const requestHeaders = new Headers(request.headers);
// Add new request headers
requestHeaders.set('x-current-path', request.nextUrl.pathname);
// You can also set response headers
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('x-custom-header', 'my-custom-value');
return response;
}
// Specify which paths middleware should run on
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api/auth (authentication routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
],
};
Authentication Middleware
jsx
// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';
// List of paths that don't require authentication
const publicPaths = [
'/',
'/login',
'/register',
'/api/auth/login',
'/api/auth/register'
];
export async function middleware(request) {
const { pathname } = request.nextUrl;
// Check if the path is public
const isPublicPath = publicPaths.some(path =>
pathname === path || pathname.startsWith('/api/auth')
);
if (isPublicPath) {
return NextResponse.next();
}
// Get token from cookie or header
const token = request.cookies.get('auth-token')?.value ||
request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
// Redirect to login if no token
return NextResponse.redirect(new URL('/login', request.url));
}
try {
// Verify the token
const decoded = await verifyToken(token);
// Add user info to request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', decoded.userId);
requestHeaders.set('x-user-email', decoded.email);
requestHeaders.set('x-user-role', decoded.role);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
return response;
} catch (error) {
// Token is invalid
const response = NextResponse.redirect(new URL('/login', request.url));
// Clear invalid token
response.cookies.delete('auth-token');
return response;
}
}
export const config = {
matcher: [
'/dashboard/:path*',
'/profile/:path*',
'/api/users/:path*',
'/api/posts/:path*'
],
};
Role-Based Access Control Middleware
jsx
// middleware/role-auth.js
import { NextResponse } from 'next/server';
// Define routes and required roles
const routePermissions = {
'/api/admin': ['admin'],
'/api/users': ['admin', 'moderator'],
'/api/posts': ['admin', 'moderator', 'user'],
'/dashboard/admin': ['admin'],
'/dashboard': ['admin', 'moderator', 'user'],
};
export function roleAuthMiddleware(request) {
const { pathname } = request.nextUrl;
const userRole = request.headers.get('x-user-role');
// Find the most specific matching route
let requiredRoles = ['user']; // Default role
for (const [route, roles] of Object.entries(routePermissions)) {
if (pathname.startsWith(route)) {
requiredRoles = roles;
break;
}
}
// Check if user has required role
if (!requiredRoles.includes(userRole)) {
return NextResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
);
}
return NextResponse.next();
}
Logging Middleware
jsx
// middleware/logging.js
import { NextResponse } from 'next/server';
export async function loggingMiddleware(request) {
const start = Date.now();
const { method, url } = request;
// Process the request
const response = NextResponse.next();
const duration = Date.now() - start;
// Log request details
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
method,
url: url,
path: request.nextUrl.pathname,
status: response.status,
duration: `${duration}ms`,
userAgent: request.headers.get('user-agent'),
ip: request.ip || request.headers.get('x-forwarded-for'),
userId: request.headers.get('x-user-id') || 'anonymous'
}));
return response;
}
Comprehensive Middleware with Multiple Functions
jsx
// middleware.js
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';
// Chain multiple middleware functions
export async function middleware(request) {
// 1. Logging
const response = await loggingMiddleware(request);
// 2. Authentication (skip for public paths)
if (!isPublicPath(request.nextUrl.pathname)) {
const authResponse = await authMiddleware(request);
if (authResponse.status !== 200) {
return authResponse;
}
}
// 3. Role-based access control
const roleResponse = roleAuthMiddleware(request);
if (roleResponse.status !== 200) {
return roleResponse;
}
// 4. CORS headers for API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
return corsMiddleware(request, response);
}
return response;
}
// Individual middleware functions
async function loggingMiddleware(request) {
const start = Date.now();
const response = NextResponse.next();
const duration = Date.now() - start;
console.log(`${request.method} ${request.nextUrl.pathname} - ${duration}ms`);
return response;
}
async function authMiddleware(request) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
if (request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const decoded = await verifyToken(token);
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', decoded.userId);
requestHeaders.set('x-user-email', decoded.email);
requestHeaders.set('x-user-role', decoded.role);
return NextResponse.next({
request: { headers: requestHeaders },
});
} catch (error) {
const response = request.nextUrl.pathname.startsWith('/api/')
? NextResponse.json({ error: 'Invalid token' }, { status: 401 })
: NextResponse.redirect(new URL('/login', request.url));
response.cookies.delete('auth-token');
return response;
}
}
function corsMiddleware(request, response) {
// Add CORS headers
response.headers.set('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGINS || '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Allow-Credentials', 'true');
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { status: 200, headers: response.headers });
}
return response;
}
function isPublicPath(pathname) {
const publicPaths = ['/', '/login', '/register', '/api/auth', '/_next', '/favicon.ico'];
return publicPaths.some(path => pathname.startsWith(path));
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
*/
'/((?!_next/static|_next/image).*)',
],
};
API Route with Authentication
jsx
// pages/api/protected/user-profile.js
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';
export default async function handler(req, res) {
// Check authentication using NextAuth.js
const session = await getServerSession(req, res, authOptions);
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { method } = req;
switch (method) {
case 'GET':
try {
// Get user profile from database
const userProfile = await getUserProfile(session.user.id);
res.status(200).json(userProfile);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch profile' });
}
break;
case 'PUT':
try {
const updates = req.body;
const updatedProfile = await updateUserProfile(session.user.id, updates);
res.status(200).json(updatedProfile);
} catch (error) {
res.status(500).json({ error: 'Failed to update profile' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT']);
res.status(405).end(`Method ${method} Not Allowed`);
}
}
// Mock database functions
async function getUserProfile(userId) {
return { id: userId, name: 'John Doe', email: 'john@example.com' };
}
async function updateUserProfile(userId, updates) {
return { id: userId, ...updates, updatedAt: new Date() };
}
Rate Limiting Middleware
jsx
// middleware/rate-limit.js
import { NextResponse } from 'next/server';
// Simple in-memory store (use Redis in production)
const requestCounts = new Map();
export function rateLimitMiddleware(request) {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const windowMs = 15 * 60 * 1000; // 15 minutes
const maxRequests = 100; // 100 requests per window
// Clean old entries
cleanupOldEntries(now, windowMs);
const key = `${ip}:${request.nextUrl.pathname}`;
const requests = requestCounts.get(key) || [];
// Remove requests outside the current window
const recentRequests = requests.filter(time => now - time < windowMs);
if (recentRequests.length >= maxRequests) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// Add current request
recentRequests.push(now);
requestCounts.set(key, recentRequests);
// Add rate limit headers to response
const response = NextResponse.next();
response.headers.set('X-RateLimit-Limit', maxRequests.toString());
response.headers.set('X-RateLimit-Remaining', (maxRequests - recentRequests.length).toString());
return response;
}
function cleanupOldEntries(now, windowMs) {
for (const [key, requests] of requestCounts.entries()) {
const recentRequests = requests.filter(time => now - time < windowMs);
if (recentRequests.length === 0) {
requestCounts.delete(key);
} else {
requestCounts.set(key, recentRequests);
}
}
}
Best Practices
1. Error Handling
jsx
// lib/api-error.js
export class APIError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.name = 'APIError';
}
}
// pages/api/with-error-handling.js
import { APIError } from '../../lib/api-error';
export default async function handler(req, res) {
try {
if (req.method !== 'GET') {
throw new APIError('Method not allowed', 405);
}
const data = await fetchData();
if (!data) {
throw new APIError('Data not found', 404);
}
res.status(200).json(data);
} catch (error) {
if (error instanceof APIError) {
res.status(error.statusCode).json({ error: error.message });
} else {
console.error('Unexpected error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
}
2. Environment Variables
jsx
// pages/api/config.js
export default function handler(req, res) {
// Server-side environment variables
const config = {
apiUrl: process.env.API_URL,
databaseUrl: process.env.DATABASE_URL,
secretKey: process.env.SECRET_KEY,
// Never expose sensitive data to client
};
res.status(200).json({
apiUrl: config.apiUrl,
// Only send non-sensitive config to client
});
}
3. Response Helpers
jsx
// lib/api-helpers.js
export function successResponse(res, data, statusCode = 200) {
return res.status(statusCode).json({
success: true,
data,
timestamp: new Date().toISOString()
});
}
export function errorResponse(res, message, statusCode = 500) {
return res.status(statusCode).json({
success: false,
error: message,
timestamp: new Date().toISOString()
});
}
// Usage in API route
import { successResponse, errorResponse } from '../../lib/api-helpers';
export default function handler(req, res) {
try {
const data = { message: 'Success' };
return successResponse(res, data, 201);
} catch (error) {
return errorResponse(res, error.message, 400);
}
}
Next.js API Routes provide a powerful way to build full-stack applications with a unified technology stack, while middleware offers robust solutions for cross-cutting concerns like authentication, logging, and security.
Comments (0)
No comments yet. Be the first to comment!