Skip to content
Published on

REST API Design Best Practices 2025: Naming, Versioning, Error Handling, Pagination, Security

Authors

Introduction

REST APIs are the foundation of modern web services. Almost every piece of software -- mobile apps, SPAs, microservices, IoT devices -- communicates through REST APIs. Yet a surprising number of APIs claiming to be "RESTful" actually violate REST principles.

Well-designed APIs improve developer experience (DX), simplify maintenance, and ensure scalability and security. Poorly designed APIs increase communication costs between teams, create security vulnerabilities, and accumulate technical debt.

This guide covers all REST API design best practices, from resource naming to versioning, error handling, authentication, and OpenAPI.


1. REST Principles

1.1 The 6 REST Constraints

1. Client-Server (Separation of Concerns)
   - Separate UI and data storage concerns
   - Enable independent evolution

2. Stateless
   - Each request contains complete information
   - Server does not store client state
   - Improves scalability

3. Cacheable
   - Responses must declare cacheability
   - Use Cache-Control, ETag, Last-Modified headers

4. Uniform Interface
   - Resource identification (URI)
   - Resource manipulation through representations
   - Self-descriptive messages
   - HATEOAS

5. Layered System
   - Client cannot tell if communicating directly with server or intermediary
   - Load balancers, caches, gateways can be added

6. Code-on-Demand (Optional)
   - Server can send executable code to client
   - The only optional constraint

1.2 Richardson Maturity Model

Level 3: Hypermedia Controls (HATEOAS)
  --> Responses include links to related resources
Level 2: HTTP Methods
  --> Correct use of GET, POST, PUT, DELETE
Level 1: Resources
  --> Individual URIs for resources
Level 0: The Swamp of POX
  --> Single endpoint, all operations via POST

Most APIs target Level 2.
Level 3 (HATEOAS) is ideal but optional in practice.

2. Resource Naming

2.1 Basic Rules

Good:
GET    /users               # List users
GET    /users/123            # Get specific user
POST   /users               # Create user
PUT    /users/123            # Full update user
PATCH  /users/123            # Partial update user
DELETE /users/123            # Delete user

Bad:
GET    /getUsers             # No verbs
GET    /user/123             # Use plural
POST   /createUser           # No verbs
POST   /user/123/delete      # Use HTTP methods

2.2 Hierarchical Relationships

# User's orders list
GET /users/123/orders

# User's specific order
GET /users/123/orders/456

# Order's items list
GET /users/123/orders/456/items

# Warning: Avoid nesting beyond 3 levels
# Bad:
GET /users/123/orders/456/items/789/reviews

# Good (alternatives):
GET /items/789/reviews
GET /reviews?item_id=789

2.3 Naming Conventions

# Use lowercase + hyphens (kebab-case)
Good: /user-profiles
Bad:  /userProfiles, /user_profiles, /UserProfiles

# No file extensions
Good: Accept: application/json
Bad:  /users/123.json

# No trailing slashes
Good: /users
Bad:  /users/

# Use query parameters for filtering
GET /users?status=active&role=admin
GET /products?category=electronics&min_price=100&max_price=500

# Sorting
GET /users?sort=created_at&order=desc
GET /users?sort=-created_at,+name  # - descending, + ascending

# Field Selection (Sparse Fieldsets)
GET /users/123?fields=name,email,avatar

2.4 Handling Non-Resource Operations

# Actions: express as sub-resources
POST /users/123/activate          # Activate user
POST /users/123/deactivate        # Deactivate user
POST /orders/456/cancel           # Cancel order
POST /payments/789/refund         # Refund payment

# Search (verb-like action, not a resource)
GET /search?q=keyword&type=users

# Batch operations
POST /users/batch
Body: { "ids": [1, 2, 3], "action": "deactivate" }

# Aggregations
GET /orders/statistics
GET /dashboard/metrics

3. Correct HTTP Method Usage

3.1 Method Characteristics

MethodPurposeIdempotentSafeRequest BodyResponse Body
GETReadYesYesNoYes
POSTCreateNoNoYesYes
PUTFull replaceYesNoYesOptional
PATCHPartial updateNo*NoYesYes
DELETEDeleteYesNoOptionalOptional
HEADHeaders onlyYesYesNoNo
OPTIONSAvailable methodsYesYesNoYes

*PATCH can be idempotent depending on implementation

