Back to Articles
30 min read

Engineering Professional APIs with Express.js: REST Standards, GraphQL, and Documentation

A robust API is more than just routing logic—it requires a standardized contract, discoverability, and resilience. This comprehensive guide covers the spectrum of modern API development: from implementing rigorous RESTful standards and pagination strategies to handling CORS issues, enforcing rate limits, and hybridizing your architecture with GraphQL.

API Development

RESTful API Design

REST (Representational State Transfer) is an architectural style that uses HTTP methods semantically, resource-based URLs, and stateless communication to create predictable, scalable APIs that are easy to understand and consume.

// RESTful API structure const express = require('express'); const router = express.Router(); // Resource: /api/users router .route('/users') .get(getUsers) // GET /users - List all users .post(createUser); // POST /users - Create a user router .route('/users/:id') .get(getUser) // GET /users/:id - Get one user .put(updateUser) // PUT /users/:id - Replace user .patch(patchUser) // PATCH /users/:id - Partial update .delete(deleteUser);// DELETE /users/:id - Delete user // Nested resources router.get('/users/:userId/posts', getUserPosts); router.post('/users/:userId/posts', createUserPost); // Proper HTTP status codes async function createUser(req, res) { const user = await User.create(req.body); res.status(201) // Created .location(`/api/users/${user.id}`) // Location header .json(user); } async function getUser(req, res) { const user = await User.findById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); // 200 OK (implicit) } async function deleteUser(req, res) { await User.findByIdAndDelete(req.params.id); res.status(204).send(); // No Content } // Standard response format const response = { success: true, data: { /* resource data */ }, meta: { timestamp: new Date().toISOString(), requestId: req.id } };
┌─────────────────────────────────────────────────────────────┐
│                   REST API CONVENTIONS                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  HTTP Method │ CRUD   │ Endpoint        │ Response         │
│  ────────────┼────────┼─────────────────┼─────────────────  │
│  GET         │ Read   │ /users          │ 200 + list       │
│  GET         │ Read   │ /users/:id      │ 200 + item       │
│  POST        │ Create │ /users          │ 201 + created    │
│  PUT         │ Replace│ /users/:id      │ 200 + updated    │
│  PATCH       │ Update │ /users/:id      │ 200 + updated    │
│  DELETE      │ Delete │ /users/:id      │ 204 No Content   │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                   URL NAMING CONVENTIONS                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✓ /users                    (plural nouns)                 │
│  ✓ /users/123                (resource ID)                  │
│  ✓ /users/123/posts          (nested resources)             │
│  ✓ /users?status=active      (filtering)                    │
│                                                             │
│  ✗ /getUsers                 (no verbs)                     │
│  ✗ /user/create              (no actions in URL)            │
│  ✗ /Users/123                (lowercase)                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

API Versioning

API versioning allows you to evolve your API over time without breaking existing clients, providing a way to introduce breaking changes while maintaining backward compatibility for consumers using older versions.

