TECHNICAL PRESENTATION

Introduction to
REST API Design

Principles, Patterns & Production-Ready APIs
HTTP methods · resources · status codes · pagination · versioning · HATEOAS
02

Agenda

Foundations

  • What is REST & Fielding's constraints
  • HTTP methods & semantics
  • URL design & resource naming
  • Request & response bodies

Status & Errors

  • HTTP status codes in depth
  • Error response format (RFC 7807)
  • Pagination strategies
  • Filtering, sorting & field selection

Advanced Patterns

  • Versioning strategies
  • Authentication & API keys
  • Rate limiting & throttling
  • HATEOAS & hypermedia

Production

  • Caching & conditional requests
  • API documentation & OpenAPI
  • Express implementation
  • Common anti-patterns
03

What Is REST?

REpresentational State Transfer was defined by Roy Fielding in his 2000 doctoral dissertation. It is an architectural style, not a protocol or specification. REST describes how distributed hypermedia systems should behave when designed well.

A REST API exposes resources (nouns) at stable URIs and lets clients manipulate them using standard HTTP methods (verbs). The server transfers representations of resource state — typically JSON.

Fielding's 6 Constraints

  • Client–Server — separation of concerns
  • Stateless — each request carries all context
  • Cacheable — responses declare cacheability
  • Uniform Interface — resource identification, self-descriptive messages, HATEOAS
  • Layered System — intermediaries (proxies, gateways) are transparent
  • Code-on-Demand (optional) — server can extend client with executable code

Resource-Oriented Architecture

  • Everything is a resource with a unique URI
  • Resources have representations (JSON, XML, HTML)
  • State transitions via hypermedia links
  • Standard methods operate on all resources uniformly

REST vs RPC vs GraphQL

  • REST — resource-centric, HTTP-native, cacheable
  • RPC — action-centric (POST /getUser), tight coupling
  • GraphQL — query language, single endpoint, client-driven schema

REST remains the dominant style for public APIs because of simplicity, cacheability, and tooling maturity.

04

HTTP Methods

REST maps CRUD operations to HTTP verbs. Understanding safety (no side effects) and idempotency (same result if repeated) is critical for correct API design.

MethodSemanticsSafeIdempotentRequest BodyExample
GETRead a resourceYesYesNoGET /users/42
POSTCreate a resourceNoNoYesPOST /users
PUTReplace a resource entirelyNoYesYesPUT /users/42
PATCHPartial updateNoNo*YesPATCH /users/42
DELETERemove a resourceNoYesRareDELETE /users/42

Safe Methods

GET and HEAD never modify server state. Clients and intermediaries can retry, cache, and prefetch them freely.

Idempotent Methods

PUT and DELETE produce the same result no matter how many times you call them. This is essential for retry logic and fault tolerance.

PATCH Caveat

PATCH is not inherently idempotent. {"op":"increment","path":"/views"} changes state on each call. JSON Merge Patch is idempotent; JSON Patch may not be.

05

URL Design & Resource Naming

Golden Rules

  • Use nouns, not verbs — /users not /getUsers
  • Use plurals/orders not /order
  • Use kebab-case/line-items not /lineItems
  • Hierarchical nesting for ownership — /users/42/orders
  • Limit nesting to 2 levels max
  • Use query params for filtering, not path segments
# Good URL patterns
GET    /api/v1/users              # list users
GET    /api/v1/users/42           # single user
GET    /api/v1/users/42/orders    # user's orders
POST   /api/v1/users              # create user
DELETE /api/v1/users/42           # delete user

# Bad URL patterns
GET    /api/v1/getUser?id=42      # verb in URL
POST   /api/v1/user/create        # verb + singular
GET    /api/v1/users/42/orders/7/items/3/tags  # too deep

Query Parameters

  • Filtering: ?status=active&role=admin
  • Sorting: ?sort=-created_at,name
  • Pagination: ?page=2&per_page=25
  • Fields: ?fields=id,name,email
  • Search: ?q=john+doe