3.2 Idempotency in Detail

# PUT - Idempotent (same request multiple times = same result)
PUT /users/123
Body: { "name": "Alice", "email": "alice@example.com" }
# Sending once or 10 times yields same result

# DELETE - Idempotent
DELETE /users/123
# First time: 200 OK (deleted)
# Second time: 404 Not Found (already deleted) - no side effects

# POST - NOT idempotent (each request creates new resource)
POST /orders
Body: { "product_id": 1, "quantity": 2 }
# Sending twice creates 2 orders!

Making POST safe with idempotency keys:

POST /payments
Headers:
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Body: { "amount": 10000, "currency": "USD" }

# Resending with same Idempotency-Key prevents duplicate payments
# Server stores the key and returns previous response for same key

3.3 PUT vs PATCH

# Original data
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "role": "user",
  "avatar": "default.png"
}

# PUT: Full replacement (missing fields reset)
PUT /users/123
Body: { "name": "Alice Updated", "email": "alice@new.com" }
# Result: role and avatar become null/default!

# PATCH: Partial update (only sent fields change)
PATCH /users/123
Body: { "name": "Alice Updated" }
# Result: only name changes, other fields preserved

4. Status Codes

4.1 Key Status Codes

2xx Success:
200 OK                  - General success (GET, PUT, PATCH, DELETE)
201 Created             - Resource created (POST) + Location header
202 Accepted            - Async processing accepted
204 No Content          - Success with no response body (DELETE)

3xx Redirection:
301 Moved Permanently   - Permanent redirect
302 Found               - Temporary redirect
304 Not Modified        - Use cached version

4xx Client Errors:
400 Bad Request         - Invalid request (validation failure)
401 Unauthorized        - Authentication required (not logged in)
403 Forbidden           - Authorization failure (no permission)
404 Not Found           - Resource does not exist
405 Method Not Allowed  - Unsupported HTTP method
409 Conflict            - Resource conflict (duplicate creation)
422 Unprocessable Entity - Syntax ok but semantics wrong
429 Too Many Requests   - Rate limit exceeded

5xx Server Errors:
500 Internal Server Error - Server error
502 Bad Gateway          - Upstream server error
503 Service Unavailable  - Service temporarily down
504 Gateway Timeout      - Upstream server timeout

4.2 Status Code Selection Guide

POST resource creation success -> 201 Created
  Response Headers:
    Location: /users/124
  Response Body:
    { "id": 124, "name": "Bob", ... }

DELETE success:
  Option 1: 204 No Content (no body)
  Option 2: 200 OK + deleted resource info

Authentication vs Authorization:
  Not logged in -> 401 Unauthorized
  Logged in but no permission -> 403 Forbidden

Validation failures:
  JSON parse failure -> 400 Bad Request
  Field value invalid -> 422 Unprocessable Entity

Resource conflicts:
  Duplicate email -> 409 Conflict
  Optimistic lock failure -> 409 Conflict

5. Error Response Design

5.1 RFC 7807 Problem Details

{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/users/123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address"
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 0 and 150"
    }
  ],
  "timestamp": "2025-03-25T10:30:00Z",
  "trace_id": "abc-123-def-456"
}

5.2 Error Code System

Error code naming convention:

AUTH_001  - Authentication token expired
AUTH_002  - Invalid credentials
AUTH_003  - Account locked

USER_001  - User not found
USER_002  - Email already exists
USER_003  - Password policy violation

ORDER_001 - Insufficient stock
ORDER_002 - Payment failed
ORDER_003 - Order already cancelled

RATE_001  - API call limit exceeded
RATE_002  - Daily limit exceeded

5.3 Error Response Implementation

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

class APIError(Exception):
    def __init__(self, status, error_type, title, detail, errors=None):
        self.status = status
        self.error_type = error_type
        self.title = title
        self.detail = detail
        self.errors = errors or []

@app.errorhandler(APIError)
def handle_api_error(error):
    response = {
        "type": f"https://api.example.com/errors/{error.error_type}",
        "title": error.title,
        "status": error.status,
        "detail": error.detail,
    }
    if error.errors:
        response["errors"] = error.errors
    return jsonify(response), error.status

@app.errorhandler(404)
def not_found(e):
    return jsonify({
        "type": "https://api.example.com/errors/not-found",
        "title": "Resource Not Found",
        "status": 404,
        "detail": "The requested resource does not exist."
    }), 404

