Blog Tutorials Node.js API Development Best Practices Node.js API Development Best Practices #Node.js #API Development #Backend #Best Practices #Express.js
1. Introduction
Node.js has become the backbone of modern web development, powering APIs that serve millions of users worldwide. Building robust, scalable APIs requires following established patterns and best practices that ensure maintainability, security, and performance. This guide covers everything from project setup to production deployment.
2. Project Architecture and Setup
2.1. Project Structure
src/
├── controllers/
├── middlewares/
├── models/
├── routes/
├── services/
├── utils/
├── config/
└── tests/
2.2. Express.js Setup
// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors());
// Performance middleware
app.use(compression());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
module.exports = app;
3. Database Integration
3.1. MongoDB with Mongoose
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
password: {
type: String,
required: true,
minlength: 6,
},
name: {
type: String,
required: true,
trim: true,
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user',
},
}, {
timestamps: true,
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
userSchema.methods.comparePassword = async function(password) {
return bcrypt.compare(password, this.password);
};
module.exports = mongoose.model('User', userSchema);
4. Authentication and Authorization
4.1. JWT Implementation
// services/authService.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
class AuthService {
generateToken(userId) {
return jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
}
async verifyToken(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) throw new Error('User not found');
return user;
} catch (error) {
throw new Error('Invalid token');
}
}
async login(email, password) {
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
throw new Error('Invalid credentials');
}
const token = this.generateToken(user._id);
return { user, token };
}
async register(userData) {
const existingUser = await User.findOne({ email: userData.email });
if (existingUser) {
throw new Error('User already exists');
}
const user = new User(userData);
await user.save();
const token = this.generateToken(user._id);
return { user, token };
}
}
module.exports = new AuthService();
5. Error Handling
5.1. Global Error Handler
// middlewares/errorHandler.js
const AppError = require('../utils/AppError');
const handleCastErrorDB = (err) => {
const message = `Invalid ${err.path}: ${err.value}.`;
return new AppError(message, 400);
};
const handleDuplicateFieldsDB = (err) => {
const value = err.errmsg.match(/(["'])(\\?.)*?\1/)[0];
const message = `Duplicate field value: ${value}. Please use another value!`;
return new AppError(message, 400);
};
const handleValidationErrorDB = (err) => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data. ${errors.join('. ')}`;
return new AppError(message, 400);
};
const sendErrorDev = (err, res) => {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
};
const sendErrorProd = (err, res) => {
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!'
});
}
};
module.exports = (err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
let error = { ...err };
error.message = err.message;
if (error.name === 'CastError') error = handleCastErrorDB(error);
if (error.code === 11000) error = handleDuplicateFieldsDB(error);
if (error.name === 'ValidationError') error = handleValidationErrorDB(error);
sendErrorProd(error, res);
}
};
6. API Design Patterns
6.1. RESTful Controllers
// controllers/userController.js
const User = require('../models/User');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/AppError');
exports.getAllUsers = catchAsync(async (req, res, next) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const users = await User.find()
.select('-password')
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
res.status(200).json({
status: 'success',
results: users.length,
data: {
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
}
});
});
exports.getUser = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id).select('-password');
if (!user) {
return next(new AppError('No user found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: { user }
});
});
exports.updateUser = catchAsync(async (req, res, next) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
).select('-password');
if (!user) {
return next(new AppError('No user found with that ID', 404));
}
res.status(200).json({
status: 'success',
data: { user }
});
});
exports.deleteUser = catchAsync(async (req, res, next) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return next(new AppError('No user found with that ID', 404));
}
res.status(204).json({
status: 'success',
data: null
});
});
7. Testing Strategies
7.1. Unit Testing with Jest
// tests/services/authService.test.js
const AuthService = require('../../services/authService');
const User = require('../../models/User');
jest.mock('../../models/User');
describe('AuthService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('login', () => {
it('should login user with valid credentials', async () => {
const mockUser = {
_id: 'user123',
email: '[email protected] ',
comparePassword: jest.fn().mockResolvedValue(true)
};
User.findOne.mockResolvedValue(mockUser);
const result = await AuthService.login('[email protected] ', 'password');
expect(result.user).toBe(mockUser);
expect(result.token).toBeDefined();
expect(User.findOne).toHaveBeenCalledWith({ email: '[email protected] ' });
});
it('should throw error for invalid credentials', async () => {
User.findOne.mockResolvedValue(null);
await expect(
AuthService.login('[email protected] ', 'wrongpassword')
).rejects.toThrow('Invalid credentials');
});
});
});
8. Performance Optimization
8.1. Caching with Redis
// services/cacheService.js
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);
class CacheService {
async get(key) {
try {
const data = await client.get(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
async set(key, data, expiration = 3600) {
try {
await client.setex(key, expiration, JSON.stringify(data));
} catch (error) {
console.error('Cache set error:', error);
}
}
async del(key) {
try {
await client.del(key);
} catch (error) {
console.error('Cache delete error:', error);
}
}
async flush() {
try {
await client.flushall();
} catch (error) {
console.error('Cache flush error:', error);
}
}
}
module.exports = new CacheService();
9. Security Best Practices
9.1. Input Validation
// validation/userValidation.js
const Joi = require('joi');
const registerSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
name: Joi.string().min(2).max(50).required(),
});
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
});
module.exports = {
registerSchema,
loginSchema,
};
10. API Documentation
10.1. Swagger Integration
// swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'API Documentation',
version: '1.0.0',
description: 'A comprehensive API documentation'
},
servers: [
{
url: process.env.API_URL || 'http://localhost:3000',
description: 'Development server'
}
],
},
apis: ['./routes/*.js'],
};
const specs = swaggerJSDoc(options);
module.exports = specs;
11. Deployment and DevOps
11.1. Docker Configuration
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
USER node
CMD ["npm", "start"]
11.2. Environment Configuration
// config/environment.js
module.exports = {
development: {
port: process.env.PORT || 3000,
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/api-dev'
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379'
}
},
production: {
port: process.env.PORT,
mongodb: {
uri: process.env.MONGODB_URI
},
redis: {
url: process.env.REDIS_URL
}
}
};
12. Conclusion
This guide provides a comprehensive foundation for building production-ready Node.js APIs. By following these best practices, you'll create maintainable, secure, and scalable applications that can handle real-world demands.
Key Takeaways
Architecture : Well-structured project organization
Security : Comprehensive authentication and authorization
Performance : Caching and optimization strategies
Testing : Thorough testing coverage
Documentation : Clear API documentation
Deployment : Production-ready configuration
These patterns and practices will help you build robust APIs that scale with your application's growth.