Building REST APIs
Design and build production-ready RESTful APIs with Express.js
RESTful API Design
REST (Representational State Transfer) is an architectural style for building APIs. A well-designed REST API is intuitive, consistent, and easy to use.
📐 REST Principles
Resources
Use nouns, not verbs (/users not /getUsers)
HTTP Methods
GET, POST, PUT, PATCH, DELETE
Status Codes
200, 201, 400, 401, 404, 500
Stateless
Each request contains all needed info
API Structure
// RESTful endpoint patterns
GET /api/v1/users // List all users
POST /api/v1/users // Create a user
GET /api/v1/users/:id // Get one user
PUT /api/v1/users/:id // Replace user
PATCH /api/v1/users/:id // Update user
DELETE /api/v1/users/:id // Delete user
// Nested resources
GET /api/v1/users/:userId/posts // User's posts
POST /api/v1/users/:userId/posts // Create post for user
GET /api/v1/users/:userId/posts/:id // Get specific post
// Query parameters for filtering, sorting, pagination
GET /api/v1/users?role=admin&sort=-createdAt&page=1&limit=10
// Project structure
src/
├── app.ts
├── server.ts
├── routes/
│ ├── index.ts
│ └── v1/
│ ├── index.ts
│ ├── users.ts
│ └── posts.ts
├── controllers/
│ ├── userController.ts
│ └── postController.ts
├── services/
│ ├── userService.ts
│ └── postService.ts
├── models/
├── middleware/
├── validators/
└── utils/
Complete CRUD Example
// routes/v1/users.ts
import { Router } from 'express';
import * as userController from '../../controllers/userController';
import { authenticate, authorize } from '../../middleware/auth';
import { validateUser, validateUserUpdate } from '../../validators/userValidator';
const router = Router();
router.route('/')
.get(userController.getAll)
.post(authenticate, authorize('admin'), validateUser, userController.create);
router.route('/:id')
.get(userController.getOne)
.patch(authenticate, validateUserUpdate, userController.update)
.delete(authenticate, authorize('admin'), userController.remove);
export default router;
// controllers/userController.ts
import { Request, Response } from 'express';
import * as userService from '../services/userService';
import { NotFoundError } from '../errors/AppError';
export const getAll = async (req: Request, res: Response) => {
const { page = 1, limit = 10, sort, role } = req.query;
const result = await userService.findAll({
page: Number(page),
limit: Number(limit),
sort: sort as string,
filter: role ? { role } : {}
});
res.json(result);
};
export const getOne = async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
};
export const create = async (req: Request, res: Response) => {
const user = await userService.create(req.body);
res.status(201).json(user);
};
export const update = async (req: Request, res: Response) => {
const user = await userService.update(req.params.id, req.body);
if (!user) throw new NotFoundError('User');
res.json(user);
};
export const remove = async (req: Request, res: Response) => {
await userService.remove(req.params.id);
res.status(204).send();
};
Request Validation with Zod
npm install zod
// validators/userValidator.ts
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
import { BadRequestError, ValidationError } from '../errors/AppError';
// Schema definitions
const createUserSchema = z.object({
body: z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
name: z.string().min(1, 'Name is required'),
role: z.enum(['user', 'admin']).optional()
})
});
const updateUserSchema = z.object({
body: z.object({
email: z.string().email().optional(),
name: z.string().min(1).optional(),
role: z.enum(['user', 'admin']).optional()
}).refine(data => Object.keys(data).length > 0, {
message: 'At least one field must be provided'
}),
params: z.object({
id: z.string()
})
});
// Validation middleware factory
const validate = (schema: z.ZodSchema) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params
});
next();
} catch (error) {
if (error instanceof z.ZodError) {
const errors = error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}));
throw new ValidationError(errors);
}
throw error;
}
};
};
export const validateUser = validate(createUserSchema);
export const validateUserUpdate = validate(updateUserSchema);
Pagination & Filtering
// services/userService.ts
import { prisma } from '../db/prisma';
interface QueryOptions {
page: number;
limit: number;
sort?: string;
filter?: Record<string, any>;
}
export const findAll = async ({ page, limit, sort, filter }: QueryOptions) => {
const skip = (page - 1) * limit;
// Build sort object
const orderBy = sort
? { [sort.replace('-', '')]: sort.startsWith('-') ? 'desc' : 'asc' }
: { createdAt: 'desc' };
const [users, total] = await Promise.all([
prisma.user.findMany({
where: filter,
skip,
take: limit,
orderBy,
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true
}
}),
prisma.user.count({ where: filter })
]);
return {
data: users,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
};
};
// Advanced filtering
export const search = async (query: string, filters: any) => {
return prisma.user.findMany({
where: {
AND: [
// Text search
query ? {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } }
]
} : {},
// Additional filters
filters.role ? { role: filters.role } : {},
filters.createdAfter ? { createdAt: { gte: new Date(filters.createdAfter) } } : {}
]
}
});
};
API Response Format
// Consistent response format
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
message: string;
code?: string;
errors?: any[];
};
meta?: {
page?: number;
limit?: number;
total?: number;
};
}
// Response helper
export const sendResponse = <T>(
res: Response,
statusCode: number,
data: T,
meta?: any
) => {
res.status(statusCode).json({
success: statusCode < 400,
data,
meta
});
};
// Usage
app.get('/users', async (req, res) => {
const result = await userService.findAll(options);
sendResponse(res, 200, result.data, result.meta);
});
// Error response format
{
"success": false,
"error": {
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "Invalid email format" }
]
}
}
API Versioning
// routes/index.ts
import { Router } from 'express';
import v1Routes from './v1';
import v2Routes from './v2';
const router = Router();
// Version in URL path (recommended)
router.use('/v1', v1Routes);
router.use('/v2', v2Routes);
export default router;
// app.ts
app.use('/api', routes);
// Results in: /api/v1/users, /api/v2/users
// Alternative: Version in header
app.use('/api/users', (req, res, next) => {
const version = req.headers['api-version'] || '1';
if (version === '2') {
return v2UserRoutes(req, res, next);
}
v1UserRoutes(req, res, next);
});
API Documentation with Swagger
npm install swagger-ui-express swagger-jsdoc
npm install -D @types/swagger-ui-express @types/swagger-jsdoc
// config/swagger.ts
import swaggerJsdoc from 'swagger-jsdoc';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation'
},
servers: [
{ url: 'http://localhost:3000/api/v1' }
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
},
apis: ['./src/routes/**/*.ts']
};
export const specs = swaggerJsdoc(options);
// app.ts
import swaggerUi from 'swagger-ui-express';
import { specs } from './config/swagger';
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
// Document routes with JSDoc
/**
* @swagger
* /users:
* get:
* summary: Get all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* responses:
* 200:
* description: List of users
*/
router.get('/', userController.getAll);
✅ REST API Checklist
- ☐ Use plural nouns for resources (/users, /posts)
- ☐ Use proper HTTP methods and status codes
- ☐ Validate all input data
- ☐ Implement pagination for list endpoints
- ☐ Use consistent response format
- ☐ Version your API (/api/v1/...)
- ☐ Document with OpenAPI/Swagger
- ☐ Implement proper error handling
- ☐ Add rate limiting
- ☐ Use HTTPS in production