6. Pagination

6.1 Offset-based

GET /users?page=3&per_page=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total_items": 1543,
    "total_pages": 78,
    "has_next": true,
    "has_prev": true
  }
}

Pros: Simple implementation, direct page jump possible
Cons: Slow on large datasets (OFFSET 10000), duplicates/misses on data changes

The Offset Problem:

-- When page=500, per_page=20
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 9980;
-- DB reads 9980 rows and discards them, returns 20 -> very inefficient!
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==

Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTQzfQ==",
    "prev_cursor": "eyJpZCI6MTI0fQ==",
    "has_next": true,
    "has_prev": true,
    "limit": 20
  }
}

Cursor Implementation:

import base64
import json

def encode_cursor(last_item):
    """Encode sort key of last item as cursor"""
    cursor_data = {"id": last_item["id"], "created_at": last_item["created_at"]}
    return base64.urlsafe_b64encode(
        json.dumps(cursor_data).encode()
    ).decode()

def decode_cursor(cursor_str):
    """Decode cursor to extract sort key"""
    return json.loads(
        base64.urlsafe_b64decode(cursor_str.encode()).decode()
    )

def get_users(cursor=None, limit=20):
    query = "SELECT * FROM users"
    if cursor:
        decoded = decode_cursor(cursor)
        query += f" WHERE id > {decoded['id']}"
    query += f" ORDER BY id ASC LIMIT {limit + 1}"

    results = db.execute(query)
    has_next = len(results) > limit
    items = results[:limit]

    return {
        "data": items,
        "pagination": {
            "next_cursor": encode_cursor(items[-1]) if has_next else None,
            "has_next": has_next,
            "limit": limit
        }
    }

6.3 Keyset-based

-- Keyset Pagination is the core of Cursor
-- Composite sort key example

-- First page
SELECT * FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Next page (last item: created_at='2025-03-20', id=456)
SELECT * FROM orders
WHERE (created_at, id) < ('2025-03-20', 456)
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Very fast with proper index!
CREATE INDEX idx_orders_cursor ON orders(created_at DESC, id DESC);

6.4 Pagination Comparison

MethodPerformanceConsistencyDirect Page JumpComplexity
OffsetSlow on large dataIssues on data changesYesLow
CursorConsistent performanceMaintains consistencyNoMedium
KeysetVery fastMaintains consistencyNoMedium

7. Filtering, Sorting, Field Selection

7.1 Filtering

# Basic filtering
GET /products?category=electronics&status=available

# Range filters
GET /products?min_price=100&max_price=500
GET /orders?created_after=2025-01-01&created_before=2025-03-25

# Multiple value filter
GET /products?tags=phone,tablet,laptop

# Search
GET /products?q=wireless+keyboard

# Complex filters (advanced)
GET /products?filter[category]=electronics&filter[price][gte]=100&filter[price][lte]=500

7.2 Sorting

# Single field sorting
GET /users?sort=name          # ascending
GET /users?sort=-name         # descending

# Multiple field sorting
GET /users?sort=-created_at,name

# JSON:API style
GET /articles?sort=-published_at,title

7.3 Field Selection (Sparse Fieldsets)

# Request only needed fields (save bandwidth)
GET /users/123?fields=id,name,email

# Include related resources (embedding)
GET /users/123?include=orders,profile
GET /articles/456?include=author,comments&fields[articles]=title,body&fields[author]=name

8. Versioning

8.1 URL Path Versioning (Most Common)

GET /v1/users/123
GET /v2/users/123

Pros:
- Clear and intuitive
- Easy to test in browser
- Easy routing separation

Cons:
- Violates REST principle that URL should identify resource location
- All URLs change on version change

8.2 Header Versioning

# Accept header (Content Negotiation)
GET /users/123
Accept: application/vnd.myapi.v2+json

# Custom header
GET /users/123
X-API-Version: 2

Pros: Clean URLs, follows REST principles
Cons: Testing inconvenient, harder to debug

8.3 Version Management Strategy

1. Compatibility principles:
   - Adding fields is non-breaking (no impact on existing clients)
   - Removing/renaming fields is breaking -> new version needed
   - Changing field types is breaking -> new version needed

