Building RESTful APIs with Node.js
A comprehensive guide to designing and building scalable RESTful APIs using Node.js and Express.
Building RESTful APIs with Node.js
RESTful APIs are the backbone of modern web applications. In this guide, we'll explore best practices for building robust, scalable APIs with Node.js.
REST Fundamentals
REST (Representational State Transfer) is an architectural style that uses HTTP methods to perform CRUD operations:
- GET: Retrieve resources
- POST: Create new resources
- PUT: Update existing resources (full replacement)
- PATCH: Partially update resources
- DELETE: Remove resources
Project Structure
A well-organized project structure is crucial for maintainability:
src/
├── controllers/ # Request handlers
├── middleware/ # Custom middleware
├── models/ # Data models
├── routes/ # Route definitions
├── services/ # Business logic
├── utils/ # Helper functions
├── validators/ # Input validation
└── app.js # Application entry
Route Design
Resource Naming
Use plural nouns for resources:
✅ GET /api/users
✅ GET /api/users/123
✅ GET /api/users/123/posts
❌ GET /api/getUsers
❌ GET /api/user/123
Implementing Routes
// routes/users.js
import { Router } from 'express';
import * as userController from '../controllers/users.js';
import { validateUser } from '../validators/user.js';
import { authenticate } from '../middleware/auth.js';
const router = Router();
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', authenticate, validateUser, userController.create);
router.put('/:id', authenticate, validateUser, userController.update);
router.delete('/:id', authenticate, userController.remove);
export default router;
Error Handling
Custom Error Class
class APIError extends Error {
constructor(message, statusCode, code) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
// Usage
throw new APIError('User not found', 404, 'USER_NOT_FOUND');
Global Error Handler
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Internal server error';
// Log the error
console.error(err);
res.status(statusCode).json({
error: {
message,
code: err.code || 'INTERNAL_ERROR',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
});
}
app.use(errorHandler);
Input Validation
Always validate input data before processing:
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
function validateUser(req, res, next) {
try {
req.body = userSchema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: {
message: 'Validation failed',
details: error.errors,
},
});
}
}
Authentication & Authorization
JWT Authentication
import jwt from 'jsonwebtoken';
function authenticate(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
Response Format
Consistent Response Structure
// Success response
{
"data": { ... },
"meta": {
"total": 100,
"page": 1,
"limit": 10
}
}
// Error response
{
"error": {
"message": "User not found",
"code": "USER_NOT_FOUND"
}
}
Pagination Helper
function paginate(query, { page = 1, limit = 10 }) {
const offset = (page - 1) * limit;
return {
...query,
skip: offset,
take: limit,
};
}
// Controller usage
async function getUsers(req, res) {
const { page, limit } = req.query;
const users = await prisma.user.findMany(
paginate({}, { page: Number(page), limit: Number(limit) })
);
const total = await prisma.user.count();
res.json({
data: users,
meta: { total, page: Number(page), limit: Number(limit) },
});
}
Rate Limiting
Protect your API from abuse:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: {
error: {
message: 'Too many requests',
code: 'RATE_LIMIT_EXCEEDED',
},
},
});
app.use('/api/', limiter);
Best Practices Summary
- Use proper HTTP status codes - 200, 201, 400, 401, 403, 404, 500
- Version your API -
/api/v1/users - Implement proper error handling - Never expose internal errors
- Validate all inputs - Trust no one
- Use HTTPS - Always encrypt in transit
- Document your API - Use OpenAPI/Swagger
- Add rate limiting - Prevent abuse
- Log everything - Debug faster
Conclusion
Building RESTful APIs is about following conventions and best practices. A well-designed API is easy to understand, maintain, and consume. Focus on consistency, security, and documentation to create APIs that developers love to use.