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);

📖 OpenAPI Specification →

✅ 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