Sub-Resources vs Top-Level

If a resource can exist independently, give it its own top-level endpoint. Use nesting only for true ownership relationships.

# Order belongs to user — nested OK
GET /users/42/orders

# Order can be accessed directly too
GET /orders/789

# Tag is independent — don't nest 3 levels
GET /tags/5  (not /users/42/orders/7/tags/5)
06

Request & Response Bodies

JSON Structure Conventions

  • Use camelCase for property names
  • Use ISO 8601 for dates: "2025-03-15T10:30:00Z"
  • Use null for absent values, don't omit keys
  • Wrap collections in a named key, not bare arrays
// POST /api/v1/users — request body
{
  "firstName": "Alice",
  "lastName": "Chen",
  "email": "alice@example.com",
  "role": "editor"
}
// Response — single resource
{
  "data": {
    "id": 42,
    "firstName": "Alice",
    "lastName": "Chen",
    "email": "alice@example.com",
    "role": "editor",
    "createdAt": "2025-03-15T10:30:00Z"
  }
}

Envelope Pattern

Wrap responses in a consistent envelope with data, meta, and errors keys. This makes parsing predictable for every client.

// Response — collection with metadata
{
  "data": [
    { "id": 1, "name": "Widget A" },
    { "id": 2, "name": "Widget B" }
  ],
  "meta": {
    "totalCount": 87,
    "page": 1,
    "perPage": 25,
    "totalPages": 4
  },
  "links": {
    "self":  "/api/v1/products?page=1",
    "next":  "/api/v1/products?page=2",
    "last":  "/api/v1/products?page=4"
  }
}

Content Negotiation

Clients send Accept: application/json. Servers respond with Content-Type: application/json. Support Accept header for multiple formats. Return 406 Not Acceptable if the format is unsupported.

07

HTTP Status Codes

Use the most specific status code that applies. Clients rely on these for control flow, retry logic, and error handling.

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (include Location header)
204No ContentSuccessful DELETE (no body)
301Moved PermanentlyResource URL changed forever
304Not ModifiedConditional GET, cache still valid
400Bad RequestMalformed syntax, invalid body
401UnauthorizedMissing or invalid credentials
403ForbiddenAuthenticated but not authorised
404Not FoundResource does not exist
409ConflictDuplicate, version conflict
422Unprocessable EntityValidation errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server failure
503Service UnavailableMaintenance, overloaded

2xx — Success

The request was received, understood, and accepted. Always return the appropriate 2xx code. 200 is not a catch-all.

4xx — Client Error

The client sent something wrong. Include a descriptive error body. Never return 200 with an error in the body.

5xx — Server Error

Something broke on the server. Log the full error internally but expose only a safe message to clients. Include a Retry-After header for 503.

Anti-Pattern: 200 Everything

Returning 200 with {"success": false} defeats HTTP semantics. Proxies, caches, and monitoring tools all rely on status codes for correct behaviour.

08

Error Response Format

RFC 7807 — Problem Details

A standard format for HTTP API error responses. Provides machine-readable error details with a consistent structure.

// 422 Unprocessable Entity
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation Error",
  "status": 422,
  "detail": "Request body contains invalid fields",
  "instance": "/api/v1/users",
  "errors": [
    {
      "field": "email",
      "message": "must be a valid email address",
      "value": "not-an-email"
    },
    {
      "field": "age",
      "message": "must be at least 18",
      "value": 12
    }
  ]
}

Required Fields

  • type — URI identifying the error type
  • title — short human-readable summary
  • status — HTTP status code (repeated for convenience)
  • detail — human-readable explanation specific to this occurrence
  • instance — URI reference for the specific occurrence
// Express error handler
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    type: `https://api.example.com/errors/${err.code}`,
    title: err.title || 'Internal Server Error',
    status,
    detail: status === 500
      ? 'An unexpected error occurred'
      : err.message,
    instance: req.originalUrl,
    ...(err.errors && { errors: err.errors })
  });
});