// Method 1: URL Path versioning (most common) const v1Router = require('./routes/v1'); const v2Router = require('./routes/v2'); app.use('/api/v1', v1Router); app.use('/api/v2', v2Router); // routes/v1/users.js router.get('/users', (req, res) => { res.json({ users: [{ name: 'John' }] }); // v1 format }); // routes/v2/users.js router.get('/users', (req, res) => { res.json({ data: [{ fullName: 'John Doe', email: 'john@example.com' }], meta: { version: 2 } }); // v2 format with different structure }); // Method 2: Header versioning const apiVersion = (req, res, next) => { const version = req.headers['api-version'] || req.headers['accept-version'] || '1'; req.apiVersion = parseInt(version); next(); }; app.use(apiVersion); app.get('/api/users', (req, res) => { if (req.apiVersion === 2) { return res.json({ data: users, meta: {} }); } res.json({ users }); // v1 default }); // Method 3: Accept header (content negotiation) // Accept: application/vnd.myapi.v2+json app.get('/api/users', (req, res) => { const accept = req.headers.accept || ''; const match = accept.match(/application\/vnd\.myapi\.v(\d+)\+json/); const version = match ? parseInt(match[1]) : 1; // Return appropriate version }); // Method 4: Query parameter // GET /api/users?version=2 app.get('/api/users', (req, res) => { const version = parseInt(req.query.version) || 1; // ... });
┌─────────────────────────────────────────────────────────────┐
│                 VERSIONING STRATEGIES                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Strategy       │ Example                  │ Pros/Cons      │
│  ───────────────┼──────────────────────────┼──────────────  │
│  URL Path       │ /api/v1/users            │ ✓ Clear        │
│                 │ /api/v2/users            │ ✓ Cacheable    │
│                 │                          │ ✗ URL clutter  │
│  ───────────────┼──────────────────────────┼──────────────  │
│  Header         │ Api-Version: 2           │ ✓ Clean URLs   │
│                 │ Accept-Version: 2        │ ✗ Not visible  │
│  ───────────────┼──────────────────────────┼──────────────  │
│  Accept Header  │ Accept: application/     │ ✓ RESTful      │
│                 │ vnd.api.v2+json          │ ✗ Complex      │
│  ───────────────┼──────────────────────────┼──────────────  │
│  Query Param    │ /api/users?version=2     │ ✓ Simple       │
│                 │                          │ ✗ Not RESTful  │
│                                                             │
│  Recommendation: URL Path for public APIs                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) is a REST constraint where API responses include hyperlinks to related actions and resources, enabling clients to discover available operations dynamically without hardcoding URLs.

// HATEOAS response example app.get('/api/users/:id', async (req, res) => { const user = await User.findById(req.params.id); res.json({ data: { id: user.id, name: user.name, email: user.email, status: user.status }, links: { self: { href: `/api/users/${user.id}`, method: 'GET' }, update: { href: `/api/users/${user.id}`, method: 'PUT' }, delete: { href: `/api/users/${user.id}`, method: 'DELETE' }, posts: { href: `/api/users/${user.id}/posts`, method: 'GET' }, avatar: { href: `/api/users/${user.id}/avatar`, method: 'GET' } }, actions: { deactivate: user.status === 'active' ? { href: `/api/users/${user.id}/deactivate`, method: 'POST' } : null, activate: user.status === 'inactive' ? { href: `/api/users/${user.id}/activate`, method: 'POST' } : null } }); }); // Collection with HATEOAS app.get('/api/users', async (req, res) => { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const users = await User.find().skip((page-1)*limit).limit(limit); const total = await User.countDocuments(); res.json({ data: users.map(user => ({ ...user.toJSON(), links: { self: { href: `/api/users/${user.id}` } } })), links: { self: { href: `/api/users?page=${page}&limit=${limit}` }, first: { href: `/api/users?page=1&limit=${limit}` }, prev: page > 1 ? { href: `/api/users?page=${page-1}&limit=${limit}` } : null, next: page * limit < total ? { href: `/api/users?page=${page+1}&limit=${limit}` } : null, last: { href: `/api/users?page=${Math.ceil(total/limit)}&limit=${limit}` } }, meta: { total, page, limit, pages: Math.ceil(total/limit) } }); });
┌─────────────────────────────────────────────────────────────┐
│                    HATEOAS RESPONSE                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  GET /api/orders/123                                        │
│                                                             │
│  {                                                          │
│    "data": {                                                │
│      "id": 123,                                             │
│      "status": "pending",                                   │
│      "total": 99.99                                         │
│    },                                                       │
│    "links": {                                               │
│      "self":    "/api/orders/123",                          │
│      "payment": "/api/orders/123/pay",     ◀── Discoverable │
│      "cancel":  "/api/orders/123/cancel",       actions     │
│      "items":   "/api/orders/123/items"                     │
│    }                                                        │
│  }                                                          │
│                                                             │
│  Benefits:                                                  │
│  ✓ Client doesn't need to construct URLs                   │
│  ✓ API can evolve without breaking clients                 │
│  ✓ Self-documenting responses                              │
│  ✓ Available actions are explicit                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Request/Response Formatting

Consistent request/response formatting ensures predictable API behavior, using standard structures for success responses, errors, and data transformation through DTOs (Data Transfer Objects) to maintain a clean contract with API consumers.

