Back to Blog
Node.jsAPIBackendExpressTypeScript

Modern API Design: Building RESTful APIs with Node.js

Learn best practices for designing and building production-ready RESTful APIs using Node.js, Express, and TypeScript.

D
Dheeraj Jha
June 5, 2024
4 min read
Modern API Design: Building RESTful APIs with Node.js

Modern API Design: Building RESTful APIs with Node.js

Building a well-designed API is crucial for any modern application. Let's explore best practices for creating robust, scalable RESTful APIs.

Setting Up the Foundation

Start with a solid TypeScript and Express setup:

// src/app.ts
import express, { Application, Request, Response } from 'express'
import cors from 'cors'
import helmet from 'helmet'
import morgan from 'morgan'

const app: Application = express()

// Middleware
app.use(helmet()) // Security headers
app.use(cors()) // CORS support
app.use(morgan('combined')) // Logging
app.use(express.json()) // Parse JSON bodies

export default app

RESTful Route Design

Follow REST conventions for intuitive APIs:

// src/routes/users.ts
import { Router } from 'express'
import { UserController } from '../controllers/UserController'

const router = Router()
const controller = new UserController()

// Collection routes
router.get('/users', controller.getAll) // GET all users
router.post('/users', controller.create) // CREATE new user

// Resource routes
router.get('/users/:id', controller.getById) // GET specific user
router.put('/users/:id', controller.update) // UPDATE user
router.delete('/users/:id', controller.delete) // DELETE user

export default router

Controller Pattern

Separate route handling logic:

// src/controllers/UserController.ts
import { Request, Response } from 'express'
import { UserService } from '../services/UserService'

export class UserController {
  private userService: UserService
  
  constructor() {
    this.userService = new UserService()
  }
  
  getAll = async (req: Request, res: Response) => {
    try {
      const users = await this.userService.findAll()
      
      res.status(200).json({
        success: true,
        data: users,
        count: users.length
      })
    } catch (error) {
      res.status(500).json({
        success: false,
        error: 'Failed to fetch users'
      })
    }
  }
  
  create = async (req: Request, res: Response) => {
    try {
      const user = await this.userService.create(req.body)
      
      res.status(201).json({
        success: true,
        data: user
      })
    } catch (error) {
      res.status(400).json({
        success: false,
        error: 'Failed to create user'
      })
    }
  }
}

Request Validation

Validate incoming data with Zod:

import { z } from 'zod'

const UserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().min(18).max(120),
  role: z.enum(['admin', 'user', 'guest'])
})

type User = z.infer<typeof UserSchema>

// Validation middleware
const validateUser = (req: Request, res: Response, next: NextFunction) => {
  try {
    UserSchema.parse(req.body)
    next()
  } catch (error) {
    res.status(400).json({
      success: false,
      error: 'Invalid user data',
      details: error.errors
    })
  }
}

router.post('/users', validateUser, controller.create)

Error Handling

Centralized error handling:

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express'

class AppError extends Error {
  statusCode: number
  
  constructor(message: string, statusCode: number) {
    super(message)
    this.statusCode = statusCode
  }
}

const errorHandler = (
  err: AppError,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const statusCode = err.statusCode || 500
  
  res.status(statusCode).json({
    success: false,
    error: err.message,
    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
  })
}

export { AppError, errorHandler }

Authentication with JWT

Secure your API with JWT tokens:

import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'

// Generate token
const generateToken = (userId: string) => {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET!,
    { expiresIn: '7d' }
  )
}

// Verify token middleware
const authenticate = (req: Request, res: Response, next: NextFunction) => {
  const token = req.headers.authorization?.split(' ')[1]
  
  if (!token) {
    return res.status(401).json({
      success: false,
      error: 'No token provided'
    })
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!)
    req.user = decoded
    next()
  } catch (error) {
    res.status(401).json({
      success: false,
      error: 'Invalid token'
    })
  }
}

Pagination and Filtering

Implement efficient pagination:

interface QueryParams {
  page?: string
  limit?: string
  sort?: string
  filter?: string
}

const getAll = async (req: Request<{}, {}, {}, QueryParams>, res: Response) => {
  const page = parseInt(req.query.page || '1')
  const limit = parseInt(req.query.limit || '10')
  const skip = (page - 1) * limit
  
  const users = await User.find()
    .skip(skip)
    .limit(limit)
    .sort({ createdAt: -1 })
  
  const total = await User.countDocuments()
  
  res.json({
    success: true,
    data: users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  })
}

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
  message: 'Too many requests from this IP'
})

app.use('/api', limiter)

API Documentation

Document your API with Swagger:

/**
 * @swagger
 * /users:
 *   get:
 *     summary: Get all users
 *     tags: [Users]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: Page number
 *     responses:
 *       200:
 *         description: List of users
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/User'
 */