Building Scalable REST APIs with Node.js and Express
Back to Home
Backend Development

Building Scalable REST APIs with Node.js and Express

Ankit ChaubeyAnkit Chaubey
February 8, 2024
5 min read

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/express

Project 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=production

Docker

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

Ankit Chaubey

About Ankit Chaubey

Full-stack developer passionate about modern web technologies