Web Development

Building Scalable REST APIs with Node.js and Express

Learn how to design and implement robust REST APIs using modern Node.js practices, including authentication, validation, and error handling.

Sasank - BTech CSE Student
January 10, 2025
12 min read
Building Scalable REST APIs with Node.js and Express
Node.js
Express
API
Backend

Building Scalable REST APIs with Node.js and Express

Creating robust and scalable REST APIs is crucial for modern web applications. In this comprehensive guide, we'll explore best practices for building APIs with Node.js and Express that can handle real-world traffic and requirements.

Setting Up the Foundation

Project Structure
A well-organized project structure is essential for maintainability:

src/
├── controllers/
├── middleware/
├── models/
├── routes/
├── services/
├── utils/
└── app.js


### Basic Express Setup

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors());

// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));


## RESTful Route Design

### Resource-Based URLs
Design your URLs around resources, not actions:

// Good
GET /api/users // Get all users
GET /api/users/:id // Get specific user
POST /api/users // Create new user
PUT /api/users/:id // Update user
DELETE /api/users/:id // Delete user

// Bad
GET /api/getUsers
POST /api/createUser


### Controller Pattern

// controllers/userController.js
const User = require('../models/User');

exports.getAllUsers = async (req, res, next) => {
try {
const users = await User.find()
.select('-password')
.limit(parseInt(req.query.limit) || 10)
.skip(parseInt(req.query.skip) || 0);

res.json({
success: true,
data: users,
count: users.length
});
} catch (error) {
next(error);
}
};

exports.createUser = async (req, res, next) => {
try {
const user = new User(req.body);
await user.save();

res.status(201).json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};


## Error Handling

### Global Error Handler

// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;

// Log error
console.error(err);

// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}

// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}

// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}

res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error'
});
};

module.exports = errorHandler;


## Authentication & Authorization

### JWT Implementation

// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

exports.protect = async (req, res, next) => {
try {
let token;

if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}

if (!token) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}

const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
} catch (error) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
};


## Validation

### Input Validation with Joi

const Joi = require('joi');

const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});

exports.validateUser = (req, res, next) => {
const { error } = userSchema.validate(req.body);

if (error) {
return res.status(400).json({
success: false,
error: error.details[0].message
});
}

next();
};


## Performance Optimization

### Caching with Redis

const redis = require('redis');
const client = redis.createClient();

exports.cache = (duration = 300) => {
return async (req, res, next) => {
const key = req.originalUrl;

try {
const cached = await client.get(key);

if (cached) {
return res.json(JSON.parse(cached));
}

res.sendResponse = res.json;
res.json = (body) => {
client.setex(key, duration, JSON.stringify(body));
res.sendResponse(body);
};

next();
} catch (error) {
next();
}
};
};


## Testing

### Unit Testing with Jest

// tests/user.test.js
const request = require('supertest');
const app = require('../src/app');

describe('User API', () => {
test('GET /api/users should return users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);

expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});

test('POST /api/users should create user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
};

const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);

expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe(userData.name);
});
});


## Conclusion

Building scalable REST APIs requires attention to structure, security, performance, and maintainability. By following these patterns and best practices, you'll create APIs that can grow with your application's needs.

Key takeaways:
- Use proper HTTP methods and status codes
- Implement comprehensive error handling
- Add authentication and validation layers
- Optimize with caching and rate limiting
- Write tests for reliability

Remember, scalability isn't just about handling more requests—it's about building maintainable code that your team can work with effectively.