Content-Type

RFC 7807 specifies application/problem+json as the media type. Many APIs use plain application/json with the same structure.

09

Pagination

Never return unbounded collections. Choose a pagination strategy based on data characteristics and client needs.

StrategyParamsProsCons
Offset?page=3&per_page=25Simple, jump to any pageSlow on large tables (OFFSET 100000), drift on inserts
Cursor?cursor=eyJpZCI6NDJ9&limit=25Stable, no drift, fastNo random page access, opaque tokens
Keyset?after_id=42&limit=25Fast (index seek), transparentRequires sortable unique key, forward-only

Link Headers (RFC 8288)

HTTP/1.1 200 OK
Link: </api/v1/users?cursor=abc&limit=25>; rel="next",
      </api/v1/users?cursor=xyz&limit=25>; rel="prev"
X-Total-Count: 1482

Link headers keep pagination metadata out of the body. GitHub's API uses this pattern extensively.

Response Body Pagination

{
  "data": [ ... ],
  "meta": {
    "totalCount": 1482,
    "nextCursor": "eyJpZCI6NjcsImNyZWF0ZWQiOiIyMDI1LTAzIn0=",
    "hasMore": true
  }
}

Embed pagination metadata in the response body when Link headers are insufficient for your client.

10

Filtering, Sorting & Field Selection

Filtering

Use query parameters for simple equality. Use operator suffixes or bracket notation for richer queries.

# Simple equality
GET /api/v1/products?category=electronics&in_stock=true

# Operator suffixes
GET /api/v1/products?price_gte=10&price_lte=100

# Bracket notation (JSON:API style)
GET /api/v1/products?filter[price][gte]=10&filter[price][lte]=100

# Search
GET /api/v1/products?q=wireless+headphones

Field Selection (Sparse Fieldsets)

Let clients request only the fields they need, reducing payload size and bandwidth.

# Only return id, name, and price
GET /api/v1/products?fields=id,name,price

# JSON:API style
GET /api/v1/products?fields[products]=id,name,price

Sorting

Use a sort parameter with comma-separated fields. Prefix with - for descending order.

# Sort by created_at descending, then name ascending
GET /api/v1/products?sort=-created_at,name

# Explicit direction style
GET /api/v1/products?sort=created_at:desc,name:asc

Implementation Tips

  • Whitelist sortable and filterable fields
  • Return 400 for unknown field names
  • Set default sort and max page size
  • Index database columns used in filters
  • Document supported operators per field
// Express query parser
const allowedSort = ['name', 'price', 'created_at'];
const sortField = allowedSort.includes(req.query.sort)
  ? req.query.sort : 'created_at';
const order = req.query.order === 'desc' ? 'DESC' : 'ASC';
const results = await db.query(
  `SELECT * FROM products ORDER BY ${sortField} ${order}`
);
11

Versioning Strategies

APIs evolve. Breaking changes need a versioning strategy so existing clients keep working while new clients get new features.

StrategyExampleProsCons
URL Path/api/v1/usersVisible, simple routing, cacheableViolates REST (URI should identify resource, not version)
Custom HeaderAPI-Version: 2Clean URLs, explicitHidden, harder to test in browser, not cacheable by URL
Accept HeaderAccept: application/vnd.api.v2+jsonMost RESTful (content negotiation)Complex, unfamiliar to many developers
Query Param/api/users?version=2Easy to add, easy to testOptional params can be forgotten, cache issues

URL Path (Most Common)

const express = require('express');
const app = express();

// Version 1
const v1Router = express.Router();
v1Router.get('/users', v1.listUsers);

// Version 2
const v2Router = express.Router();
v2Router.get('/users', v2.listUsers);

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

When to Version

  • Breaking: removing a field, changing a type, renaming a key
  • Non-breaking: adding a new optional field, adding a new endpoint
  • Version only on breaking changes
  • Support at most 2–3 versions simultaneously
  • Announce deprecation with Sunset header

