Menu

HTML & JS Topic

Node.js RESTful APIs

What is a REST API?

REST (Representational State Transfer) is an architectural style for designing networked applications. A RESTful API is an API that follows REST principles, using HTTP protocols for communication.

Key Characteristics:

  • Client-Server Architecture: Separation of concerns

  • Stateless: Each request contains all necessary information

  • Cacheable: Responses can be cached

  • Uniform Interface: Consistent way of interacting

  • Layered System: Can have intermediaries (proxies, load balancers)

  • Code on Demand (optional): Servers can send executable code

REST vs SOAP vs GraphQL:

javascript

// REST (Resource-oriented)
GET    /api/users/123
POST   /api/users
PUT    /api/users/123
DELETE /api/users/123

// SOAP (Action-oriented - XML-based)
POST /soap-endpoint
Body: <soap:Envelope>
        <soap:Body>
          <getUser><id>123</id></getUser>
        </soap:Body>
      </soap:Envelope>

// GraphQL (Query language)
POST /graphql
Body: { query: "{ user(id: 123) { id name email } }" }

REST API Components:

javascript

// URL Structure
https://api.example.com/v1/users/123/posts?limit=10&page=1
│        │              │    │     │   │       └─────└── Query parameters
│        │              │    │     │   └── Resource ID
│        │              │    │     └── Sub-resource
│        │              │    └── Resource collection
│        │              └── API version
│        └── Domain
└── Protocol

// Example API endpoints
GET    /api/v1/users           // List all users
GET    /api/v1/users/:id       // Get specific user
POST   /api/v1/users           // Create new user
PUT    /api/v1/users/:id       // Update entire user
PATCH  /api/v1/users/:id       // Partially update user
DELETE /api/v1/users/:id       // Delete user
GET    /api/v1/users/:id/posts // Get user's posts

REST Principles

1. Resource-Based Architecture

Everything is a resource identified by URLs.

javascript

// Resources as nouns (not verbs)
// GOOD - Resource-oriented
GET    /articles          // Get all articles
GET    /articles/123      // Get article 123
POST   /articles          // Create article
PUT    /articles/123      // Update article 123
DELETE /articles/123      // Delete article 123

// BAD - Action-oriented (RPC style)
GET    /getAllArticles
GET    /getArticle?id=123
POST   /createArticle
POST   /updateArticle
POST   /deleteArticle

2. Statelessness

Each request must contain all information needed to process it.

javascript

// Stateless - Session data in each request
app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  const token = generateToken(username);
  res.json({ token }); // Client stores token
});

// Subsequent requests include token
app.get('/api/profile', authenticateToken, (req, res) => {
  // Token verified from Authorization header
  res.json({ user: req.user });
});

// Stateful (not RESTful) - Server stores session
app.post('/login', (req, res) => {
  req.session.userId = 123; // Server stores state
  res.json({ success: true });
});

app.get('/profile', (req, res) => {
  const userId = req.session.userId; // Relies on server state
  // Not RESTful!
});

3. Uniform Interface

Consistent way to interact with resources.

javascript

// HTTP methods map to CRUD operations
const operations = {
  // Collection operations
  'GET /users': 'Retrieve all users',
  'POST /users': 'Create a new user',
  
  // Single resource operations
  'GET /users/:id': 'Retrieve user by ID',
  'PUT /users/:id': 'Replace user by ID',
  'PATCH /users/:id': 'Partially update user by ID',
  'DELETE /users/:id': 'Delete user by ID',
  
  // Sub-resource operations
  'GET /users/:id/posts': 'Retrieve user\'s posts',
  'POST /users/:id/posts': 'Create post for user',
  
  // Special operations (rare, usually avoid)
  'POST /users/:id/activate': 'Custom action (not RESTful)'
};

// Resource representation (usually JSON)
const userRepresentation = {
  id: 123,
  name: "John Doe",
  email: "john@example.com",
  _links: { // HATEOAS - optional
    self: { href: "/api/v1/users/123" },
    posts: { href: "/api/v1/users/123/posts" }
  }
};

4. HATEOAS (Hypermedia As The Engine Of Application State)

Resources include links to related resources.

