RESTful APIs are the backbone of modern web applications, enabling seamless communication between frontend and backend systems. In this article, I'll share my experience building robust and scalable APIs using Node.js and Express.
Getting Started with Express
Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. Setting up a basic Express server is straightforward:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;
app.use(express.json());
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Structuring Your API
As your API grows, organization becomes crucial. I recommend using a modular approach with separate files for routes, controllers, models, and middleware:
project/
├── config/
├── controllers/
├── models/
├── routes/
├── middleware/
├── utils/
├── app.js
└── server.js
Authentication and Authorization
Security is paramount when building APIs. Implementing JWT (JSON Web Tokens) for authentication provides a stateless and secure way to authenticate users:
const jwt = require('jsonwebtoken');
// Generate token
const generateToken = (user) => {
return jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '30d' }
);
};
// Auth middleware
const protect = async (req, res, next) => {
let token;
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
try {
token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (error) {
res.status(401).json({ message: 'Not authorized' });
}
}
if (!token) {
res.status(401).json({ message: 'Not authorized, no token' });
}
};
Error Handling
Proper error handling ensures your API remains robust even when things go wrong. I create a centralized error handler middleware:
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Error handler middleware
const errorHandler = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else if (process.env.NODE_ENV === 'production') {
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
console.error('ERROR 💥', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
}
}
};
Request Validation
Validating incoming requests helps prevent bad data from entering your system. Using a library like Joi makes this process simple:
const Joi = require('joi');
const validateUser = (req, res, next) => {
const schema = Joi.object({
name: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ message: error.details[0].message });
}
next();
};
Rate Limiting
To protect your API from abuse, implementing rate limiting is essential:
const rateLimit = require('express-rate-limit');
const apiLimiter = 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', apiLimiter);
Documentation
A well-documented API is more accessible to other developers. I use Swagger to generate interactive documentation:
const swaggerJsDoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const swaggerOptions = {
swaggerDefinition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API Documentation'
},
servers: [
{
url: 'http://localhost:5000'
}
]
},
apis: ['./routes/*.js']
};
const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
Conclusion
Building a scalable RESTful API with Node.js and Express involves more than just handling HTTP requests. It requires careful attention to architecture, security, error handling, and performance. By following these best practices, you can create a robust API that serves as a solid foundation for your applications.
In future articles, I'll dive deeper into advanced topics like caching strategies, database optimization, and microservices architecture. Stay tuned!