// Response wrapper utility class ApiResponse { static success(res, data, statusCode = 200, meta = {}) { return res.status(statusCode).json({ success: true, data, meta: { timestamp: new Date().toISOString(), ...meta } }); } static error(res, message, statusCode = 500, errors = null) { return res.status(statusCode).json({ success: false, error: { message, code: statusCode, ...(errors && { details: errors }) }, meta: { timestamp: new Date(). # Express.js Comprehensive Guide - Part 3 (Continued) --- ### Request/Response Formatting (Continued) ```javascript // Response wrapper utility (continued) class ApiResponse { static success(res, data, statusCode = 200, meta = {}) { return res.status(statusCode).json({ success: true, data, meta: { timestamp: new Date().toISOString(), ...meta } }); } static error(res, message, statusCode = 500, errors = null) { return res.status(statusCode).json({ success: false, error: { message, code: statusCode, ...(errors && { details: errors }) }, meta: { timestamp: new Date().toISOString() } }); } static paginated(res, data, pagination) { return res.status(200).json({ success: true, data, pagination: { page: pagination.page, limit: pagination.limit, total: pagination.total, pages: Math.ceil(pagination.total / pagination.limit) }, meta: { timestamp: new Date().toISOString() } }); } } // DTO (Data Transfer Object) pattern class UserDTO { static toResponse(user) { return { id: user.id, name: user.name, email: user.email, avatar: user.avatar, createdAt: user.createdAt // Excludes: password, internalNotes, etc. }; } static toList(users) { return users.map(user => this.toResponse(user)); } } // Usage in routes app.get('/api/users', async (req, res) => { const users = await User.find(); ApiResponse.success(res, UserDTO.toList(users)); }); app.get('/api/users/:id', async (req, res) => { const user = await User.findById(req.params.id); if (!user) { return ApiResponse.error(res, 'User not found', 404); } ApiResponse.success(res, UserDTO.toResponse(user)); }); app.post('/api/users', async (req, res) => { const user = await User.create(req.body); ApiResponse.success(res, UserDTO.toResponse(user), 201); });
┌─────────────────────────────────────────────────────────────┐
│              STANDARD RESPONSE FORMATS                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  SUCCESS RESPONSE:              ERROR RESPONSE:             │
│  ┌─────────────────────┐        ┌─────────────────────┐    │
│  │ {                   │        │ {                   │    │
│  │   "success": true,  │        │   "success": false, │    │
│  │   "data": {         │        │   "error": {        │    │
│  │     "id": 1,        │        │     "message": "..",│    │
│  │     "name": "John"  │        │     "code": 404,    │    │
│  │   },                │        │     "details": []   │    │
│  │   "meta": {         │        │   },                │    │
│  │     "timestamp":".."│        │   "meta": {...}     │    │
│  │   }                 │        │ }                   │    │
│  │ }                   │        └─────────────────────┘    │
│  └─────────────────────┘                                   │
│                                                             │
│  PAGINATED RESPONSE:                                        │
│  ┌─────────────────────────────────────────────┐           │
│  │ {                                           │           │
│  │   "success": true,                          │           │
│  │   "data": [...],                            │           │
│  │   "pagination": {                           │           │
│  │     "page": 1, "limit": 10,                 │           │
│  │     "total": 100, "pages": 10               │           │
│  │   }                                         │           │
│  │ }                                           │           │
│  └─────────────────────────────────────────────┘           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Pagination

Pagination divides large datasets into manageable chunks, reducing response sizes and improving performance, with common strategies including offset-based (page/limit), cursor-based (for real-time data), and keyset pagination.