Deprecation Headers

HTTP/1.1 200 OK
Sunset: Sat, 01 Nov 2025 00:00:00 GMT
Deprecation: true
Link: </api/v2/users>; rel="successor-version"

The Sunset header (RFC 8594) tells clients when an endpoint will be removed.

12

Authentication & API Keys

Authentication Methods

MethodBest For
API KeyServer-to-server, simple integrations
Bearer Token (JWT)User-facing APIs, stateless auth
OAuth 2.0Third-party access, scoped permissions
Basic AuthInternal tools (over HTTPS only)
# API Key (header)
GET /api/v1/data
X-API-Key: sk_live_abc123def456

# Bearer Token (JWT)
GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# Basic Auth
GET /api/v1/admin
Authorization: Basic YWRtaW46cGFzc3dvcmQ=
// JWT middleware in Express
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer ')) {
    return res.status(401).json({
      type: '/errors/unauthorized',
      title: 'Missing or invalid token',
      status: 401
    });
  }

  try {
    const token = header.slice(7);
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({
      type: '/errors/unauthorized',
      title: 'Token expired or invalid',
      status: 401
    });
  }
}

OAuth 2.0 Scopes

Scopes limit what a token can do: read:users, write:orders, admin. Return 403 Forbidden when a valid token lacks the required scope.

13

Rate Limiting & Throttling

Rate limiting protects your API from abuse, ensures fair usage, and prevents cascading failures. Every production API needs it.

AlgorithmHow It WorksTrade-off
Fixed WindowN requests per time windowSimple, but burst at window edges
Sliding WindowRolling window of weighted countsSmooth, slight complexity
Token BucketTokens refill at steady rate, burst OKAllows bursts, widely used
Leaky BucketRequests drain at fixed rateSmoothest output, queues requests

Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1711036800

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
// Express rate limiting with express-rate-limit
const rateLimit = require('express-rate-limit');

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,      // RateLimit-* headers
  legacyHeaders: false,
  message: {
    type: '/errors/rate-limit',
    title: 'Too Many Requests',
    status: 429,
    detail: 'Rate limit exceeded. Try again later.'
  }
});

app.use('/api/', apiLimiter);

// Tiered limits per plan
const premiumLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5000,
  keyGenerator: (req) => req.user?.apiKey
});

Key Strategies

  • Rate limit by API key, IP address, or user ID
  • Different tiers for free vs paid plans
  • Use Redis for distributed rate limiting across multiple servers
14

HATEOAS & Hypermedia

HATEOAS (Hypermedia As The Engine Of Application State) means the API response includes links that tell the client what actions are available next. The client never hard-codes URLs.

// GET /api/v1/orders/789
{
  "data": {
    "id": 789,
    "status": "pending",
    "total": 59.99
  },
  "links": {
    "self":    { "href": "/api/v1/orders/789" },
    "cancel":  { "href": "/api/v1/orders/789/cancel", "method": "POST" },
    "pay":     { "href": "/api/v1/orders/789/pay", "method": "POST" },
    "customer": { "href": "/api/v1/users/42" }
  }
}
// After payment — cancel link disappears
{
  "data": { "id": 789, "status": "paid", "total": 59.99 },
  "links": {
    "self":   { "href": "/api/v1/orders/789" },
    "refund": { "href": "/api/v1/orders/789/refund", "method": "POST" },
    "receipt": { "href": "/api/v1/orders/789/receipt" }
  }
}

Richardson Maturity Model

  • Level 0 — Single URI, single verb (SOAP-style RPC)
  • Level 1 — Multiple URIs (resources), but only POST
  • Level 2 — Multiple URIs + correct HTTP verbs (most APIs today)
  • Level 3 — Hypermedia controls (HATEOAS) — true REST

Benefits

  • Clients discover capabilities at runtime
  • Server can evolve URLs without breaking clients
  • Self-documenting API responses
  • State machine transitions are explicit

