Node.js Backend Development Complete Guide - Building Scalable APIs
· 12 min read
Node.js has revolutionized backend development by enabling JavaScript on the server side. This comprehensive guide covers everything you need to know about building scalable, production-ready Node.js applications.
Why Choose Node.js for Backend Development?
Node.js offers several compelling advantages for backend development:
- JavaScript Everywhere: Use the same language for frontend and backend
- High Performance: Non-blocking I/O and event-driven architecture
- Rich Ecosystem: NPM provides access to thousands of packages
- Scalability: Built for handling concurrent connections
- Rapid Development: Quick prototyping and development cycles
- Community Support: Large, active community with excellent resources
Setting Up a Node.js Project
Project Initialization
# Create new project directory
mkdir my-nodejs-api
cd my-nodejs-api
# Initialize package.json
npm init -y
# Install essential dependencies
npm install express cors helmet morgan dotenv
npm install -D nodemon @types/node typescript ts-node
# Install development dependencies
npm install -D jest supertest eslint prettier
Project Structure
my-nodejs-api/
├── src/
│ ├── controllers/
│ ├── middleware/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── utils/
│ ├── config/
│ └── app.js
├── tests/
├── docs/
├── .env
├── .gitignore
├── package.json
└── README.md
Basic Express Server
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Enable CORS
app.use(morgan('combined')); // Logging
app.use(express.json({ limit: '10mb' })); // Parse JSON
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded data
// Routes
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Something went wrong!',
message: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error'
});
});
// 404 handler
app.use('*', (req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Advanced Express.js Patterns
Modular Route Organization
// src/routes/users.js
const express = require('express');
const router = express.Router();
const UserController = require('../controllers/UserController');
const authMiddleware = require('../middleware/auth');
const validationMiddleware = require('../middleware/validation');
// Validation schemas
const userValidation = {
create: {
body: {
name: { type: 'string', required: true, minLength: 2 },
email: { type: 'string', required: true, format: 'email' },
password: { type: 'string', required: true, minLength: 6 }
}
},
update: {
body: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' }
}
}
};
// Routes
router.get('/', authMiddleware, UserController.getAllUsers);
router.get('/:id', authMiddleware, UserController.getUserById);
router.post('/', validationMiddleware(userValidation.create), UserController.createUser);
router.put('/:id', authMiddleware, validationMiddleware(userValidation.update), UserController.updateUser);
router.delete('/:id', authMiddleware, UserController.deleteUser);
module.exports = router;
Advanced Middleware Patterns
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const authMiddleware = 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.userId).select('-password');
if (!user) {
return res.status(401).json({ error: 'Invalid token.' });
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token.' });
}
};
// Role-based authorization
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Access denied. User not authenticated.' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied. Insufficient permissions.' });
}
next();
};
};
module.exports = { authMiddleware, authorize };
Request Validation Middleware
// src/middleware/validation.js
const Joi = require('joi');
const validationMiddleware = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
const errorMessage = error.details.map(detail => detail.message).join(', ');
return res.status(400).json({
error: 'Validation failed',
details: errorMessage
});
}
next();
};
};
// Custom validation schemas
const schemas = {
user: {
create: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
role: Joi.string().valid('user', 'admin').default('user')
}),
update: Joi.object({
name: Joi.string().min(2).max(50),
email: Joi.string().email(),
role: Joi.string().valid('user', 'admin')
})
}
};
module.exports = { validationMiddleware, schemas };
Database Integration
MongoDB with Mongoose
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please enter a valid email']
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
select: false
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
avatar: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Generate JWT token
userSchema.methods.generateAuthToken = function() {
return jwt.sign(
{ userId: this._id, email: this.email, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE || '7d' }
);
};
// Remove password from JSON output
userSchema.methods.toJSON = function() {
const userObject = this.toObject();
delete userObject.password;
return userObject;
};
module.exports = mongoose.model('User', userSchema);
Database Connection and Configuration
// src/config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('Database connection error:', error);
process.exit(1);
}
};
// Handle connection events
mongoose.connection.on('connected', () => {
console.log('Mongoose connected to MongoDB');
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('Mongoose disconnected');
});
// Graceful shutdown
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('Mongoose connection closed through app termination');
process.exit(0);
});
module.exports = connectDB;
Advanced Controller Patterns
User Controller with Full CRUD Operations
// src/controllers/UserController.js
const User = require('../models/User');
const { validationResult } = require('express-validator');
class UserController {
// Get all users with pagination and filtering
static async getAllUsers(req, res) {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Build filter object
const filter = {};
if (req.query.role) filter.role = req.query.role;
if (req.query.isActive !== undefined) filter.isActive = req.query.isActive === 'true';
// Build sort object
const sort = {};
if (req.query.sortBy) {
const sortOrder = req.query.sortOrder === 'desc' ? -1 : 1;
sort[req.query.sortBy] = sortOrder;
} else {
sort.createdAt = -1; // Default sort by creation date
}
const users = await User.find(filter)
.select('-password')
.sort(sort)
.skip(skip)
.limit(limit);
const total = await User.countDocuments(filter);
const totalPages = Math.ceil(total / limit);
res.json({
users,
pagination: {
currentPage: page,
totalPages,
totalUsers: total,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
}
// Get user by ID
static async getUserById(req, res) {
try {
const user = await User.findById(req.params.id).select('-password');
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user' });
}
}
// Create new user
static async createUser(req, res) {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
});
}
const { name, email, password, role } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'User already exists with this email' });
}
const user = new User({ name, email, password, role });
await user.save();
const token = user.generateAuthToken();
res.status(201).json({
message: 'User created successfully',
user,
token
});
} catch (error) {
res.status(500).json({ error: 'Failed to create user' });
}
}
// Update user
static async updateUser(req, res) {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
});
}
const { name, email, role } = req.body;
const userId = req.params.id;
// Check if user exists
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Check if email is being changed and if it's already taken
if (email && email !== user.email) {
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already in use' });
}
}
// Update user
const updatedUser = await User.findByIdAndUpdate(
userId,
{ name, email, role },
{ new: true, runValidators: true }
).select('-password');
res.json({
message: 'User updated successfully',
user: updatedUser
});
} catch (error) {
res.status(500).json({ error: 'Failed to update user' });
}
}
// Delete user
static async deleteUser(req, res) {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete user' });
}
}
}
module.exports = UserController;
Authentication and Security
JWT Authentication Service
// src/services/AuthService.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
class AuthService {
// Register new user
static async register(userData) {
const { name, email, password, role } = userData;
// Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new Error('User already exists with this email');
}
// Create new user
const user = new User({ name, email, password, role });
await user.save();
// Generate token
const token = user.generateAuthToken();
return {
user: user.toJSON(),
token
};
}
// Login user
static async login(email, password) {
// Find user and include password for comparison
const user = await User.findOne({ email }).select('+password');
if (!user) {
throw new Error('Invalid credentials');
}
// Check if user is active
if (!user.isActive) {
throw new Error('Account is deactivated');
}
// Compare password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
throw new Error('Invalid credentials');
}
// Generate token
const token = user.generateAuthToken();
return {
user: user.toJSON(),
token
};
}
// Refresh token
static async refreshToken(userId) {
const user = await User.findById(userId);
if (!user) {
throw new Error('User not found');
}
const token = user.generateAuthToken();
return { token };
}
// Verify token
static verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('Invalid token');
}
}
}
module.exports = AuthService;
Rate Limiting and Security
// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
// General rate limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
// Strict rate limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Limit each IP to 5 requests per windowMs
message: {
error: 'Too many authentication attempts, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
// Speed limiter
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 2, // Allow 2 requests per 15 minutes, then...
delayMs: 500, // Add 500ms delay per request above delayAfter
});
module.exports = {
generalLimiter,
authLimiter,
speedLimiter
};
Testing
Unit Tests with Jest
// tests/controllers/UserController.test.js
const request = require('supertest');
const app = require('../../src/app');
const User = require('../../src/models/User');
const connectDB = require('../../src/config/database');
describe('User Controller', () => {
beforeAll(async () => {
await connectDB();
});
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
password: 'password123',
role: 'user'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.message).toBe('User created successfully');
expect(response.body.user.name).toBe(userData.name);
expect(response.body.user.email).toBe(userData.email);
expect(response.body.token).toBeDefined();
});
it('should not create user with duplicate email', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
password: 'password123'
};
// Create first user
await request(app)
.post('/api/users')
.send(userData)
.expect(201);
// Try to create second user with same email
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(response.body.error).toBe('User already exists with this email');
});
});
describe('GET /api/users', () => {
it('should get all users with pagination', async () => {
// Create test users
const users = [
{ name: 'User 1', email: '[email protected]', password: 'password123' },
{ name: 'User 2', email: '[email protected]', password: 'password123' },
{ name: 'User 3', email: '[email protected]', password: 'password123' }
];
for (const user of users) {
await new User(user).save();
}
const response = await request(app)
.get('/api/users?page=1&limit=2')
.expect(200);
expect(response.body.users).toHaveLength(2);
expect(response.body.pagination.totalUsers).toBe(3);
expect(response.body.pagination.totalPages).toBe(2);
});
});
});
Deployment and Production
Environment Configuration
// src/config/index.js
const config = {
development: {
port: process.env.PORT || 3000,
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/myapp_dev'
},
jwt: {
secret: process.env.JWT_SECRET || 'dev-secret-key',
expire: process.env.JWT_EXPIRE || '7d'
},
cors: {
origin: ['http://localhost:3000', 'http://localhost:3001']
}
},
production: {
port: process.env.PORT || 3000,
mongodb: {
uri: process.env.MONGODB_URI
},
jwt: {
secret: process.env.JWT_SECRET,
expire: process.env.JWT_EXPIRE || '7d'
},
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || []
}
}
};
const env = process.env.NODE_ENV || 'development';
module.exports = config[env];
Docker Configuration
# Dockerfile
FROM node:16-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# Start application
CMD ["node", "src/app.js"]
Docker Compose for Development
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/myapp
- JWT_SECRET=your-jwt-secret
depends_on:
- mongo
restart: unless-stopped
mongo:
image: mongo:4.4
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
volumes:
mongo_data:
Performance Optimization
Caching with Redis
// src/services/CacheService.js
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
});
class CacheService {
static 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;
}
}
static async set(key, value, expireInSeconds = 3600) {
try {
await client.setex(key, expireInSeconds, JSON.stringify(value));
} catch (error) {
console.error('Cache set error:', error);
}
}
static async del(key) {
try {
await client.del(key);
} catch (error) {
console.error('Cache delete error:', error);
}
}
static async flush() {
try {
await client.flushall();
} catch (error) {
console.error('Cache flush error:', error);
}
}
}
module.exports = CacheService;
Caching Middleware
// src/middleware/cache.js
const CacheService = require('../services/CacheService');
const cache = (duration = 3600) => {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cachedData = await CacheService.get(key);
if (cachedData) {
return res.json(cachedData);
}
// Store original res.json
const originalJson = res.json;
// Override res.json to cache the response
res.json = function(data) {
CacheService.set(key, data, duration);
originalJson.call(this, data);
};
next();
} catch (error) {
console.error('Cache middleware error:', error);
next();
}
};
};
module.exports = cache;
Conclusion
Node.js provides a powerful platform for building scalable backend applications. By following the patterns and practices outlined in this guide, you can create robust, maintainable, and production-ready Node.js applications.
Key takeaways:
- Use Express.js for rapid API development
- Implement proper error handling and validation
- Secure your applications with authentication and rate limiting
- Write comprehensive tests for reliability
- Optimize performance with caching and proper database queries
- Deploy with Docker for consistency across environments
Next Steps
Ready to dive deeper into Node.js development? Check out our comprehensive tutorials:
What Node.js patterns do you find most useful? Share your experiences and tips in the comments below!