Skip to main content
โšก Calmops

Building RESTful APIs with Node.js

Introduction

Node.js is ideal for building RESTful APIs. Its event-driven, non-blocking I/O model handles concurrent requests efficiently. This guide covers building production-ready APIs with Node.js and Express.

Setting Up the Project

Initialization

mkdir my-api
cd my-api
npm init -y
npm install express cors helmet morgan
npm install --save-dev nodemon

Project Structure

src/
โ”œโ”€โ”€ controllers/
โ”‚   โ””โ”€โ”€ userController.js
โ”œโ”€โ”€ models/
โ”‚   โ””โ”€โ”€ userModel.js
โ”œโ”€โ”€ routes/
โ”‚   โ””โ”€โ”€ userRoutes.js
โ”œโ”€โ”€ middleware/
โ”‚   โ””โ”€โ”€ auth.js
โ”œโ”€โ”€ services/
โ”‚   โ””โ”€โ”€ userService.js
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ database.js
โ”œโ”€โ”€ utils/
โ”‚   โ””โ”€โ”€ helpers.js
โ”œโ”€โ”€ app.js
โ””โ”€โ”€ server.js

Express Basics

Creating the Server

// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to my API' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Routing

Basic Routes

// routes/users.js
const express = require('express');
const router = express.Router();

// Get all users
router.get('/', async (req, res) => {
  const users = await User.find();
  res.json(users);
});

// Get user by ID
router.get('/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// Create user
router.post('/', async (req, res) => {
  const user = new User(req.body);
  await user.save();
  res.status(201).json(user);
});

// Update user
router.put('/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true }
  );
  res.json(user);
});

// Delete user
router.delete('/:id', async (req, res) => {
  await User.findByIdAndDelete(req.params.id);
  res.status(204).send();
});

module.exports = router;

Mounting Routes

// app.js
const userRoutes = require('./routes/users');

app.use('/api/users', userRoutes);

Middleware

Custom Middleware

// middleware/logger.js
function logger(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${duration}ms`);
  });
  
  next();
}

// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal server error',
    message: err.message
  });
}

Using Middleware

app.use(logger);
app.use(errorHandler);

Data 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(8).required(),
  age: Joi.number().integer().min(0).max(150)
});

// Validation middleware
function validate(schema) {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details
      });
    }
    next();
  };
}

router.post('/', validate(userSchema), createUser);

Authentication

JWT Implementation

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

function authenticate(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.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Generate token
function generateToken(user) {
  return jwt.sign(
    { id: user._id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
}

Protected Routes

router.get('/profile', authenticate, (req, res) => {
  res.json(req.user);
});

Error Handling

Async Error Handling

// Better error handling wrapper
const asyncHandler = fn => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Usage
router.get('/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
}));

RESTful Best Practices

URL Design

GET    /api/users          # List users
GET    /api/users/123     # Get user
POST   /api/users          # Create user
PUT    /api/users/123     # Update user
DELETE /api/users/123     # Delete user

GET    /api/users/123/posts       # Get user's posts
POST   /api/users/123/follow      # Follow user

Status Codes

Code Meaning
200 OK
201 Created
204 No Content
400 Bad Request
401 Unauthorized
404 Not Found
500 Server Error

Response Format

// Success response
{
  "success": true,
  "data": { ... },
  "meta": { "page": 1, "total": 100 }
}

// Error response
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input"
  }
}

Pagination

// middleware/pagination.js
function paginate(schema) {
  return async (req, res, next) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;

    const results = {};
    
    try {
      results.data = await schema.find()
        .skip(skip)
        .limit(limit);
      
      const total = await schema.countDocuments();
      
      results.meta = {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      };
      
      res.paginatedResults = results;
      next();
    } catch (err) {
      next(err);
    }
  };
}

// Usage
router.get('/', paginate(User), (req, res) => {
  res.json(res.paginatedResults);
});

Rate Limiting

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP
  message: 'Too many requests'
});

app.use('/api', limiter);

Testing APIs

Supertest

const request = require('supertest');
const app = require('../app');

describe('Users API', () => {
  it('should get all users', async () => {
    const res = await request(app)
      .get('/api/users')
      .expect(200);
    
    expect(Array.isArray(res.body)).toBe(true);
  });

  it('should create user', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: '[email protected]',
        password: 'password123'
      })
      .expect(201);
  });
});

Conclusion

Building RESTful APIs with Node.js involves more than just handling routes. Focus on proper error handling, validation, authentication, and following REST conventions. Use middleware effectively and always consider security.


Resources

Comments