2. Deprecation process:
   Phase 1: Release new version + add Sunset header to old
   Phase 2: Add warning responses to old version
   Phase 3: End old version (minimum 6-month grace period)

   HTTP Headers:
   Sunset: Sat, 01 Jan 2026 00:00:00 GMT
   Deprecation: true
   Link: <https://api.example.com/v2>; rel="successor-version"

3. API changelog:
   - Document all changes
   - Provide migration guides
   - Pre-notify clients

9. Authentication and Security

9.1 Authentication Method Comparison

MethodUse CaseProsCons
API KeyServer-to-server, simple APISimple implementationHard to scope permissions
OAuth 2.0Acting on behalf of userStandardized, scope controlComplex implementation
JWTStateless authenticationEasy server scalingHard to revoke tokens
SessionTraditional web appsEasy server controlScalability limited

9.2 JWT (JSON Web Token)

JWT Structure:
Header.Payload.Signature

Header:
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "user-123",
  "email": "alice@example.com",
  "roles": ["admin", "user"],
  "iat": 1711353000,
  "exp": 1711356600
}

Signature:
RS256(base64(header) + "." + base64(payload), privateKey)

JWT Usage Pattern:

# Access Token + Refresh Token
Access Token: 15-minute validity
Refresh Token: 7-day validity

1. Login -> Issue Access + Refresh tokens
2. API call -> Authorization: Bearer access_token_value
3. Access Token expired -> Refresh using Refresh Token
4. Refresh Token expired -> Re-login

# Header example
GET /users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

9.3 OAuth 2.0 Flows

Authorization Code Flow (Web apps):
1. User -> redirect to authorization server
2. User login + consent
3. Authorization code returned to callback URL
4. Exchange authorization code for Access Token
5. Call API with Access Token

Client Credentials Flow (Server-to-server):
1. Request token directly with Client ID + Secret
2. Access Token issued
3. Call API

9.4 API Security Checklist

1. HTTPS mandatory (never HTTP)
2. Auth tokens in headers only (never URL parameters)
3. Input validation (SQL Injection, XSS prevention)
4. CORS configuration (allowed domains only)
5. Rate Limiting applied
6. Never log sensitive data
7. Never expose internal info in error messages
8. Manage API keys via environment variables
9. Regular security audits
10. Dependency vulnerability scanning

10. Rate Limiting

10.1 Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # Max requests per hour
X-RateLimit-Remaining: 742       # Remaining requests
X-RateLimit-Reset: 1711360000    # Reset time (Unix timestamp)

# When limit exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60                  # Retry after 60 seconds
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711360000

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "You have exceeded the rate limit of 1000 requests per hour.",
  "retry_after": 60
}

10.2 Rate Limiting Strategies

1. Per-user limits:
   - Authenticated users: 1000 req/hour
   - Unauthenticated users: 100 req/hour

2. Per-endpoint limits:
   - GET /users: 100 req/min
   - POST /users: 10 req/min
   - POST /payments: 5 req/min

3. Tier-based limits:
   - Free: 100 req/day
   - Basic: 10,000 req/day
   - Pro: 100,000 req/day
   - Enterprise: Custom

4. Implementation (Redis):
   key = "rate:user:123:2025-03-25-10"  # user + time window
   INCR key
   EXPIRE key 3600

11. HATEOAS

11.1 What is HATEOAS?

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "status": "active",
  "_links": {
    "self": {
      "href": "/users/123"
    },
    "orders": {
      "href": "/users/123/orders"
    },
    "deactivate": {
      "href": "/users/123/deactivate",
      "method": "POST"
    },
    "update": {
      "href": "/users/123",
      "method": "PUT"
    }
  }
}

11.2 HATEOAS Pros and Cons

Pros:
- API is explorable (self-documenting)
- Client does not need to hardcode URLs
- Server can change URL structure
- Discover available actions from responses

Cons:
- Increased response size
- Implementation complexity
- Most clients do not utilize it
- Not mandatory in practice

Conclusion: Recommended for public APIs, optional for internal APIs

12. Bulk Operations and Async Processing

12.1 Batch Endpoints

// POST /users/batch
{
  "operations": [
    { "method": "POST", "body": { "name": "Alice", "email": "a@ex.com" } },
    { "method": "POST", "body": { "name": "Bob", "email": "b@ex.com" } },
    { "method": "POST", "body": { "name": "Carol", "email": "c@ex.com" } }
  ]
}

