API Design Best Practices: Building RESTful APIs That Scale
Learn how to design robust, scalable REST APIs with proper error handling, versioning, authentication, and documentation.
API Design Best Practices: Building RESTful APIs That Scale
A well-designed API is the backbone of modern applications. In this guide, I'll share the principles and patterns I use to build RESTful APIs that are intuitive, maintainable, and scale gracefully.
Core Principles
1. Resource-Based URLs
✅ Good: Resources as nouns
1GET /api/users
2GET /api/users/123
3POST /api/users
4PUT /api/users/123
5DELETE /api/users/123❌ Bad: Actions as verbs
1GET /api/getAllUsers
2GET /api/getUserById/123
3POST /api/createUser
4POST /api/updateUser/123
5POST /api/deleteUser/1232. Use HTTP Methods Correctly
1// GET - Read/Retrieve (Idempotent, Safe)
2app.get('/api/posts/:id', async (req, res) => {
3 const post = await db.post.findUnique({ where: { id: req.params.id } });
4 res.json(post);
5});
6
7// POST - Create (Not Idempotent)
8app.post('/api/posts', async (req, res) => {
9 const post = await db.post.create({ data: req.body });
10 res.status(201).json(post);
11});
12
13// PUT - Update/Replace (Idempotent)
14app.put('/api/posts/:id', async (req, res) => {
15 const post = await db.post.update({
16 where: { id: req.params.id },
17 data: req.body,
18 });
19 res.json(post);
20});
21
22// PATCH - Partial Update (Idempotent)
23app.patch('/api/posts/:id', async (req, res) => {
24 const post = await db.post.update({
25 where: { id: req.params.id },
26 data: req.body, // Only fields provided
27 });
28 res.json(post);
29});
30
31// DELETE - Remove (Idempotent)
32app.delete('/api/posts/:id', async (req, res) => {
33 await db.post.delete({ where: { id: req.params.id } });
34 res.status(204).send();
35});Error Handling
Consistent Error Format
1// types/api.ts
2export interface APIError {
3 error: {
4 message: string;
5 code: string;
6 status: number;
7 details?: unknown;
8 timestamp: string;
9 };
10}1// middleware/errorHandler.ts
2import { Request, Response, NextFunction } from 'express';
3
4export class AppError extends Error {
5 constructor(
6 public message: string,
7 public statusCode: number,
8 public code: string,
9 public details?: unknown,
10 ) {
11 super(message);
12 }
13}
14
15export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
16 if (err instanceof AppError) {
17 return res.status(err.statusCode).json({
18 error: {
19 message: err.message,
20 code: err.code,
21 status: err.statusCode,
22 details: err.details,
23 timestamp: new Date().toISOString(),
24 },
25 });
26 }
27
28 // Unexpected errors
29 if (process.env.NODE_ENV === 'development') {
30 console.error('Unexpected error:', err);
31 }
32 res.status(500).json({
33 error: {
34 message: 'Internal server error',
35 code: 'INTERNAL_ERROR',
36 status: 500,
37 timestamp: new Date().toISOString(),
38 },
39 });
40}Standard HTTP Status Codes
1// Success codes
2200 OK // Successful GET, PUT, PATCH
3201 Created // Successful POST
4204 No Content // Successful DELETE
5
6// Client error codes
7400 Bad Request // Invalid request data
8401 Unauthorized // Missing/invalid auth
9403 Forbidden // Insufficient permissions
10404 Not Found // Resource doesn't exist
11409 Conflict // Conflict with existing data
12422 Unprocessable // Validation errors
13429 Too Many Requests // Rate limit exceeded
14
15// Server error codes
16500 Internal Error // Server error
17502 Bad Gateway // Upstream service error
18503 Service Unavailable // Temporary downtime1// Example: User creation with validation
2app.post('/api/users', async (req, res, next) => {
3 try {
4 // Validate input
5 const { email, password, name } = req.body;
6
7 if (!email || !password) {
8 throw new AppError('Email and password are required', 400, 'VALIDATION_ERROR', {
9 fields: ['email', 'password'],
10 });
11 }
12
13 // Check for existing user
14 const existing = await db.user.findUnique({ where: { email } });
15 if (existing) {
16 throw new AppError('User with this email already exists', 409, 'USER_EXISTS', { email });
17 }
18
19 // Create user
20 const user = await db.user.create({
21 data: { email, password: await hash(password), name },
22 });
23
24 res.status(201).json({
25 data: {
26 id: user.id,
27 email: user.email,
28 name: user.name,
29 },
30 });
31 } catch (error) {
32 next(error);
33 }
34});Pagination
1interface PaginationQuery {
2 page?: string;
3 limit?: string;
4 sort?: string;
5 order?: 'asc' | 'desc';
6}
7
8interface PaginatedResponse<T> {
9 data: T[];
10 pagination: {
11 page: number;
12 limit: number;
13 total: number;
14 totalPages: number;
15 hasNext: boolean;
16 hasPrev: boolean;
17 };
18}
19
20app.get('/api/posts', async (req: Request<{}, {}, {}, PaginationQuery>, res) => {
21 const page = parseInt(req.query.page || '1');
22 const limit = parseInt(req.query.limit || '20');
23 const sort = req.query.sort || 'createdAt';
24 const order = req.query.order || 'desc';
25
26 const skip = (page - 1) * limit;
27
28 const [posts, total] = await Promise.all([
29 db.post.findMany({
30 skip,
31 take: limit,
32 orderBy: { [sort]: order },
33 }),
34 db.post.count(),
35 ]);
36
37 const totalPages = Math.ceil(total / limit);
38
39 res.json({
40 data: posts,
41 pagination: {
42 page,
43 limit,
44 total,
45 totalPages,
46 hasNext: page < totalPages,
47 hasPrev: page > 1,
48 },
49 });
50});Usage:
1GET /api/posts?page=2&limit=10&sort=createdAt&order=descFiltering and Searching
1interface PostFilter {
2 author?: string;
3 status?: 'draft' | 'published';
4 tags?: string;
5 search?: string;
6}
7
8app.get('/api/posts', async (req: Request<{}, {}, {}, PostFilter & PaginationQuery>, res) => {
9 const { author, status, tags, search } = req.query;
10
11 const where: any = {};
12
13 if (author) {
14 where.authorId = author;
15 }
16
17 if (status) {
18 where.status = status;
19 }
20
21 if (tags) {
22 where.tags = { hasSome: tags.split(',') };
23 }
24
25 if (search) {
26 where.OR = [
27 { title: { contains: search, mode: 'insensitive' } },
28 { content: { contains: search, mode: 'insensitive' } },
29 ];
30 }
31
32 const posts = await db.post.findMany({
33 where,
34 // ... pagination
35 });
36
37 res.json({ data: posts });
38});Usage:
1GET /api/posts?author=user123&status=published&tags=react,typescript&search=hooksAuthentication & Authorization
JWT-Based Auth
1// middleware/auth.ts
2import jwt from 'jsonwebtoken';
3
4export interface JWTPayload {
5 userId: string;
6 email: string;
7 role: 'user' | 'admin';
8}
9
10export async function authenticate(req: Request, res: Response, next: NextFunction) {
11 try {
12 const token = req.headers.authorization?.replace('Bearer ', '');
13
14 if (!token) {
15 throw new AppError('No token provided', 401, 'UNAUTHORIZED');
16 }
17
18 const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
19
20 // Attach user to request
21 req.user = payload;
22
23 next();
24 } catch (error) {
25 next(new AppError('Invalid token', 401, 'INVALID_TOKEN'));
26 }
27}Role-Based Authorization
1// middleware/authorize.ts
2export function authorize(...roles: string[]) {
3 return (req: Request, res: Response, next: NextFunction) => {
4 if (!req.user) {
5 throw new AppError('Not authenticated', 401, 'UNAUTHORIZED');
6 }
7
8 if (!roles.includes(req.user.role)) {
9 throw new AppError('Insufficient permissions', 403, 'FORBIDDEN');
10 }
11
12 next();
13 };
14}Usage:
1// Public route
2app.get('/api/posts', getPosts);
3
4// Authenticated route
5app.post('/api/posts', authenticate, createPost);
6
7// Admin-only route
8app.delete('/api/posts/:id', authenticate, authorize('admin'), deletePost);Versioning
URL Versioning (Recommended)
1// v1 routes
2app.use('/api/v1/posts', postsV1Router);
3
4// v2 routes with breaking changes
5app.use('/api/v2/posts', postsV2Router);Header Versioning (Alternative)
1app.get('/api/posts', (req, res) => {
2 const version = req.headers['api-version'] || '1';
3
4 if (version === '2') {
5 return handleV2(req, res);
6 }
7
8 return handleV1(req, res);
9});Rate Limiting
1import rateLimit from 'express-rate-limit';
2
3// General rate limit
4const limiter = rateLimit({
5 windowMs: 15 * 60 * 1000, // 15 minutes
6 max: 100, // 100 requests per window
7 message: {
8 error: {
9 message: 'Too many requests, please try again later',
10 code: 'RATE_LIMIT_EXCEEDED',
11 status: 429,
12 },
13 },
14});
15
16// Stricter limit for auth endpoints
17const authLimiter = rateLimit({
18 windowMs: 15 * 60 * 1000,
19 max: 5, // 5 login attempts
20 skipSuccessfulRequests: true,
21});
22
23app.use('/api', limiter);
24app.use('/api/auth', authLimiter);Documentation with OpenAPI/Swagger
1import swaggerJsdoc from 'swagger-jsdoc';
2import swaggerUi from 'swagger-ui-express';
3
4const swaggerSpec = swaggerJsdoc({
5 definition: {
6 openapi: '3.0.0',
7 info: {
8 title: 'Blog API',
9 version: '1.0.0',
10 description: 'RESTful API for blog application',
11 },
12 servers: [
13 { url: 'http://localhost:3000', description: 'Development' },
14 { url: 'https://api.example.com', description: 'Production' },
15 ],
16 },
17 apis: ['./routes/*.ts'],
18});
19
20app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));1/**
2 * @openapi
3 * /api/posts:
4 * get:
5 * summary: Get all posts
6 * tags: [Posts]
7 * parameters:
8 * - in: query
9 * name: page
10 * schema:
11 * type: integer
12 * default: 1
13 * responses:
14 * 200:
15 * description: List of posts
16 * content:
17 * application/json:
18 * schema:
19 * type: object
20 * properties:
21 * data:
22 * type: array
23 * items:
24 * $ref: '#/components/schemas/Post'
25 */
26app.get('/api/posts', getPosts);Best Practices Summary
✅ Use resource-based URLs - /api/users not /api/getUsers
✅ Leverage HTTP methods - GET, POST, PUT, PATCH, DELETE correctly
✅ Return proper status codes - 200, 201, 400, 404, 500, etc.
✅ Implement pagination - For list endpoints
✅ Add filtering/sorting - Make data queries flexible
✅ Handle errors consistently - Standard error format
✅ Secure your API - Authentication, authorization, rate limiting
✅ Version your API - Plan for breaking changes
✅ Document everything - OpenAPI/Swagger specs
A well-designed API is a joy to use and maintain. Follow these patterns, and you'll build APIs that scale with your application!
More from the Blog
Explore more articles and insights on software engineering, technology, and career development.
View all articles