Building Scalable APIs with Node.js
Dinesh SutiharJanuary 28, 202614 min read
Node.jsExpressAPI DesignBackend
API Architecture Overview
Building a scalable API requires thoughtful architecture from the start. We'll use a layered architecture that separates concerns: Routes → Controllers → Services → Data Access.
Project Structure
plaintext
src/├── controllers/ # Handle HTTP requests/responses├── services/ # Business logic├── models/ # Data models and schemas├── middleware/ # Express middleware├── routes/ # Route definitions├── utils/ # Helper functions├── config/ # Configuration└── app.ts # Express app setupExpress App Configuration
typescript
import express from 'express';import cors from 'cors';import helmet from 'helmet';import rateLimit from 'express-rate-limit';import { errorHandler } from './middleware/errorHandler';import routes from './routes'; const app = express(); // Security middlewareapp.use(helmet());app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') })); // Rate limitingconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per window message: 'Too many requests, please try again later'});app.use('/api', limiter); // Body parsingapp.use(express.json({ limit: '10kb' })); // Routesapp.use('/api/v1', routes); // Global error handler (must be last)app.use(errorHandler); export default app;Error Handling
Centralized error handling is crucial for maintainability. Create a custom error class and a global error handler middleware.
typescript
// Custom API Error classclass AppError extends Error { statusCode: number; isOperational: boolean; constructor(message: string, statusCode: number) { super(message); this.statusCode = statusCode; this.isOperational = true; Error.captureStackTrace(this, this.constructor); }} // Error handler middlewarefunction errorHandler(err, req, res, next) { const statusCode = err.statusCode || 500; const message = err.isOperational ? err.message : 'Internal server error'; // Log error for debugging console.error(`[${new Date().toISOString()}] ${err.stack}`); res.status(statusCode).json({ success: false, error: message, ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) });}Authentication Middleware
typescript
import jwt from 'jsonwebtoken'; async function authenticate(req, res, next) { try { const token = req.headers.authorization?.split(' ')[1]; if (!token) { throw new AppError('Authentication required', 401); } const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (error) { next(new AppError('Invalid or expired token', 401)); }} // Role-based authorizationfunction authorize(...roles: string[]) { return (req, res, next) => { if (!roles.includes(req.user.role)) { throw new AppError('Insufficient permissions', 403); } next(); };}Controller Pattern
typescript
// controllers/userController.tsimport { userService } from '../services/userService'; export const userController = { async getAll(req, res, next) { try { const { page = 1, limit = 10 } = req.query; const users = await userService.findAll({ page, limit }); res.json({ success: true, data: users, pagination: { page, limit } }); } catch (error) { next(error); } }, async create(req, res, next) { try { const user = await userService.create(req.body); res.status(201).json({ success: true, data: user }); } catch (error) { next(error); } }};Validation with Zod
typescript
import { z } from 'zod'; const createUserSchema = z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(2).max(50)}); function validate(schema) { return (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { throw new AppError(result.error.errors[0].message, 400); } req.body = result.data; next(); };} // Usage in routesrouter.post('/users', validate(createUserSchema), userController.create);Best Practices
- Use versioning in your API paths (/api/v1/)
- Implement proper HTTP status codes
- Add request/response logging with correlation IDs
- Use environment variables for configuration
- Implement graceful shutdown for production
- Add health check endpoints
- Document your API with OpenAPI/Swagger
Conclusion
Building scalable APIs is about establishing solid patterns from the start. Focus on separation of concerns, consistent error handling, and security. As your application grows, this foundation will make it easier to maintain and extend.