// Response: 207 Multi-Status
{
  "results": [
    { "status": 201, "data": { "id": 1, "name": "Alice" } },
    { "status": 201, "data": { "id": 2, "name": "Bob" } },
    { "status": 409, "error": { "code": "USER_002", "message": "Email already exists" } }
  ]
}

12.2 Async Processing Pattern

# Long-running tasks (report generation, bulk data processing)

POST /reports
Body: { "type": "annual", "year": 2025 }

Response: 202 Accepted
{
  "task_id": "task-abc-123",
  "status": "processing",
  "estimated_time": 120,
  "_links": {
    "status": { "href": "/tasks/task-abc-123" },
    "cancel": { "href": "/tasks/task-abc-123/cancel", "method": "POST" }
  }
}

# Check status
GET /tasks/task-abc-123

{
  "task_id": "task-abc-123",
  "status": "completed",
  "result": {
    "download_url": "/reports/2025-annual.pdf"
  },
  "completed_at": "2025-03-25T10:35:00Z"
}

13. OpenAPI 3.1 Specification

13.1 OpenAPI Basic Structure

openapi: "3.1.0"
info:
  title: User Management API
  description: API for managing users
  version: "1.0.0"
  contact:
    email: api@example.com

servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging-api.example.com/v1
    description: Staging

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  pagination:
                    $ref: "#/components/schemas/Pagination"

    post:
      summary: Create user
      operationId: createUser
      tags:
        - Users
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
      responses:
        "201":
          description: User created
          headers:
            Location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
      required:
        - id
        - name
        - email

    CreateUserRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
      required:
        - name
        - email

13.2 OpenAPI Code Generation

# Auto-generate code from OpenAPI

1. Client SDK generation:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g typescript-fetch \
     -o ./generated-client

2. Server stub generation:
   npx openapi-generator-cli generate \
     -i openapi.yaml \
     -g python-flask \
     -o ./generated-server

3. Mock server:
   npx prism mock openapi.yaml

4. API documentation:
   npx redocly build-docs openapi.yaml

14. API Design Anti-Patterns

14.1 10 Common Mistakes

1. Using verbs in URLs
   Bad:  POST /createUser
   Good: POST /users

2. Inconsistent naming
   Bad:  /users, /getOrders, /product-list
   Good: /users, /orders, /products

3. Singular resource names
   Bad:  /user/123
   Good: /users/123

4. Ignoring HTTP methods
   Bad:  POST /users/123/delete
   Good: DELETE /users/123

5. Always returning 200
   Bad:  200 OK { "error": true, "message": "Not found" }
   Good: 404 Not Found { "title": "Not Found", ... }

6. Excessive nesting
   Bad:  /countries/us/cities/nyc/districts/manhattan/restaurants
   Good: /restaurants?city=nyc&district=manhattan

7. Unversioned API
   Bad:  /users (can change anytime)
   Good: /v1/users

8. No error response standard
   Bad:  { "error": "something went wrong" }
   Good: RFC 7807 Problem Details format

9. No pagination
   Bad:  GET /logs (returns 1 million records)
   Good: GET /logs?limit=50&cursor=abc

10. Missing security headers
    Bad:  Allow HTTP, CORS wide open
    Good: HTTPS required, minimal CORS, proper headers

15. GraphQL vs REST vs gRPC Comparison

+--------------+-------------------+-------------------+-------------------+
|   Feature    |      REST         |     GraphQL       |      gRPC         |
+--------------+-------------------+-------------------+-------------------+
| Protocol     | HTTP/1.1, 2       | HTTP/1.1, 2       | HTTP/2            |
| Data Format  | JSON/XML          | JSON              | Protocol Buffers  |
| Schema       | OpenAPI (optional)| SDL (required)    | .proto (required) |
| Over/Under   | Over-fetching     | Client decides    | Exact schema      |
|   fetching   | possible          |                   |                   |
| Real-time    | WebSocket/SSE     | Subscriptions     | Bidirectional     |
|              |                   |                   | Streaming         |
| Use Cases    | Public APIs,      | Complex data      | Microservices     |
|              | simple CRUD       | relations, mobile | internal comms    |
| Learning     | Low               | Medium            | High              |
| Caching      | HTTP caching easy | Difficult         | None              |
| Error        | HTTP status codes | errors array      | Status codes      |
+--------------+-------------------+-------------------+-------------------+