javascript

// API response with HATEOAS
app.get('/api/v1/users/:id', (req, res) => {
  const user = getUserById(req.params.id);
  
  res.json({
    ...user,
    _links: {
      self: { href: `/api/v1/users/${user.id}`, method: 'GET' },
      update: { href: `/api/v1/users/${user.id}`, method: 'PUT' },
      delete: { href: `/api/v1/users/${user.id}`, method: 'DELETE' },
      posts: { href: `/api/v1/users/${user.id}/posts`, method: 'GET' }
    }
  });
});

// Collection response with pagination links
app.get('/api/v1/users', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const { users, total } = getPaginatedUsers(page, limit);
  
  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    },
    _links: {
      self: { href: `/api/v1/users?page=${page}&limit=${limit}` },
      first: { href: `/api/v1/users?page=1&limit=${limit}` },
      prev: page > 1 ? { href: `/api/v1/users?page=${page-1}&limit=${limit}` } : null,
      next: page < Math.ceil(total / limit) ? 
            { href: `/api/v1/users?page=${page+1}&limit=${limit}` } : null,
      last: { href: `/api/v1/users?page=${Math.ceil(total / limit)}&limit=${limit}` }
    }
  });
});

5. Cacheability

Responses should indicate if they can be cached.

javascript

// Cache control headers
app.get('/api/v1/products', (req, res) => {
  const products = getAllProducts();
  
  // Cache for 1 hour (public cache)
  res.set('Cache-Control', 'public, max-age=3600');
  res.set('ETag', generateETag(products));
  
  res.json(products);
});

// Conditional requests (ETag/Last-Modified)
app.get('/api/v1/products/:id', (req, res) => {
  const product = getProduct(req.params.id);
  const etag = generateETag(product);
  
  // Check If-None-Match header
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // Not Modified
  }
  
  res.set('ETag', etag);
  res.set('Last-Modified', product.updatedAt.toUTCString());
  res.json(product);
});

// For frequently changing data
app.get('/api/v1/stock/:id', (req, res) => {
  res.set('Cache-Control', 'no-cache'); // Revalidate each time
  // or 'no-store' for sensitive data
  res.json(getStockInfo(req.params.id));
});

CRUD Operations

Complete RESTful CRUD Implementation:

javascript

// models/User.js
class UserModel {
  constructor() {
    this.users = [
      { id: 1, name: 'Alice', email: 'alice@example.com', active: true },
      { id: 2, name: 'Bob', email: 'bob@example.com', active: true }
    ];
    this.nextId = 3;
  }

  getAll(query = {}) {
    let filtered = [...this.users];
    
    // Filtering
    if (query.active) {
      filtered = filtered.filter(u => u.active === (query.active === 'true'));
    }
    
    if (query.name) {
      filtered = filtered.filter(u => 
        u.name.toLowerCase().includes(query.name.toLowerCase())
      );
    }
    
    // Sorting
    if (query.sort) {
      const [field, order] = query.sort.split(':');
      filtered.sort((a, b) => {
        const aVal = a[field];
        const bVal = b[field];
        const dir = order === 'desc' ? -1 : 1;
        return aVal > bVal ? dir : aVal < bVal ? -dir : 0;
      });
    }
    
    // Pagination
    const page = parseInt(query.page) || 1;
    const limit = parseInt(query.limit) || 10;
    const start = (page - 1) * limit;
    const end = start + limit;
    
    return {
      data: filtered.slice(start, end),
      total: filtered.length,
      page,
      limit,
      pages: Math.ceil(filtered.length / limit)
    };
  }

  getById(id) {
    return this.users.find(u => u.id === parseInt(id));
  }

  create(userData) {
    const newUser = {
      id: this.nextId++,
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date()
    };
    this.users.push(newUser);
    return newUser;
  }

  update(id, userData) {
    const index = this.users.findIndex(u => u.id === parseInt(id));
    if (index === -1) return null;
    
    this.users[index] = {
      ...this.users[index],
      ...userData,
      updatedAt: new Date()
    };
    
    return this.users[index];
  }

