Introduction

RESTful API design is crucial for building scalable, maintainable applications. In this lesson, you'll learn REST principles, API versioning strategies, proper HTTP status code usage, error handling patterns, and how to document your APIs professionally with Swagger/OpenAPI.

Learning Objectives

  • Understand REST architectural principles
  • Design resource-based API endpoints
  • Implement proper HTTP status codes
  • Version your APIs effectively
  • Document APIs with Swagger/OpenAPI
  • Apply API best practices and conventions

Prerequisites

  • Completion of Lessons 01-09
  • Strong understanding of HTTP methods
  • Experience building Express applications

Estimated Time

4 hours (including practice exercises)

REST Principles

REST (Representational State Transfer) is an architectural style with six constraints:

  • Client-Server - Separation of concerns
  • Stateless - Each request contains all necessary information
  • Cacheable - Responses must define themselves as cacheable or not
  • Uniform Interface - Consistent resource identification
  • Layered System - Client cannot tell if connected directly to server
  • Code on Demand - Optional: servers can extend client functionality

Resource Naming Conventions

Best Practices
// Good - Use nouns, not verbs
GET    /api/users
GET    /api/users/123
POST   /api/users
PUT    /api/users/123
DELETE /api/users/123

// Good - Use plural nouns
GET /api/products
GET /api/orders

// Good - Nested resources
GET /api/users/123/orders
GET /api/posts/456/comments

// Bad - Don't use verbs
GET /api/getUsers
POST /api/createUser
DELETE /api/deleteUser

// Bad - Don't use mixed case
GET /api/UserProfiles
GET /api/user_profiles

HTTP Status Codes

Success Codes (2xx)

  • 200 OK - Request succeeded
  • 201 Created - Resource created successfully
  • 204 No Content - Success but no content to return

Client Error Codes (4xx)

  • 400 Bad Request - Invalid request data
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Authenticated but not authorized
  • 404 Not Found - Resource doesn't exist
  • 409 Conflict - Request conflicts with current state
  • 422 Unprocessable Entity - Validation errors
  • 429 Too Many Requests - Rate limit exceeded

Server Error Codes (5xx)

  • 500 Internal Server Error - Generic server error
  • 503 Service Unavailable - Server temporarily unavailable

API Versioning

URL Versioning (Recommended)

JavaScript
// Version in URL path
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));

// Example endpoints
GET /api/v1/users
GET /api/v2/users

Header Versioning

JavaScript
// Custom header
Accept-Version: v1

// Middleware to handle version
app.use((req, res, next) => {
  const version = req.headers['accept-version'] || 'v1';
  req.apiVersion = version;
  next();
});

Standardized Response Format

utils/response.js
// Success response
exports.success = (res, data, message = 'Success', statusCode = 200) => {
  return res.status(statusCode).json({
    success: true,
    message,
    data
  });
};

// Error response
exports.error = (res, message = 'Error', statusCode = 500, errors = null) => {
  return res.status(statusCode).json({
    success: false,
    message,
    errors
  });
};

// Paginated response
exports.paginated = (res, data, page, limit, total) => {
  return res.json({
    success: true,
    data,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total,
      pages: Math.ceil(total / limit)
    }
  });
};

Pagination, Filtering & Sorting

controllers/productController.js
exports.getProducts = async (req, res) => {
  try {
    const { page = 1, limit = 10, sort = '-createdAt', category, minPrice, maxPrice, search } = req.query;
    
    // Build query
    const query = {};
    
    if (category) query.category = category;
    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }
    if (search) {
      query.$or = [
        { name: { $regex: search, $options: 'i' } },
        { description: { $regex: search, $options: 'i' } }
      ];
    }
    
    // Execute query
    const products = await Product.find(query)
      .sort(sort)
      .limit(limit * 1)
      .skip((page - 1) * limit);
    
    const count = await Product.countDocuments(query);
    
    res.json({
      success: true,
      data: products,
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total: count,
        pages: Math.ceil(count / limit)
      }
    });
  } catch (error) {
    res.status(500).json({ success: false, message: error.message });
  }
};

Error Handling Middleware

middleware/errorHandler.js
class ApiError extends Error {
  constructor(statusCode, message, errors = null) {
    super(message);
    this.statusCode = statusCode;
    this.errors = errors;
  }
}

const errorHandler = (err, req, res, next) => {
  let { statusCode, message, errors } = err;
  
  // Mongoose validation error
  if (err.name === 'ValidationError') {
    statusCode = 422;
    errors = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message
    }));
  }
  
  // Mongoose duplicate key error
  if (err.code === 11000) {
    statusCode = 409;
    message = 'Duplicate field value';
  }
  
  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    statusCode = 401;
    message = 'Invalid token';
  }
  
  res.status(statusCode || 500).json({
    success: false,
    message: message || 'Internal server error',
    errors
  });
};

module.exports = { ApiError, errorHandler };

API Documentation with Swagger

Terminal
npm install swagger-jsdoc swagger-ui-express
config/swagger.js
const swaggerJsdoc = require('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', description: 'Development' },
      { url: 'https://api.example.com', description: 'Production' }
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT'
        }
      }
    }
  },
  apis: ['./routes/*.js']
};

module.exports = swaggerJsdoc(options);
server.js
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./config/swagger');

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
routes/users.js (with Swagger annotations)
/**
 * @swagger
 * /api/users:
 *   get:
 *     summary: Get all users
 *     tags: [Users]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: Page number
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *         description: Items per page
 *     responses:
 *       200:
 *         description: Success
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/User'
 */
router.get('/', userController.getAll);

Rate Limiting

middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP',
  standardHeaders: true,
  legacyHeaders: false
});

const strictLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: 'Too many attempts, please try again later'
});

module.exports = { apiLimiter, strictLimiter };

Key Takeaways

  • Use nouns for resources, not verbs
  • Implement proper HTTP status codes
  • Version your API from the start
  • Provide consistent response formats
  • Document your API with Swagger/OpenAPI
  • Implement pagination for large datasets
  • Use rate limiting to prevent abuse
  • Handle errors consistently

Practice Exercises

Exercise 1: Design a Blog API

Task
// Design RESTful endpoints for:
// - Posts (CRUD)
// - Comments (nested under posts)
// - Categories
// - Tags
// Include proper status codes and responses

Exercise 2: Implement API Versioning

Task
// Create v1 and v2 of an API
// v1: Returns user with all fields
// v2: Returns user with selected fields only
// Implement both versions side by side

Exercise 3: Add Swagger Documentation

Task
// Document your API with Swagger
// Include all endpoints
// Add request/response schemas
// Test with Swagger UI

Exercise 4: Advanced Filtering

Task
// Implement advanced filtering:
// - Multiple field filters
// - Range queries (price, date)
// - Full-text search
// - Sorting by multiple fields

What's Next?

In Lesson 11, you'll learn testing and deployment strategies. You'll write unit tests with Jest, implement integration tests, set up CI/CD pipelines, and deploy your applications to production environments.