Building production-grade REST APIs requires more than just setting up Express routes. Scalable APIs need thoughtful architecture, proper error handling, authentication middleware, database connection management, and caching strategies. This guide covers the patterns and practices we use at PCCVDI Solutions when building enterprise Node.js applications.
Project Structure
A well-organized project separates concerns into distinct layers:
src/
├── config/
│ ├── database.js
│ ├── redis.js
│ └── environment.js
├── middleware/
│ ├── auth.js
│ ├── errorHandler.js
│ ├── rateLimiter.js
│ └── validator.js
├── routes/
│ ├── index.js
│ ├── users.js
│ └── orders.js
├── controllers/
│ ├── userController.js
│ └── orderController.js
├── services/
│ ├── userService.js
│ └── orderService.js
├── models/
│ ├── User.js
│ └── Order.js
├── utils/
│ ├── logger.js
│ └── apiResponse.js
└── app.js
Application Bootstrap
// app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const { errorHandler } = require('./middleware/errorHandler');
const { rateLimiter } = require('./middleware/rateLimiter');
const routes = require('./routes');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Rate limiting
app.use('/api/', rateLimiter);
// Routes
app.use('/api/v1', routes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Global error handler (must be last)
app.use(errorHandler);
module.exports = app;
Error Handling Pattern
Centralized error handling prevents duplicate try-catch blocks and ensures consistent error responses:
// middleware/errorHandler.js
class AppError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal server error';
// Log non-operational errors (bugs)
if (!err.isOperational) {
console.error('UNEXPECTED ERROR:', err);
}
res.status(statusCode).json({
success: false,
error: { code: err.code || 'INTERNAL_ERROR', message }
});
};
module.exports = { AppError, errorHandler };
Async Wrapper
// utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage in controller
const getUser = asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) throw new AppError('User not found', 404, 'USER_NOT_FOUND');
res.json({ success: true, data: user });
});
Authentication Middleware
// middleware/auth.js
const jwt = require('jsonwebtoken');
const { AppError } = require('./errorHandler');
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw new AppError('Authentication required', 401, 'NO_TOKEN');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
throw new AppError('Invalid or expired token', 401, 'INVALID_TOKEN');
}
};
const authorize = (...roles) => (req, res, next) => {
if (!roles.includes(req.user.role)) {
throw new AppError('Insufficient permissions', 403, 'FORBIDDEN');
}
next();
};
Database Connection Pooling
// config/database.js - PostgreSQL with pg-pool
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Maximum pool size
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 5000,
});
// Graceful shutdown
process.on('SIGTERM', () => pool.end());
module.exports = { query: (text, params) => pool.query(text, params) };
Caching with Redis
// middleware/cache.js
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);
const cache = (ttl = 300) => async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Override res.json to cache the response
const originalJson = res.json.bind(res);
res.json = (body) => {
client.setex(key, ttl, JSON.stringify(body));
return originalJson(body);
};
next();
};
Rate Limiting
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const rateLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => client.call(...args) }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { success: false, error: { code: 'RATE_LIMIT', message: 'Too many requests' } }
});
Input Validation
// middleware/validator.js
const Joi = require('joi');
const { AppError } = require('./errorHandler');
const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
const messages = error.details.map(d => d.message).join(', ');
throw new AppError(messages, 400, 'VALIDATION_ERROR');
}
next();
};
// Usage
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
});
router.post('/users', validate(createUserSchema), userController.create);
Build Your Next API With Us
At PCCVDI Solutions, we build production-grade APIs and full-stack applications using Node.js, Python, .NET, and React. Our engineering team follows industry best practices for security, performance, and maintainability. Whether you need a new API built from scratch or need to scale an existing application, contact our development team to discuss your project requirements.