  partialUpdate(id, userData) {
    const index = this.users.findIndex(u => u.id === parseInt(id));
    if (index === -1) return null;
    
    // Only update provided fields
    Object.keys(userData).forEach(key => {
      if (userData[key] !== undefined) {
        this.users[index][key] = userData[key];
      }
    });
    
    this.users[index].updatedAt = new Date();
    return this.users[index];
  }

  delete(id) {
    const index = this.users.findIndex(u => u.id === parseInt(id));
    if (index === -1) return null;
    
    const deleted = this.users.splice(index, 1);
    return deleted[0];
  }
}

module.exports = new UserModel();

Controllers:

javascript

// controllers/userController.js
const User = require('../models/User');
const { validationResult } = require('express-validator');

// Helper function for responses
const sendResponse = (res, statusCode, data = null, message = '') => {
  const response = {
    success: statusCode >= 200 && statusCode < 300,
    message,
    data,
    timestamp: new Date().toISOString()
  };
  
  // Remove null/undefined fields
  Object.keys(response).forEach(key => {
    if (response[key] === null || response[key] === undefined) {
      delete response[key];
    }
  });
  
  res.status(statusCode).json(response);
};

// GET /api/v1/users - List all users
exports.getAllUsers = async (req, res) => {
  try {
    const result = User.getAll(req.query);
    
    // Add HATEOAS links
    const baseUrl = `${req.protocol}://${req.get('host')}/api/v1/users`;
    const links = {
      self: `${baseUrl}?page=${result.page}&limit=${result.limit}`,
      first: `${baseUrl}?page=1&limit=${result.limit}`,
      last: `${baseUrl}?page=${result.pages}&limit=${result.limit}`
    };
    
    if (result.page > 1) {
      links.prev = `${baseUrl}?page=${result.page - 1}&limit=${result.limit}`;
    }
    
    if (result.page < result.pages) {
      links.next = `${baseUrl}?page=${result.page + 1}&limit=${result.limit}`;
    }
    
    sendResponse(res, 200, {
      users: result.data,
      pagination: {
        page: result.page,
        limit: result.limit,
        total: result.total,
        pages: result.pages
      },
      _links: links
    });
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// GET /api/v1/users/:id - Get user by ID
exports.getUserById = async (req, res) => {
  try {
    const user = User.getById(req.params.id);
    
    if (!user) {
      return sendResponse(res, 404, null, 'User not found');
    }
    
    // Add self link
    const userWithLinks = {
      ...user,
      _links: {
        self: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`,
        update: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`,
        delete: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`
      }
    };
    
    sendResponse(res, 200, userWithLinks);
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// POST /api/v1/users - Create new user
exports.createUser = async (req, res) => {
  try {
    // Validate input
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return sendResponse(res, 400, { errors: errors.array() }, 'Validation failed');
    }
    
    const { name, email } = req.body;
    
    // Check for duplicate email
    const existingUser = User.getAll().data.find(u => u.email === email);
    if (existingUser) {
      return sendResponse(res, 409, null, 'Email already exists');
    }
    
    const newUser = User.create({ name, email });
    
    // Set Location header
    res.set('Location', `${req.protocol}://${req.get('host')}/api/v1/users/${newUser.id}`);
    
    sendResponse(res, 201, newUser, 'User created successfully');
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// PUT /api/v1/users/:id - Update entire user
exports.updateUser = async (req, res) => {
  try {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return sendResponse(res, 400, { errors: errors.array() }, 'Validation failed');
    }
    
    const { id } = req.params;
    const { name, email, active } = req.body;
    
    // Check if user exists
    const existingUser = User.getById(id);
    if (!existingUser) {
      return sendResponse(res, 404, null, 'User not found');
    }
    
    // Update user
    const updatedUser = User.update(id, { name, email, active });
    
    sendResponse(res, 200, updatedUser, 'User updated successfully');
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// PATCH /api/v1/users/:id - Partially update user
exports.partialUpdateUser = async (req, res) => {
  try {
    const { id } = req.params;
    
    // Check if user exists
    const existingUser = User.getById(id);
    if (!existingUser) {
      return sendResponse(res, 404, null, 'User not found');
    }
    
    // Validate at least one field is provided
    const updateFields = Object.keys(req.body);
    if (updateFields.length === 0) {
      return sendResponse(res, 400, null, 'No fields to update');
    }
    
    // Validate each field
    const errors = [];
    if (req.body.email && !isValidEmail(req.body.email)) {
      errors.push({ field: 'email', message: 'Invalid email format' });
    }
    
    if (errors.length > 0) {
      return sendResponse(res, 400, { errors }, 'Validation failed');
    }
    
    // Partial update
    const updatedUser = User.partialUpdate(id, req.body);
    
    sendResponse(res, 200, updatedUser, 'User partially updated');
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// DELETE /api/v1/users/:id - Delete user
exports.deleteUser = async (req, res) => {
  try {
    const { id } = req.params;
    
    const deletedUser = User.delete(id);
    
    if (!deletedUser) {
      return sendResponse(res, 404, null, 'User not found');
    }
    
    sendResponse(res, 200, null, 'User deleted successfully');
    
  } catch (error) {
    sendResponse(res, 500, null, 'Server error');
  }
};

// Helper function
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

Routes:

javascript

// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { body, param, query } = require('express-validator');
const userController = require('../controllers/userController');

// Validation middleware
const validateUser = [
  body('name')
    .trim()
    .isLength({ min: 2, max: 50 })
    .withMessage('Name must be between 2 and 50 characters'),
  body('email')
    .trim()
    .isEmail()
    .withMessage('Valid email is required')
    .normalizeEmail(),
  body('active')
    .optional()
    .isBoolean()
    .withMessage('Active must be a boolean')
];

const validateId = [
  param('id')
    .isInt({ min: 1 })
    .withMessage('ID must be a positive integer')
];

// Routes
router.route('/')
  .get(
    [
      query('page').optional().isInt({ min: 1 }),
      query('limit').optional().isInt({ min: 1, max: 100 }),
      query('active').optional().isBoolean(),
      query('sort').optional().isIn(['name:asc', 'name:desc', 'email:asc', 'email:desc'])
    ],
    userController.getAllUsers
  )
  .post(validateUser, userController.createUser);

router.route('/:id')
  .get(validateId, userController.getUserById)
  .put(validateId, validateUser, userController.updateUser)
  .patch(validateId, userController.partialUpdateUser)
  .delete(validateId, userController.deleteUser);

// Sub-resource routes
router.get('/:id/posts', validateId, async (req, res) => {
  // Get user's posts
  const posts = []; // Fetch from database
  res.json({
    data: posts,
    _links: {
      user: `/api/v1/users/${req.params.id}`
    }
  });
});

module.exports = router;

Main Application:

javascript

// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

const app = express();

// Middleware
app.use(helmet());
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
}));
app.use(compression());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests, please try again later.'
  }
});
app.use('/api/', limiter);

// Request logging middleware
app.use((req, res, next) => {
  req.requestId = Date.now().toString(36) + Math.random().toString(36).substr(2);
  console.log(`[${req.requestId}] ${req.method} ${req.url}`);
  next();
});

// API Routes
app.use('/api/v1/users', require('./routes/userRoutes'));

// Health check
app.get('/health', (req, res) => {
  res.json({
    status: 'OK',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// API documentation
app.get('/api-docs', (req, res) => {
  res.json({
    name: 'User Management API',
    version: '1.0.0',
    description: 'RESTful API for managing users',
    endpoints: {
      users: {
        getAll: { method: 'GET', path: '/api/v1/users' },
        getOne: { method: 'GET', path: '/api/v1/users/:id' },
        create: { method: 'POST', path: '/api/v1/users' },
        update: { method: 'PUT', path: '/api/v1/users/:id' },
        delete: { method: 'DELETE', path: '/api/v1/users/:id' }
      }
    }
  });
});

// 404 handler for API routes
app.use('/api/*', (req, res) => {
  res.status(404).json({
    error: 'Endpoint not found',
    requestId: req.requestId,
    path: req.originalUrl
  });
});

// Global error handler
app.use((err, req, res, next) => {
  console.error(`[${req.requestId}] Error:`, err);
  
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';
  
  res.status(statusCode).json({
    error: message,
    requestId: req.requestId,
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`REST API Server running on port ${PORT}`);
});

module.exports = app;

Status Codes

Informational (1xx):

javascript

// Rarely used directly in REST APIs
res.writeContinue(); // 100 Continue
res.writeProcessing(); // 102 Processing

Success (2xx):

javascript

// 200 OK - General success
app.get('/users', (req, res) => {
  res.status(200).json(users);
});

// 201 Created - Resource created
app.post('/users', (req, res) => {
  const newUser = createUser(req.body);
  res.status(201)
     .set('Location', `/users/${newUser.id}`)
     .json(newUser);
});

// 202 Accepted - Request accepted for processing
app.post('/webhooks', (req, res) => {
  processWebhookAsync(req.body); // Process later
  res.status(202).json({ message: 'Request accepted' });
});

// 204 No Content - Success but no response body
app.delete('/users/:id', (req, res) => {
  deleteUser(req.params.id);
  res.status(204).end();
});

// 205 Reset Content - Clear form/reset UI
app.post('/form-reset', (req, res) => {
  // Process data
  res.status(205).end(); // Client should reset form
});

// 206 Partial Content - For range requests
app.get('/large-file', (req, res) => {
  const range = req.headers.range;
  // Send partial content
  res.status(206)
     .set('Content-Range', 'bytes 0-999/5000')
     .send(partialData);
});

Redirection (3xx):

javascript

// 301 Moved Permanently
app.get('/old-users', (req, res) => {
  res.status(301).redirect('/api/v1/users');
});

// 302 Found (Temporary redirect)
app.get('/temp-redirect', (req, res) => {
  res.status(302).redirect('/temporary-page');
});

// 303 See Other - For POST results
app.post('/form-submit', (req, res) => {
  // Process form
  res.status(303).redirect('/success-page');
});

// 304 Not Modified - Cache validation
app.get('/cache-me', (req, res) => {
  const etag = generateETag(data);
  if (req.headers['if-none-match'] === etag) {
    res.status(304).end();
  } else {
    res.set('ETag', etag).json(data);
  }
});

// 307 Temporary Redirect - Preserve method
app.post('/temporary', (req, res) => {
  res.status(307).redirect('/new-location');
});

// 308 Permanent Redirect - Preserve method
app.post('/permanent', (req, res) => {
  res.status(308).redirect('/new-permanent-location');
});

Client Errors (4xx):

javascript

// 400 Bad Request - General client error
app.post('/validate', (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({ error: 'Email is required' });
  }
});

// 401 Unauthorized - Authentication needed
app.get('/secure', authenticate, (req, res) => {
  if (!req.user) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  res.json(secureData);
});

// 403 Forbidden - Authenticated but not authorized
app.get('/admin', (req, res) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Insufficient permissions' });
  }
  res.json(adminData);
});

// 404 Not Found - Resource doesn't exist
app.get('/users/:id', (req, res) => {
  const user = findUser(req.params.id);
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// 405 Method Not Allowed
app.route('/users')
  .get(/* handler */)
  .post(/* handler */);
// PUT to /users returns 405

// 406 Not Acceptable - Can't provide requested content type
app.get('/data', (req, res) => {
  const accept = req.headers.accept;
  if (!accept.includes('application/json')) {
    return res.status(406).json({ 
      error: 'Only JSON responses available' 
    });
  }
  res.json(data);
});

// 409 Conflict - Resource conflict
app.post('/users', (req, res) => {
  if (userExists(req.body.email)) {
    return res.status(409).json({ 
      error: 'User with this email already exists' 
    });
  }
  createUser(req.body);
});

// 410 Gone - Resource permanently removed
app.get('/old-api', (req, res) => {
  res.status(410).json({ 
    error: 'This API endpoint has been permanently removed',
    message: 'Please use /api/v2 instead'
  });
});

// 422 Unprocessable Entity - Semantic errors
app.post('/complex', validateComplex, (req, res) => {
  const errors = validate(req.body);
  if (errors.length > 0) {
    return res.status(422).json({ errors });
  }
  // Process
});

// 429 Too Many Requests - Rate limiting
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  handler: (req, res) => {
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: 15 * 60 // seconds
    });
  }
}));

Server Errors (5xx):

javascript

// 500 Internal Server Error - Generic server error
app.get('/error', (req, res) => {
  try {
    throw new Error('Something went wrong');
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 501 Not Implemented
app.put('/future-feature', (req, res) => {
  res.status(501).json({ error: 'Not implemented yet' });
});

// 502 Bad Gateway - Proxy/upstream error
app.get('/proxy-data', async (req, res) => {
  try {
    const data = await fetchFromUpstream();
    res.json(data);
  } catch (error) {
    res.status(502).json({ error: 'Bad gateway' });
  }
});

// 503 Service Unavailable - Maintenance/overload
app.use((req, res, next) => {
  if (process.env.MAINTENANCE_MODE === 'true') {
    return res.status(503)
      .set('Retry-After', 3600)
      .json({ error: 'Service undergoing maintenance' });
  }
  next();
});

// 504 Gateway Timeout
app.get('/slow-resource', async (req, res) => {
  const timeout = setTimeout(() => {
    res.status(504).json({ error: 'Gateway timeout' });
  }, 10000);
  
  try {
    const data = await slowOperation();
    clearTimeout(timeout);
    res.json(data);
  } catch (error) {
    clearTimeout(timeout);
    res.status(500).json({ error: 'Internal error' });
  }
});

API Versioning

1. URL Path Versioning (Most Common)

javascript

// routes/v1/userRoutes.js
const express = require('express');
const router = express.Router();

router.get('/users', (req, res) => {
  // Version 1 logic
  res.json({ version: 'v1', users: [] });
});

module.exports = router;

// routes/v2/userRoutes.js
const express = require('express');
const router = express.Router();

router.get('/users', (req, res) => {
  // Version 2 logic with new features
  res.json({ 
    version: 'v2', 
    users: [],
    metadata: { count: 0 }
  });
});

module.exports = router;

// app.js
app.use('/api/v1/users', require('./routes/v1/userRoutes'));
app.use('/api/v2/users', require('./routes/v2/userRoutes'));

// Also support latest version alias
app.use('/api/users', require('./routes/v2/userRoutes'));

2. Header Versioning

javascript

// Versioning via Accept header
app.use('/api/users', (req, res, next) => {
  const acceptHeader = req.headers.accept || '';
  const versionMatch = acceptHeader.match(/version=(\d+)/);
  const version = versionMatch ? versionMatch[1] : '1';
  
  if (version === '1') {
    require('./routes/v1/userRoutes')(req, res, next);
  } else if (version === '2') {
    require('./routes/v2/userRoutes')(req, res, next);
  } else {
    res.status(400).json({ error: 'Unsupported API version' });
  }
});

// Custom header
app.use('/api/users', (req, res, next) => {
  const apiVersion = req.headers['x-api-version'] || '1';
  
  if (apiVersion === '1') {
    // v1 logic
  } else if (apiVersion === '2') {
    // v2 logic
  }
});

3. Query Parameter Versioning

javascript

app.use('/api/users', (req, res, next) => {
  const version = req.query.api_version || '1';
  
  if (version === '1') {
    // v1 logic
  } else if (version === '2') {
    // v2 logic
  } else {
    res.status(400).json({ error: 'Invalid API version' });
  }
});

// Usage: GET /api/users?api_version=2

4. Comprehensive Versioning Strategy:

javascript

// versioning.js - Version negotiation middleware
const versioning = (options = {}) => {
  const {
    defaultVersion = '1',
    supportedVersions = ['1', '2'],
    headerName = 'x-api-version',
    paramName = 'api_version',
    strict = false
  } = options;
  
  return (req, res, next) => {
    let version = defaultVersion;
    
    // 1. Check URL path first (highest priority)
    const pathMatch = req.path.match(/\/v(\d+)\//);
    if (pathMatch) {
      version = pathMatch[1];
    }
    
    // 2. Check custom header
    else if (req.headers[headerName]) {
      version = req.headers[headerName];
    }
    
    // 3. Check query parameter
    else if (req.query[paramName]) {
      version = req.query[paramName];
    }
    
    // 4. Check Accept header
    else if (req.headers.accept) {
      const acceptMatch = req.headers.accept.match(/version=(\d+)/);
      if (acceptMatch) {
        version = acceptMatch[1];
      }
    }
    
    // Validate version
    if (strict && !supportedVersions.includes(version)) {
      return res.status(400).json({
        error: 'Unsupported API version',
        supportedVersions,
        defaultVersion
      });
    }
    
    // Normalize to supported version
    if (!supportedVersions.includes(version)) {
      version = defaultVersion;
    }
    
    // Attach version to request
    req.apiVersion = version;
    next();
  };
};

// Usage
app.use('/api', versioning({
  defaultVersion: '2',
  supportedVersions: ['1', '2', '3']
}));

app.get('/api/users', (req, res) => {
  if (req.apiVersion === '1') {
    // v1 response
    res.json({ version: 'v1', users: [] });
  } else if (req.apiVersion === '2') {
    // v2 response
    res.json({ version: 'v2', users: [], metadata: {} });
  } else {
    // v3 response
    res.json({ version: 'v3', data: { users: [] } });
  }
});

5. Version Deprecation Strategy:

javascript

// Middleware to handle deprecated versions
const deprecationNotice = (version, sunsetDate) => {
  return (req, res, next) => {
    if (req.apiVersion === version) {
      res.set({
        'Deprecation': `version="${version}"`,
        'Sunset': new Date(sunsetDate).toUTCString(),
        'Link': '</api/v2/users>; rel="successor-version"',
        'Warning': `299 - "Deprecated API version ${version}. Migrate to v2 by ${sunsetDate}"`
      });
    }
    next();
  };
};

// Usage
app.use('/api', versioning());
app.use('/api', deprecationNotice('1', '2024-12-31'));

// Version migration guide endpoint
app.get('/api/migration-guide', (req, res) => {
  res.json({
    title: 'API Migration Guide',
    versions: [
      {
        version: 'v1',
        status: 'deprecated',
        sunsetDate: '2024-12-31',
        changes: [
          'Removed field: user.age',
          'Added field: user.dateOfBirth',
          'Changed endpoint: POST /login to POST /auth/login'
        ]
      },
      {
        version: 'v2',
        status: 'current',
        changes: [
          'Added pagination metadata',
          'Added HATEOAS links',
          'Improved error responses'
        ]
      }
    ]
  });
});

Complete REST API with Versioning:

javascript

// project structure
project/
├── src/
│   ├── api/
│   │   ├── v1/
│   │   │   ├── controllers/
│   │   │   ├── models/
│   │   │   ├── routes/
│   │   │   └── index.js
│   │   ├── v2/
│   │   │   ├── controllers/
│   │   │   ├── models/
│   │   │   ├── routes/
│   │   │   └── index.js
│   │   └── middleware/
│   ├── config/
│   └── app.js
└── package.json

// src/api/v1/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');

router.use('/users', userRoutes);
router.use('/products', productRoutes);

module.exports = router;

// src/api/v2/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');

router.use('/users', userRoutes);
router.use('/products', productRoutes);

// New v2-only routes
router.use('/analytics', require('./routes/analyticsRoutes'));

module.exports = router;

// src/app.js
const express = require('express');
const app = express();

// Import versions
const v1Routes = require('./api/v1');
const v2Routes = require('./api/v2');

// Mount versioned routes
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

// Default to latest version
app.use('/api', v2Routes);

// API info endpoint
app.get('/api', (req, res) => {
  res.json({
    api: {
      name: 'My REST API',
      versions: {
        v1: {
          status: 'deprecated',
          endpoint: '/api/v1',
          sunset: '2024-12-31'
        },
        v2: {
          status: 'current',
          endpoint: '/api/v2'
        }
      },
      documentation: 'https://api.example.com/docs'
    }
  });
});

// Test the API
// GET /api/v1/users     → v1 users
// GET /api/v2/users     → v2 users  
// GET /api/users        → v2 users (default)
// GET /api              → API information

This comprehensive guide covers RESTful API design principles, implementation patterns, status code usage, and versioning strategies for building professional-grade APIs with Node.js and Express.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?