// Offset-based pagination (most common) app.get('/api/posts', async (req, res) => { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10)); const skip = (page - 1) * limit; const [posts, total] = await Promise.all([ Post.find().skip(skip).limit(limit).sort({ createdAt: -1 }), Post.countDocuments() ]); res.json({ data: posts, pagination: { page, limit, total, pages: Math.ceil(total / limit), hasNext: page * limit < total, hasPrev: page > 1 } }); }); // Cursor-based pagination (better for large datasets) app.get('/api/feed', async (req, res) => { const limit = parseInt(req.query.limit) || 20; const cursor = req.query.cursor; // Last item's ID or timestamp const query = cursor ? { _id: { $lt: cursor } } // Get items before cursor : {}; const posts = await Post.find(query) .sort({ _id: -1 }) .limit(limit + 1); // Fetch one extra to check hasMore const hasMore = posts.length > limit; if (hasMore) posts.pop(); // Remove extra item const nextCursor = posts.length > 0 ? posts[posts.length - 1]._id : null; res.json({ data: posts, pagination: { nextCursor, hasMore } }); }); // Pagination middleware const paginate = (defaultLimit = 10, maxLimit = 100) => { return (req, res, next) => { req.pagination = { page: Math.max(1, parseInt(req.query.page) || 1), limit: Math.min(maxLimit, Math.max(1, parseInt(req.query.limit) || defaultLimit)) }; req.pagination.skip = (req.pagination.page - 1) * req.pagination.limit; next(); }; }; app.get('/api/users', paginate(20, 50), async (req, res) => { const { skip, limit, page } = req.pagination; // Use pagination values... });
┌─────────────────────────────────────────────────────────────┐
│                  PAGINATION STRATEGIES                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  OFFSET-BASED:                   CURSOR-BASED:              │
│  ┌─────────────────┐            ┌─────────────────┐        │
│  │ ?page=2&limit=10│            │ ?cursor=abc123  │        │
│  └────────┬────────┘            └────────┬────────┘        │
│           │                              │                  │
│           ▼                              ▼                  │
│  ┌─────────────────┐            ┌─────────────────┐        │
│  │ SKIP 10, LIMIT 10│           │ WHERE id < cursor│        │
│  └─────────────────┘            │ LIMIT 10        │        │
│                                 └─────────────────┘        │
│  ✓ Random page access           ✓ Consistent results       │
│  ✓ Simple to implement          ✓ Better performance       │
│  ✗ Slow on large offsets        ✗ No random access         │
│  ✗ Inconsistent if data changes ✗ Slightly complex         │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Page 1    Page 2    Page 3    Page 4    Page 5     │   │
│  │ [1-10]    [11-20]   [21-30]   [31-40]   [41-50]    │   │
│  │    │         │         │         │         │        │   │
│  │    └─────────┴─────────┴─────────┴─────────┘        │   │
│  │                    Total: 50 items                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Filtering and Sorting

Filtering and sorting allow clients to request specific subsets of data and control the order of results, making APIs flexible and reducing the need for multiple specialized endpoints.

