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 JhaJune 5, 2024
4 min read

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'
*/