Practical Reality

Most public APIs implement Level 2 only. Full HATEOAS adds complexity and many clients ignore links. GitHub, PayPal, and Spring Data REST are notable exceptions that implement HATEOAS.

15

Caching

Cache-Control Header

# Public resource — cacheable for 1 hour
Cache-Control: public, max-age=3600

# Private user data — only browser can cache
Cache-Control: private, max-age=600

# Never cache (auth endpoints, real-time data)
Cache-Control: no-store

# Cache but revalidate every time
Cache-Control: no-cache

ETags & Conditional Requests

# First response
HTTP/1.1 200 OK
ETag: "a1b2c3d4"

# Subsequent request — conditional
GET /api/v1/products/42
If-None-Match: "a1b2c3d4"

# Server responds — not changed
HTTP/1.1 304 Not Modified

Saves bandwidth — the server only sends the full body if the resource actually changed.

Last-Modified / If-Modified-Since

# First response
HTTP/1.1 200 OK
Last-Modified: Thu, 15 Mar 2025 10:30:00 GMT

# Conditional request
GET /api/v1/products/42
If-Modified-Since: Thu, 15 Mar 2025 10:30:00 GMT

# 304 if unchanged, 200 with body if changed
// Express ETag and caching middleware
app.get('/api/v1/products/:id', async (req, res) => {
  const product = await db.findById(req.params.id);
  const etag = crypto
    .createHash('md5')
    .update(JSON.stringify(product))
    .digest('hex');

  res.set({
    'ETag': `"${etag}"`,
    'Cache-Control': 'public, max-age=300',
    'Vary': 'Accept, Authorization'
  });

  if (req.headers['if-none-match'] === `"${etag}"`) {
    return res.status(304).end();
  }
  res.json({ data: product });
});

CDN Patterns

Put a CDN (CloudFront, Fastly) in front of GET endpoints. Use Vary header to cache different representations. Invalidate on write operations.

16

API Documentation

OpenAPI / Swagger

The OpenAPI Specification (OAS) is the industry standard for describing REST APIs. Write it in YAML or JSON and generate docs, SDKs, and mock servers automatically.

openapi: 3.0.3
info:
  title: Products API
  version: 1.0.0
paths:
  /api/v1/products:
    get:
      summary: List products
      parameters:
        - name: category
          in: query
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: A list of products
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'

Documentation Tools

  • Swagger UI — interactive API explorer from OAS spec
  • Redoc — beautiful three-panel docs from OAS
  • Postman — collections, examples, auto-generated docs
  • Stoplight — visual API design and documentation
// Auto-generate Swagger in Express
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const specs = swaggerJsdoc({
  definition: {
    openapi: '3.0.3',
    info: { title: 'My API', version: '1.0.0' }
  },
  apis: ['./routes/*.js']  // JSDoc annotations
});

app.use('/docs', swaggerUi.serve,
  swaggerUi.setup(specs));

Documentation Best Practices

  • Include request and response examples for every endpoint
  • Document all error codes and their meanings
  • Provide a quick-start guide with curl commands
  • Keep docs versioned alongside code
  • Add authentication instructions prominently
17

Express Implementation

A complete REST API skeleton with Express — routes, controllers, validation, and error handling following every pattern discussed.

// server.js — app setup
const express = require('express');
const app = express();

app.use(express.json());
app.use(cors());

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

// 404 handler
app.use((req, res) => {
  res.status(404).json({
    type: '/errors/not-found',
    title: 'Not Found',
    status: 404,
    detail: `No route matches ${req.method} ${req.path}`
  });
});

// Global error handler
app.use((err, req, res, next) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    type: `/errors/${err.code || 'server-error'}`,
    title: err.title || 'Internal Server Error',
    status,
    detail: status === 500 ? 'Something went wrong' : err.message,
    ...(err.errors && { errors: err.errors })
  });
});