Selection Guide:

Choose REST:
- Public APIs (for external developers)
- Simple CRUD operations
- HTTP caching needed
- Direct browser calls

Choose GraphQL:
- Multiple clients (web, mobile, IoT)
- Complex data relationships
- Need to solve over/under-fetching
- Rapid frontend development

Choose gRPC:
- Internal microservice communication
- Low latency requirements
- Bidirectional streaming needed
- Strong typed contracts needed

16. Quiz

Q1. REST Principles

Explain why the "Stateless" constraint contributes to scalability.

Answer: The Stateless constraint means the server does not store client state, so each request contains all information needed for processing. This enables:

  1. Simple load balancing: Any server can handle any request (no session stickiness needed)
  2. Easy horizontal scaling: Servers can be added/removed freely
  3. Easy fault recovery: On server failure, another server immediately takes over
  4. Improved caching efficiency: Requests are independent, making cache key generation straightforward

Q2. Pagination

Describe 2 situations where you should choose Cursor pagination over Offset pagination.

Answer:

  1. Large datasets: With millions of records, Offset requires OFFSET 100000 which skips many rows and is very slow. Cursor uses indexes with a WHERE clause to jump directly, maintaining consistent performance regardless of data size.

  2. Real-time data: When data is frequently added or deleted, Offset causes duplicates or misses when re-requesting the same page. Cursor uses the unique identifier of the last seen item as reference point, maintaining consistency.

Q3. Error Handling

Explain the difference between 401 Unauthorized and 403 Forbidden with specific examples.

Answer:

  • 401 Unauthorized (Authentication failure): The client has not proven who they are. For example, calling an API without an Authorization header, or using an expired JWT token. "I don't know who you are. Please log in."

  • 403 Forbidden (Authorization failure): The client is authenticated but lacks permission for the resource. For example, a regular user calling an admin-only API. "I know who you are, but you don't have permission to access this resource."

Q4. Versioning

Compare the pros and cons of URL path versioning (/v1/users) vs Accept header versioning.

Answer: URL Path Versioning (/v1/users):

  • Pros: Intuitive, easy browser testing, easy routing separation, easy caching
  • Cons: Violates REST principles (URL should identify resource location), all URLs change

Accept Header Versioning (Accept: application/vnd.myapi.v2+json):

  • Pros: Clean URLs, follows REST principles, one URL for multiple versions
  • Cons: Testing inconvenient (need curl/Postman), harder debugging, complex caching

In practice, URL path versioning is overwhelmingly more common (GitHub, Stripe, Google, etc.).

Q5. Security

Explain JWT pros/cons and how to respond to token theft.

Answer: Pros: No server session storage needed (Stateless), easy horizontal scaling, easy propagation between microservices, user info in claims

Cons: Hard to revoke tokens (valid until expiration), larger than session IDs, payload not encrypted (only encoded)

Token theft response:

  1. Short Access Token validity (15 minutes)
  2. Refresh Token rotation (issue new token on use, invalidate old)
  3. Token blacklist (store revoked tokens in Redis)
  4. IP/User-Agent binding
  5. Invalidate all Refresh Tokens on suspicious activity detection

References

  1. RFC 7231 - HTTP/1.1 Semantics and Content - https://tools.ietf.org/html/rfc7231
  2. RFC 7807 - Problem Details for HTTP APIs - https://tools.ietf.org/html/rfc7807
  3. OpenAPI Specification 3.1 - https://spec.openapis.org/oas/v3.1.0
  4. JSON:API Specification - https://jsonapi.org/
  5. Google API Design Guide - https://cloud.google.com/apis/design
  6. Microsoft REST API Guidelines - https://github.com/microsoft/api-guidelines
  7. Stripe API Reference - https://stripe.com/docs/api
  8. GitHub REST API - https://docs.github.com/en/rest
  9. Zalando RESTful API Guidelines - https://opensource.zalando.com/restful-api-guidelines/
  10. REST API Design Rulebook - Mark Masse (O'Reilly)
  11. OAuth 2.0 RFC 6749 - https://tools.ietf.org/html/rfc6749
  12. JWT RFC 7519 - https://tools.ietf.org/html/rfc7519
  13. Roy Fielding's Dissertation - https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  14. Richardson Maturity Model - https://martinfowler.com/articles/richardsonMaturityModel.html