// Advanced filtering and sorting app.get('/api/products', async (req, res) => { const { // Filtering category, minPrice, maxPrice, inStock, search, // Sorting sort = '-createdAt', // Default: newest first // Pagination page = 1, limit = 20 } = req.query; // Build filter object const filter = {}; if (category) filter.category = category; if (inStock !== undefined) filter.inStock = inStock === 'true'; if (minPrice || maxPrice) { filter.price = {}; if (minPrice) filter.price.$gte = parseFloat(minPrice); if (maxPrice) filter.price.$lte = parseFloat(maxPrice); } if (search) { filter.$or = [ { name: { $regex: search, $options: 'i' } }, { description: { $regex: search, $options: 'i' } } ]; } // Parse sort string: "price,-createdAt" -> { price: 1, createdAt: -1 } const sortObj = {}; sort.split(',').forEach(field => { if (field.startsWith('-')) { sortObj[field.substring(1)] = -1; } else { sortObj[field] = 1; } }); const products = await Product.find(filter) .sort(sortObj) .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Product.countDocuments(filter); res.json({ data: products, total, page: parseInt(page), limit: parseInt(limit) }); }); // Query builder middleware const queryBuilder = (allowedFilters, allowedSorts) => { return (req, res, next) => { const filter = {}; const sort = {}; // Build filters from allowed fields allowedFilters.forEach(field => { if (req.query[field]) { filter[field] = req.query[field]; } // Range queries if (req.query[`${field}_gte`]) { filter[field] = { ...filter[field], $gte: req.query[`${field}_gte`] }; } if (req.query[`${field}_lte`]) { filter[field] = { ...filter[field], $lte: req.query[`${field}_lte`] }; } }); // Build sort if (req.query.sort) { const sortFields = req.query.sort.split(','); sortFields.forEach(field => { const cleanField = field.replace('-', ''); if (allowedSorts.includes(cleanField)) { sort[cleanField] = field.startsWith('-') ? -1 : 1; } }); } req.filter = filter; req.sort = Object.keys(sort).length ? sort : { createdAt: -1 }; next(); }; }; // Usage app.get('/api/orders', queryBuilder( ['status', 'customerId', 'total'], // Allowed filters ['createdAt', 'total', 'status'] // Allowed sort fields ), async (req, res) => { const orders = await Order.find(req.filter).sort(req.sort); res.json({ data: orders }); } );
┌─────────────────────────────────────────────────────────────┐
│              FILTERING & SORTING EXAMPLES                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  FILTERING:                                                 │
│  ─────────────────────────────────────────────────────────  │
│  GET /products?category=electronics                         │
│  GET /products?minPrice=100&maxPrice=500                    │
│  GET /products?inStock=true                                 │
│  GET /products?category=electronics&inStock=true            │
│                                                             │
│  SORTING:                                                   │
│  ─────────────────────────────────────────────────────────  │
│  GET /products?sort=price          (ascending)              │
│  GET /products?sort=-price         (descending)             │
│  GET /products?sort=-createdAt,name (multiple fields)       │
│                                                             │
│  COMBINED:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  GET /products?category=electronics&minPrice=100            │
│               &sort=-rating&page=1&limit=20                 │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Request                                             │   │
│  │  GET /products?category=phones&sort=-price&limit=5   │   │
│  │                                                      │   │
│  │  Response: [iPhone, Samsung, Pixel, OnePlus, Xiaomi] │   │
│  │            (phones sorted by price high to low)      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Search Implementation

Search functionality enables users to find resources using text queries, implemented through database text indexes, full-text search engines like Elasticsearch, or simple regex matching for smaller datasets.

// Simple search with MongoDB text index // First, create text index: db.products.createIndex({ name: "text", description: "text" }) app.get('/api/search', async (req, res) => { const { q, category, page = 1, limit = 20 } = req.query; if (!q || q.length < 2) { return res.status(400).json({ error: 'Search query too short' }); } const filter = { $text: { $search: q } }; if (category) filter.category = category; const products = await Product.find(filter, { score: { $meta: 'textScore' } // Include relevance score }) .sort({ score: { $meta: 'textScore' } }) // Sort by relevance .skip((page - 1) * limit) .limit(parseInt(limit)); const total = await Product.countDocuments(filter); res.json({ data: products, query: q, total, page: parseInt(page) }); }); // Elasticsearch integration for advanced search const { Client } = require('@elastic/elasticsearch'); const elastic = new Client({ node: 'http://localhost:9200' }); app.get('/api/search/advanced', async (req, res) => { const { q, category, minPrice, maxPrice, page = 1, limit = 20 } = req.query; const must = [ { multi_match: { query: q, fields: ['name^3', 'description', 'tags'], // name has 3x weight fuzziness: 'AUTO' // Handles typos } } ]; const filter = []; if (category) filter.push({ term: { category } }); if (minPrice || maxPrice) { filter.push({ range: { price: { ...(minPrice && { gte: parseFloat(minPrice) }), ...(maxPrice && { lte: parseFloat(maxPrice) }) } } }); } const result = await elastic.search({ index: 'products', body: { query: { bool: { must, filter } }, highlight: { fields: { name: {}, description: {} } }, from: (page - 1) * limit, size: parseInt(limit) } }); res.json({ data: result.hits.hits.map(hit => ({ ...hit._source, _score: hit._score, _highlights: hit.highlight })), total: result.hits.total.value, page: parseInt(page) }); }); // Autocomplete / Suggestions app.get('/api/search/suggest', async (req, res) => { const { q } = req.query; const suggestions = await Product.find({ name: { $regex: `^${q}`, $options: 'i' } }) .select('name category') .limit(10); res.json({ suggestions }); });
┌─────────────────────────────────────────────────────────────┐
│                   SEARCH ARCHITECTURE                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────┐     ┌─────────────┐     ┌─────────────────┐   │
│  │  User   │────▶│   Express   │────▶│  Search Engine  │   │
│  │ "iphon" │     │   /search   │     │  Elasticsearch  │   │
│  └─────────┘     └─────────────┘     └────────┬────────┘   │
│                                               │             │
│                                               ▼             │
│                                    ┌─────────────────────┐ │
│                                    │  Fuzzy Matching     │ │
│                                    │  "iphon" → "iPhone" │ │
│                                    │                     │ │
│                                    │  Results:           │ │
│                                    │  • iPhone 15 Pro    │ │
│                                    │  • iPhone 14        │ │
│                                    │  • iPhone Case      │ │
│                                    └─────────────────────┘ │
│                                                             │
│  SEARCH FEATURES:                                           │
│  ─────────────────────────────────────────────────────────  │
│  ✓ Full-text search      ✓ Fuzzy matching (typo tolerance) │
│  ✓ Relevance scoring     ✓ Highlighting matched terms      │
│  ✓ Faceted search        ✓ Autocomplete/suggestions        │
│  ✓ Synonyms              ✓ Boosting important fields       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Rate Limiting

