Introduction
Building scalable APIs is a crucial skill for any backend developer. In this comprehensive guide, we'll explore the best practices and patterns for creating production-ready RESTful APIs using Node.js and Express.
Setting Up the Project
First, let's set up our project structure. A well-organized codebase is the foundation of a maintainable application.
mkdir my-api && cd my-api
npm init -y
npm install express mongoose dotenv cors helmet
npm install -D typescript @types/node @types/expressProject Structure
Here's the recommended folder structure for a scalable API:
src/
├── config/
├── controllers/
├── middleware/
├── models/
├── routes/
├── services/
├── utils/
└── app.tsCreating the Express Server
Let's start by setting up our Express server with essential middleware:
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { errorHandler } from './middleware/errorHandler';
const app = express();
// Security middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/products', productRoutes);
// Error handling
app.use(errorHandler);
export default app;Error Handling
Proper error handling is essential for a production API. Here's a robust error handling pattern:
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);
}
}
export const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
success: false,
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};Authentication with JWT
Implementing secure authentication is crucial. Here's how to set up JWT authentication:
import jwt from 'jsonwebtoken';
export const generateToken = (userId: string) => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET!, {
expiresIn: '7d'
});
};
export const protect = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return next(new AppError('Not authorized', 401));
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
req.user = await User.findById(decoded.id);
next();
};Rate Limiting
Protect your API from abuse with rate limiting:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.'
});
app.use('/api', limiter);Validation
Input validation prevents bad data from entering your system:
import { body, validationResult } from 'express-validator';
export const validateUser = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().notEmpty(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];Conclusion
Building scalable APIs requires attention to detail in areas like error handling, authentication, validation, and rate limiting. By following these patterns, you'll create robust APIs that can handle production traffic.
Remember to always:
Happy coding!
Tags
Mahedi H Sharif
Full Stack Developer
Passionate about building scalable web applications and sharing knowledge with the developer community.