Building Scalable APIs with Node.js and Express
Building scalable APIs is crucial for modern web applications. In this comprehensive guide, we'll explore best practices for creating robust REST APIs using Node.js and Express.
Setting Up the Project Structure
Start with a well-organized project structure:
src/
├── controllers/
├── models/
├── routes/
├── middleware/
├── services/
├── utils/
└── config/
Creating Express Routes
Organize your routes effectively:
// routes/users.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const auth = require('../middleware/auth');
const router = express.Router();
router.post('/users', [
body('email').isEmail(),
body('password').isLength({ min: 6 })
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
module.exports = router;
Error Handling Middleware
Implement global error handling:
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
console.error(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error'
});
};
module.exports = errorHandler;
Database Connection with MongoDB
Set up MongoDB connection with proper error handling:
// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('Database connection failed:', error);
process.exit(1);
}
};
module.exports = connectDB;
Authentication with JWT
Implement JWT-based authentication:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({ error: 'Invalid token.' });
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token.' });
}
};
module.exports = auth;
Rate Limiting
Protect your API with rate limiting:
// middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const createRateLimit = (windowMs, max, message) => {
return rateLimit({
windowMs,
max,
message: {
error: message
},
standardHeaders: true,
legacyHeaders: false,
});
};
const generalLimiter = createRateLimit(
15 * 60 * 1000, // 15 minutes
100, // limit each IP to 100 requests per windowMs
'Too many requests from this IP, please try again later.'
);
const authLimiter = createRateLimit(
15 * 60 * 1000, // 15 minutes
5, // limit each IP to 5 login requests per windowMs
'Too many login attempts from this IP, please try again later.'
);
module.exports = { generalLimiter, authLimiter };
API Documentation with Swagger
Document your API using Swagger:
// config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'A scalable REST API built with Node.js and Express',
},
servers: [
{
url: process.env.BASE_URL || 'http://localhost:3000',
description: 'Development server',
},
],
},
apis: ['./routes/*.js'], // paths to files containing OpenAPI definitions
};
const specs = swaggerJsdoc(options);
module.exports = { specs, swaggerUi };
Testing with Jest
Write comprehensive tests:
// tests/users.test.js
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
describe('User endpoints', () => {
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('_id');
expect(response.body.email).toBe(userData.email);
});
it('should return 400 for invalid email', async () => {
const userData = {
name: 'John Doe',
email: 'invalid-email',
password: 'password123'
};
await request(app)
.post('/api/users')
.send(userData)
.expect(400);
});
});
});
Performance Optimization
Implement caching and optimization strategies:
// middleware/cache.js
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
const cacheMiddleware = (duration = 600) => {
return (req, res, next) => {
const key = req.originalUrl;
const cachedResponse = cache.get(key);
if (cachedResponse) {
return res.json(cachedResponse);
}
res.originalJson = res.json;
res.json = (body) => {
res.originalJson(body);
cache.set(key, body, duration);
};
next();
};
};
module.exports = cacheMiddleware;
Conclusion
Building scalable APIs requires careful planning, proper error handling, authentication, rate limiting, and comprehensive testing. By following these best practices, you can create robust APIs that can handle growth and provide excellent developer experience.
Remember to always validate input, handle errors gracefully, implement proper security measures, and maintain good documentation for your API endpoints.