Backend

Building Scalable APIs with Node.js

Dinesh SutiharJanuary 28, 202614 min read
Node.jsExpressAPI DesignBackend
Building Scalable APIs with Node.js

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 setup

Express 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 middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
 
// Rate limiting
const 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 parsing
app.use(express.json({ limit: '10kb' }));
 
// Routes
app.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 class
class 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 middleware
function 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 authorization
function 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.ts
import { 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 routes
router.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.

Share this article