Rate limiting controls how many requests a client can make within a time window, protecting your API from abuse, DDoS attacks, and ensuring fair usage across all consumers.

const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redis = new Redis(); // Basic rate limiter const basicLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window message: { error: 'Too many requests', retryAfter: '15 minutes' }, standardHeaders: true, // Return rate limit info in headers legacyHeaders: false }); // Redis-backed limiter (for distributed systems) const redisLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }), windowMs: 60 * 1000, // 1 minute max: 60, // 60 requests per minute keyGenerator: (req) => { // Rate limit by API key or IP return req.headers['x-api-key'] || req.ip; } }); // Different limits for different endpoints const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 login attempts per hour message: { error: 'Too many login attempts, try again later' }, skipSuccessfulRequests: true // Only count failed attempts }); const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 100, skip: (req) => { // Skip rate limiting for premium users return req.user?.plan === 'premium'; } }); // Apply limiters app.use('/api/', redisLimiter); app.use('/auth/login', authLimiter); // Tiered rate limiting based on plan const tierLimiter = (req, res, next) => { const limits = { free: { windowMs: 60000, max: 10 }, basic: { windowMs: 60000, max: 100 }, premium: { windowMs: 60000, max: 1000 } }; const plan = req.user?.plan || 'free'; const limiter = rateLimit(limits[plan]); return limiter(req, res, next); }; app.use('/api/', tierLimiter);
┌─────────────────────────────────────────────────────────────┐
│                    RATE LIMITING                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Request Flow:                                              │
│  ┌─────────┐                        ┌─────────┐            │
│  │ Client  │───Request #1──────────▶│ Server  │  ✓ 200 OK  │
│  │         │───Request #2──────────▶│         │  ✓ 200 OK  │
│  │         │───Request #3──────────▶│         │  ✓ 200 OK  │
│  │         │         ...            │         │            │
│  │         │───Request #101────────▶│         │  ✗ 429     │
│  └─────────┘                        └─────────┘            │
│                                                             │
│  Response Headers:                                          │
│  ─────────────────────────────────────────────────────────  │
│  X-RateLimit-Limit: 100         (max requests)             │
│  X-RateLimit-Remaining: 45      (requests left)            │
│  X-RateLimit-Reset: 1699234567  (window reset timestamp)   │
│  Retry-After: 60                (seconds until retry)      │
│                                                             │
│  TIERED LIMITS:                                             │
│  ┌────────────┬─────────────┬─────────────────────────┐    │
│  │    Plan    │  Requests   │       Window            │    │
│  ├────────────┼─────────────┼─────────────────────────┤    │
│  │   Free     │     10      │      per minute         │    │
│  │   Basic    │    100      │      per minute         │    │
│  │   Premium  │   1000      │      per minute         │    │
│  └────────────┴─────────────┴─────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

