Building Scalable APIs with Node.js and Express
Building scalable APIs is crucial for modern web applications. This guide covers essential patterns and practices for creating robust backend services.
Project Structure
A well-organized project structure is the foundation of maintainable code:
``
src/
├── controllers/
├── middleware/
├── models/
├── routes/
├── services/
├── utils/
└── app.js
`
## Essential Middleware
Error Handling
`javascript
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500
res.status(statusCode).json({
success: false,
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
})
}
`
### Rate Limiting
`javascript
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
})
app.use('/api/', limiter)
`
## Database Integration
### Using Mongoose with MongoDB
`javascript
const mongoose = require('mongoose')
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
})
module.exports = mongoose.model('User', userSchema)
`
## Authentication & Authorization
Implement JWT-based authentication:
`javascript
const jwt = require('jsonwebtoken')
const generateToken = (userId) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '7d' })
}
const authenticateToken = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Access denied' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.userId = decoded.userId
next()
} catch (error) {
res.status(401).json({ error: 'Invalid token' })
}
}
`
## Testing Your API
Use Jest and Supertest for comprehensive testing:
`javascript
const request = require('supertest')
const app = require('../app')
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
}
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201)
expect(response.body.name).toBe(userData.name)
})
})
``
## Deployment Considerations
- Use environment variables for configuration
- Implement proper logging with Winston
- Set up monitoring with tools like New Relic or DataDog
- Use PM2 for process management in production
## Conclusion
Building scalable APIs requires careful planning and adherence to best practices. Focus on clean architecture, proper error handling, and comprehensive testing to create robust backend services.
A well-organized project structure is the foundation of maintainable code:
``
src/
├── controllers/
├── middleware/
├── models/
├── routes/
├── services/
├── utils/
└── app.js
`
## Essential Middleware
Error Handling
`javascript
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500
res.status(statusCode).json({
success: false,
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
})
}
`
### Rate Limiting
`javascript
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
})
app.use('/api/', limiter)
`
## Database Integration
### Using Mongoose with MongoDB
`javascript
const mongoose = require('mongoose')
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
})
module.exports = mongoose.model('User', userSchema)
`
## Authentication & Authorization
Implement JWT-based authentication:
`javascript
const jwt = require('jsonwebtoken')
const generateToken = (userId) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '7d' })
}
const authenticateToken = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Access denied' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.userId = decoded.userId
next()
} catch (error) {
res.status(401).json({ error: 'Invalid token' })
}
}
`
## Testing Your API
Use Jest and Supertest for comprehensive testing:
`javascript
const request = require('supertest')
const app = require('../app')
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
}
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201)
expect(response.body.name).toBe(userData.name)
})
})
``## Deployment Considerations
- Use environment variables for configuration
- Implement proper logging with Winston
- Set up monitoring with tools like New Relic or DataDog
- Use PM2 for process management in production
## Conclusion
Building scalable APIs requires careful planning and adherence to best practices. Focus on clean architecture, proper error handling, and comprehensive testing to create robust backend services.