app.listen(3000);
// routes/users.js — full CRUD
const router = require('express').Router();
const { body, validationResult } = require('express-validator');

// List users (paginated, filtered, sorted)
router.get('/', async (req, res, next) => {
  try {
    const { page = 1, per_page = 25, sort = '-created_at' } = req.query;
    const { data, total } = await User.findAll({
      page, perPage: per_page, sort
    });
    res.json({
      data,
      meta: { totalCount: total, page: +page, perPage: +per_page }
    });
  } catch (err) { next(err); }
});

// Create user (with validation)
router.post('/',
  body('email').isEmail(),
  body('firstName').trim().notEmpty(),
  async (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({
        type: '/errors/validation',
        title: 'Validation Error',
        status: 422,
        errors: errors.array()
      });
    }
    try {
      const user = await User.create(req.body);
      res.status(201)
        .location(`/api/v1/users/${user.id}`)
        .json({ data: user });
    } catch (err) { next(err); }
  }
);
18

Common Anti-Patterns

Chatty APIs

Requiring many round-trips to accomplish a single task. Loading a dashboard that needs 15 API calls is a sign of poor resource design.

# Bad — 4 requests to load a user profile
GET /users/42
GET /users/42/address
GET /users/42/preferences
GET /users/42/avatar

# Better — compound document or expand param
GET /users/42?expand=address,preferences

Over-Fetching & Under-Fetching

  • Over-fetching: returning 50 fields when the client needs 3. Use sparse fieldsets: ?fields=id,name
  • Under-fetching: not enough data per response, forcing multiple calls. Provide expand or include parameters.

Verbs in URLs

# Anti-pattern
POST /api/createUser
POST /api/deleteUser?id=42
GET  /api/getUserById?id=42

# Correct — let HTTP methods be the verbs
POST   /api/v1/users
DELETE /api/v1/users/42
GET    /api/v1/users/42

Ignoring Idempotency

If PUT /orders/42 creates a new order on each call instead of replacing, clients can't safely retry on network failures. This leads to duplicate records and inconsistent state.

// Anti-pattern: PUT creates duplicates
app.put('/orders/:id', async (req, res) => {
  await Order.create(req.body); // WRONG — always creates
});

// Correct: PUT is idempotent replace
app.put('/orders/:id', async (req, res) => {
  await Order.upsert(req.params.id, req.body);
});

More Anti-Patterns

  • 200 for everything — hiding errors behind {"error": true}
  • Inconsistent naming — mixing camelCase, snake_case, plural/singular
  • No pagination — returning 100k records in one response
  • Exposing internals — database IDs, SQL errors, stack traces
  • Breaking changes without versioning — removing or renaming fields
  • No rate limiting — one rogue client can take down the whole API
19

Summary & Next Steps

Key Takeaways

  • REST is an architectural style, not a spec
  • Use nouns for URLs, HTTP verbs for actions
  • Return correct status codes always
  • Consistent error format (RFC 7807)
  • Paginate, filter, and sort collections
  • Version your API from day one

Checklist

  • Authentication & authorization
  • Rate limiting on all endpoints
  • Input validation & sanitisation
  • Caching with ETags & Cache-Control
  • Comprehensive error handling
  • OpenAPI documentation
  • Monitoring, logging, alerting

Recommended Reading

  • Fielding, REST Dissertation (2000)
  • Richardson & Ruby, RESTful Web APIs
  • RFC 7231 — HTTP Semantics
  • RFC 7807 — Problem Details
  • RFC 8288 — Web Linking
  • JSON:API Specification
  • Microsoft REST API Guidelines
  • Google API Design Guide

Next Steps

Build a complete REST API with Express. Start with 3–4 resources, implement proper status codes, validation, pagination, and error handling. Add OpenAPI docs. Deploy behind a reverse proxy with rate limiting and caching. Then explore GraphQL, gRPC, and event-driven architectures to understand when REST is — and isn't — the right choice.