API Documentation (Swagger/OpenAPI)

Swagger/OpenAPI provides a standardized way to document REST APIs, enabling automatic generation of interactive documentation, client SDKs, and server stubs from a single specification file.

const swaggerJsdoc = require('swagger-jsdoc'); const swaggerUi = require('swagger-ui-express'); // Swagger configuration const swaggerOptions = { definition: { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0', description: 'A sample API documentation', contact: { name: 'API Support', email: 'support@example.com' } }, 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'] // Files with JSDoc annotations }; const swaggerSpec = swaggerJsdoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.get('/api-docs.json', (req, res) => res.json(swaggerSpec)); // Route documentation with JSDoc annotations /** * @swagger * components: * schemas: * User: * type: object * required: * - name * - email * properties: * id: * type: string * description: Auto-generated ID * name: * type: string * description: User's full name * email: * type: string * format: email * description: User's email address * example: * id: "123" * name: "John Doe" * email: "john@example.com" */ /** * @swagger * /api/users: * get: * summary: Get all users * tags: [Users] * security: * - bearerAuth: [] * 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: List of users * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/User' * 401: * description: Unauthorized */ app.get('/api/users', authenticateJWT, getUsers); /** * @swagger * /api/users/{id}: * get: * summary: Get user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: User ID * responses: * 200: * description: User data * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ app.get('/api/users/:id', getUser);
┌─────────────────────────────────────────────────────────────┐
│                    SWAGGER UI                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  My API v1.0.0                      [Authorize 🔐]   │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │                                                      │   │
│  │  Users                                          ▼    │   │
│  │  ├── GET    /api/users         Get all users        │   │
│  │  ├── POST   /api/users         Create user          │   │
│  │  ├── GET    /api/users/{id}    Get user by ID       │   │
│  │  ├── PUT    /api/users/{id}    Update user          │   │
│  │  └── DELETE /api/users/{id}    Delete user          │   │
│  │                                                      │   │
│  │  Products                                       ▼    │   │
│  │  ├── GET    /api/products      Get all products     │   │
│  │  └── ...                                            │   │
│  │                                                      │   │
│  │  ┌───────────────────────────────────────────────┐  │   │
│  │  │ GET /api/users                                │  │   │
│  │  │ ─────────────────────────────────────────     │  │   │
│  │  │ Parameters:                                   │  │   │
│  │  │   page: [1    ]  limit: [20   ]               │  │   │
│  │  │                                               │  │   │
│  │  │ [Try it out]  [Execute]                       │  │   │
│  │  └───────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Access at: http://localhost:3000/api-docs                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

GraphQL with Express

GraphQL is a query language for APIs that allows clients to request exactly the data they need, implemented in Express using Apollo Server or express-graphql to provide a flexible alternative to REST.

const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); // Type definitions const typeDefs = ` type User { id: ID! name: String! email: String! posts: [Post!]! } type Post { id: ID! title: String! content: String! author: User! createdAt: String! } type Query { users: [User!]! user(id: ID!): User posts(limit: Int, offset: Int): [Post!]! post(id: ID!): Post } type Mutation { createUser(name: String!, email: String!): User! createPost(title: String!, content: String!, authorId: ID!): Post! updatePost(id: ID!, title: String, content: String): Post deletePost(id: ID!): Boolean! } `; // Resolvers const resolvers = { Query: { users: () => User.find(), user: (_, { id }) => User.findById(id), posts: (_, { limit = 10, offset = 0 }) => Post.find().skip(offset).limit(limit), post: (_, { id }) => Post.findById(id) }, Mutation: { createUser: (_, args) => User.create(args), createPost: (_, args) => Post.create(args), updatePost: (_, { id, ...updates }) => Post.findByIdAndUpdate(id, updates, { new: true }), deletePost: async (_, { id }) => { await Post.findByIdAndDelete(id); return true; } }, // Field resolvers User: { posts: (user) => Post.find({ authorId: user.id }) }, Post: { author: (post) => User.findById(post.authorId) } }; // Create Apollo Server const server = new ApolloServer({ typeDefs, resolvers }); async function startServer() { await server.start(); app.use('/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => ({ user: req.user, // Pass authenticated user to resolvers db: { User, Post } }) }) ); } startServer(); // Example queries: // query { users { id name posts { title } } } // mutation { createUser(name: "John", email: "john@test.com") { id } }
┌─────────────────────────────────────────────────────────────┐
│                  REST vs GRAPHQL                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  REST (Multiple requests):       GraphQL (Single request):  │
│                                                             │
│  GET /users/1                    query {                    │
│       ↓                            user(id: "1") {          │
│  { id, name, email }                 name                   │
│                                      email                  │
│  GET /users/1/posts                  posts {                │
│       ↓                                title                │
│  [{ id, title }, ...]                  comments {           │
│                                          text              │
│  GET /posts/1/comments                 }                    │
│       ↓                              }                      │
│  [{ id, text }, ...]               }                        │
│                                  }                          │
│  ───────────────────────────────────────────────────────── │
│  3 requests, over-fetching      1 request, exact data       │
│                                                             │
│  GRAPHQL BENEFITS:                                          │
│  ✓ Client specifies exact data needed                      │
│  ✓ Single endpoint                                         │
│  ✓ Strongly typed schema                                   │
│  ✓ No over/under fetching                                  │
│  ✓ Built-in introspection                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CORS Handling

