Building Scalable REST APIs with Node.js and Express
Introduction to REST APIs
REST (Representational State Transfer) is an architectural style for building web services. In this guide, we'll build a production-ready REST API using Node.js and Express.
Why Node.js for APIs?
Node.js is perfect for building APIs because of:
- Non-blocking I/O: Handle thousands of concurrent connections
- JavaScript everywhere: Use the same language on frontend and backend
- Rich ecosystem: NPM has packages for almost everything
- Fast performance: V8 engine makes Node.js incredibly fast
- Great for microservices: Lightweight and easy to scale
Setting Up Your Project
Installation
First, initialize your project:
mkdir my-api
cd my-api
npm init -y
npm install express mongoose dotenv cors helmet
npm install -D nodemon typescript @types/node @types/expressProject Structure
my-api/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── middleware/
│ ├── utils/
│ └── server.ts
├── .env
├── tsconfig.json
└── package.json
Building Your First API
1. Basic Express Server
Create a simple Express server:
// src/server.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Routes
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});2. Creating a Model
Define your data models using Mongoose:
// src/models/User.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IUser extends Document {
name: string;
email: string;
password: string;
createdAt: Date;
}
const UserSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
});
export default mongoose.model<IUser>('User', UserSchema);3. Building Controllers
Create controllers to handle business logic:
// src/controllers/userController.ts
import { Request, Response } from 'express';
import User from '../models/User';
export const createUser = async (req: Request, res: Response) => {
try {
const { name, email, password } = req.body;
const user = new User({ name, email, password });
await user.save();
res.status(201).json({
success: true,
data: user,
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message,
});
}
};
export const getUsers = async (req: Request, res: Response) => {
try {
const users = await User.find().select('-password');
res.status(200).json({
success: true,
count: users.length,
data: users,
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message,
});
}
};4. Setting Up Routes
Define your API routes:
// src/routes/users.ts
import express from 'express';
import { createUser, getUsers } from '../controllers/userController';
const router = express.Router();
router.post('/', createUser);
router.get('/', getUsers);
export default router;Best Practices
1. Error Handling
Implement centralized error handling:
// src/middleware/errorHandler.ts
export const errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
success: false,
error: err.message || 'Server Error',
});
};2. Input Validation
Always validate user input:
import { body, validationResult } from 'express-validator';
export const validateUser = [
body('email').isEmail().withMessage('Enter a valid email'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];3. 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 windowMs
});
app.use('/api/', limiter);4. Authentication & Authorization
Implement JWT authentication:
import jwt from 'jsonwebtoken';
export const protect = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ message: 'Not authorized' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: 'Invalid token' });
}
};Testing Your API
Use tools like:
- Postman: Interactive API testing
- Jest: Unit and integration tests
- Supertest: HTTP assertion library
import request from 'supertest';
import app from '../server';
describe('GET /api/users', () => {
it('should return all users', async () => {
const res = await request(app).get('/api/users');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('data');
});
});Deployment
Environment Variables
PORT=3000
MONGODB_URI=mongodb://localhost:27017/mydb
JWT_SECRET=your-secret-key
NODE_ENV=productionDocker
Create a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]Conclusion
Building REST APIs with Node.js and Express is straightforward when following best practices. Focus on proper error handling, validation, authentication, and testing to create production-ready APIs.
Next Steps:
- Add database indexing for better performance
- Implement caching with Redis
- Set up monitoring with tools like PM2 or New Relic
- Add API documentation with Swagger
Happy coding! 🚀
Share this post

About Ankit Chaubey
Full-stack developer passionate about modern web technologies