CORS (Cross-Origin Resource Sharing) is a security mechanism that controls which domains can access your API, requiring proper configuration to allow legitimate cross-origin requests from browsers while blocking unauthorized access.

const cors = require('cors'); // Simple CORS - Allow all origins (development only!) app.use(cors()); // Production CORS configuration const corsOptions = { origin: (origin, callback) => { const allowedOrigins = [ 'https://myapp.com', 'https://www.myapp.com', 'https://admin.myapp.com' ]; // Allow requests with no origin (mobile apps, Postman) if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], exposedHeaders: ['X-Total-Count', 'X-Page-Count'], credentials: true, // Allow cookies maxAge: 86400 // Cache preflight for 24 hours }; app.use(cors(corsOptions)); // Per-route CORS app.get('/api/public', cors(), (req, res) => { res.json({ data: 'Public data' }); }); app.get('/api/private', cors(corsOptions), authenticateJWT, (req, res) => { res.json({ data: 'Private data' }); }); // Manual CORS handling app.use((req, res, next) => { const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Allow-Credentials', 'true'); // Handle preflight if (req.method === 'OPTIONS') { return res.status(204).send(); } next(); }); // Environment-based configuration const corsConfig = { development: { origin: true // Allow all origins }, production: { origin: ['https://myapp.com'], credentials: true } }; app.use(cors(corsConfig[process.env.NODE_ENV] || corsConfig.development));
┌─────────────────────────────────────────────────────────────┐ │ CORS FLOW │ ├─────────────────────────────────────────────────────────────┤ │ │ │ SIMPLE REQUEST: │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Browser │ GET /api/data │ Server │ │ │ │ myapp.com │───────────────────▶│ api.com │ │ │ │ │ │ │ │ │ │ │◀───────────────────│ │ │ │ │ │ Access-Control- │ │ │ │ │ │ Allow-Origin: │ │ │ │ │ │ https://myapp.com │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ PREFLIGHT REQUEST (POST, PUT, custom headers): │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Browser │ OPTIONS /api/data │ Server │ │ │ │ │───────────────────▶│ │ │ │ │ │◀───────────────────│ │ │ │ │ │ 204 + CORS headers│ │ │ │ │ │ │ │ │ │ │ │ POST /api/data │ │ │ │ │ │───────────────────▶│ │ │ │ │ │◀───────────────────│ │ │ │ │ │ 200 OK + data │ │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ CORS HEADERS: │ │ ───────────────────────────────────────────────────────── │ │ Access-Control-Allow-Origin: https://myapp.com │ │ Access-Control-Allow-Methods: GET, POST, PUT, DELETE │ │ Access-Control-Allow-Headers: Content-Type, Authorization │ │ Access-Control-Allow-Credentials: true │ │ Access-Control-Max-Age: 86400 │ │ │ └─────────────────────────────────────────────────────────────┘