REST API Design: The Complete Guide for 2026

1. What is REST and RESTful API Design

REST stands for Representational State Transfer. It is an architectural style for designing networked applications, defined by Roy Fielding in his 2000 doctoral dissertation at UC Irvine. REST is not a protocol, not a standard, and not a specification — it is a set of constraints that, when followed, produce systems that are scalable, reliable, and easy to evolve.

An API that follows REST constraints is called a RESTful API. In practice, a RESTful API uses HTTP as its transport protocol, represents data as resources identified by URLs, and uses standard HTTP methods to perform operations on those resources.

The Six REST Constraints

  1. Client-Server Separation — The client and server are independent. The client does not know how data is stored. The server does not know how data is displayed. This separation allows each to evolve independently.
  2. Statelessness — Every request from client to server must contain all the information needed to understand and process the request. The server does not store client session state between requests. Authentication tokens, pagination cursors, and filters must be sent with every request.
  3. Cacheability — Responses must define themselves as cacheable or non-cacheable. When a response is cacheable, the client (or intermediary) can reuse that response for identical future requests, reducing load and improving performance.
  4. Uniform Interface — All resources are accessed through a consistent, standardized interface. This is the most fundamental constraint and includes four sub-constraints: resource identification through URIs, resource manipulation through representations, self-descriptive messages, and hypermedia as the engine of application state (HATEOAS).
  5. Layered System — The client cannot tell whether it is connected directly to the server or to an intermediary such as a load balancer, cache, or API gateway. Layers can be added or removed without affecting the client-server interaction.
  6. Code on Demand (Optional) — Servers can extend client functionality by sending executable code (such as JavaScript). This is the only optional constraint and is rarely used in modern REST APIs.

Resources: The Core of REST

In REST, everything is a resource. A resource is any concept that can be named and addressed: a user, a blog post, an order, a shopping cart, a configuration setting. Each resource has a unique identifier (its URL) and one or more representations (JSON, XML, HTML). Clients interact with resources by exchanging representations — they never interact with the resource directly.

# A resource (a specific user) identified by a URL
GET /api/v1/users/42

# The representation returned (JSON)
{
  "id": 42,
  "name": "Alice Chen",
  "email": "alice@example.com",
  "role": "admin",
  "created_at": "2026-01-15T08:30:00Z"
}

This distinction between a resource and its representation is fundamental. The same resource can have multiple representations: JSON for an API client, HTML for a browser, CSV for a data export. The client specifies which representation it wants using the Accept header.

2. HTTP Methods

HTTP methods (also called HTTP verbs) define the action to perform on a resource. REST APIs use these methods to create a uniform interface that every developer understands without reading documentation.

Method Purpose Idempotent Safe Request Body
GET Retrieve a resource or collection Yes Yes No
POST Create a new resource No No Yes
PUT Replace a resource entirely Yes No Yes
PATCH Partially update a resource Can be No Yes
DELETE Remove a resource Yes No Optional
OPTIONS Describe communication options Yes Yes No
HEAD Same as GET but without body Yes Yes No

GET — Retrieve Resources

GET requests retrieve data without modifying it. They are safe (no side effects) and idempotent (calling them multiple times produces the same result). GET requests must never create, update, or delete data.

# Get a collection of users
GET /api/v1/users
Accept: application/json

# Get a specific user
GET /api/v1/users/42
Accept: application/json

# Get a filtered collection
GET /api/v1/users?role=admin&status=active
Accept: application/json

POST — Create Resources

POST creates a new resource within a collection. The server assigns the resource identifier (ID) and returns it in the response. POST is not idempotent — sending the same POST request twice creates two resources.

# Create a new user
POST /api/v1/users
Content-Type: application/json

{
  "name": "Bob Smith",
  "email": "bob@example.com",
  "role": "editor"
}

# Response: 201 Created
# Location: /api/v1/users/43
{
  "id": 43,
  "name": "Bob Smith",
  "email": "bob@example.com",
  "role": "editor",
  "created_at": "2026-02-11T10:00:00Z"
}

PUT — Replace a Resource

PUT replaces the entire resource with the data provided in the request body. Every field must be included. If you omit a field, it should be set to its default value or null. PUT is idempotent — sending the same PUT request multiple times always produces the same result.

# Replace user 43 entirely
PUT /api/v1/users/43
Content-Type: application/json

{
  "name": "Robert Smith",
  "email": "robert@example.com",
  "role": "admin"
}

# Response: 200 OK
{
  "id": 43,
  "name": "Robert Smith",
  "email": "robert@example.com",
  "role": "admin",
  "created_at": "2026-02-11T10:00:00Z"
}

PATCH — Partially Update a Resource

PATCH applies a partial modification to a resource. Only the fields included in the request body are updated; all other fields remain unchanged. This is more bandwidth-efficient than PUT and less error-prone when dealing with resources that have many fields.

# Update only the role
PATCH /api/v1/users/43
Content-Type: application/json

{
  "role": "admin"
}

# Response: 200 OK
{
  "id": 43,
  "name": "Robert Smith",
  "email": "robert@example.com",
  "role": "admin",
  "created_at": "2026-02-11T10:00:00Z"
}

DELETE — Remove a Resource

DELETE removes a resource. It is idempotent — deleting a resource that has already been deleted should return the same result (typically 204 No Content or 404 Not Found, depending on your design choice).

# Delete user 43
DELETE /api/v1/users/43

# Response: 204 No Content
# (empty body)

OPTIONS — Describe Communication Options

OPTIONS returns the HTTP methods and other options available for a resource. It is primarily used in CORS preflight requests, where the browser sends an OPTIONS request before the actual cross-origin request to check if the server allows it.

# Options request
OPTIONS /api/v1/users

# Response: 200 OK
# Allow: GET, POST, OPTIONS
# Access-Control-Allow-Origin: https://example.com
# Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE

HEAD — Metadata Without Body

HEAD is identical to GET except the server must not return a response body. It is used to check if a resource exists, retrieve metadata (content type, content length, last modified date), or verify caching headers without downloading the full response.

# Check if user exists and get metadata
HEAD /api/v1/users/42

# Response: 200 OK
# Content-Type: application/json
# Content-Length: 247
# ETag: "a1b2c3d4"
# Last-Modified: Thu, 11 Feb 2026 08:30:00 GMT
# (no body)

Test HTTP Methods Interactively

Use the HTTP Request Tester to send GET, POST, PUT, PATCH, and DELETE requests to any API endpoint and inspect the response headers, status codes, and body in real time.

3. URL Structure and Naming Conventions

Well-designed URLs are the foundation of a usable API. They should be intuitive, consistent, and predictable. A developer should be able to guess the URL for a resource without reading documentation.

Core Rules

Resource Hierarchy

Use URL paths to express relationships between resources. Nest resources only when the child cannot exist without the parent.

# Good: clear hierarchy
GET /api/v1/users/42/posts          # Posts by user 42
GET /api/v1/users/42/posts/7        # Post 7 by user 42
GET /api/v1/posts/7/comments        # Comments on post 7
GET /api/v1/posts/7/comments/15     # Comment 15 on post 7

# Bad: too deeply nested (more than 2 levels)
GET /api/v1/users/42/posts/7/comments/15/likes    # Too deep

# Better: flatten when relationship is not strictly hierarchical
GET /api/v1/comments/15/likes       # Likes on comment 15
GET /api/v1/likes?comment_id=15     # Or use query parameter

Common URL Patterns

# Collection operations
GET    /api/v1/users              # List all users
POST   /api/v1/users              # Create a user

# Single resource operations
GET    /api/v1/users/42           # Get user 42
PUT    /api/v1/users/42           # Replace user 42
PATCH  /api/v1/users/42           # Update user 42
DELETE /api/v1/users/42           # Delete user 42

# Sub-resource collections
GET    /api/v1/users/42/posts     # List posts by user 42
POST   /api/v1/users/42/posts     # Create a post for user 42

# Search and filtering
GET    /api/v1/users?role=admin              # Filter by role
GET    /api/v1/users?q=alice                 # Search
GET    /api/v1/posts?author=42&status=published  # Multiple filters

# Actions (when CRUD does not fit)
POST   /api/v1/users/42/activate             # Custom action
POST   /api/v1/orders/99/cancel              # Cancel an order
POST   /api/v1/posts/7/publish               # Publish a post

Handling Non-CRUD Actions

Not every operation maps cleanly to CRUD. For actions like "send email", "activate account", or "cancel order", you have several options:

4. Request and Response Formats

JSON is the standard format for REST API request and response bodies. It is human-readable, widely supported across every programming language, and has a smaller payload size than XML.

Content Negotiation

Content negotiation allows clients and servers to agree on the format of data. The client sends an Accept header specifying what formats it can handle, and the server responds with the best match.

# Client requests JSON
GET /api/v1/users/42
Accept: application/json

# Client requests XML (if supported)
GET /api/v1/users/42
Accept: application/xml

# Client sends JSON
POST /api/v1/users
Content-Type: application/json
Accept: application/json

{
  "name": "Alice Chen",
  "email": "alice@example.com"
}

JSON Conventions

# Good: wrapped collection with metadata
{
  "data": [
    {"id": "1", "name": "Alice"},
    {"id": "2", "name": "Bob"}
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "per_page": 20
  }
}

# Bad: bare array (cannot add metadata without breaking change)
[
  {"id": 1, "name": "Alice"},
  {"id": 2, "name": "Bob"}
]

Response Envelope Pattern

Many APIs use a consistent response envelope that wraps every response in a standard structure:

# Success response
{
  "status": "success",
  "data": {
    "id": "42",
    "name": "Alice Chen",
    "email": "alice@example.com"
  }
}

# Error response
{
  "status": "error",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The email field is required.",
    "details": [
      {
        "field": "email",
        "message": "This field is required."
      }
    ]
  }
}

Format and Validate JSON Responses

Use the JSON Formatter to pretty-print, validate, and analyze API response payloads. Paste any JSON and instantly see it formatted with syntax highlighting.

5. HTTP Status Codes

Status codes tell the client what happened with its request. Using the correct status code is essential for a well-designed API — clients rely on status codes to determine how to handle the response.

2xx Success

Code Name When to Use
200 OK Successful GET, PUT, PATCH, or DELETE that returns data
201 Created Successful POST that creates a resource. Include Location header.
204 No Content Successful DELETE or PUT/PATCH with no response body

3xx Redirection

Code Name When to Use
301 Moved Permanently Resource URL has permanently changed. Clients should update their bookmarks.
304 Not Modified Resource has not changed since last request (used with ETags and conditional requests).
307 Temporary Redirect Resource temporarily available at different URL. Preserves HTTP method.

4xx Client Errors

Code Name When to Use
400 Bad Request Invalid syntax, malformed JSON, or missing required fields
401 Unauthorized Authentication required or credentials are invalid
403 Forbidden Authenticated but does not have permission to access this resource
404 Not Found Resource does not exist at this URL
405 Method Not Allowed HTTP method not supported for this endpoint (e.g., DELETE on a read-only resource)
409 Conflict Request conflicts with current state (e.g., duplicate email, version mismatch)
422 Unprocessable Entity Well-formed request but semantic errors (e.g., email format invalid, age is negative)
429 Too Many Requests Rate limit exceeded. Include Retry-After header.

5xx Server Errors

Code Name When to Use
500 Internal Server Error Unexpected server error. Never expose stack traces to clients.
502 Bad Gateway Server received an invalid response from an upstream server
503 Service Unavailable Server temporarily unavailable (maintenance, overload). Include Retry-After.
504 Gateway Timeout Upstream server did not respond in time

Status Code Decision Guide

# Common patterns by HTTP method:

# GET success    → 200 OK (with body)
# POST success   → 201 Created (with body + Location header)
# PUT success    → 200 OK (with body) or 204 No Content
# PATCH success  → 200 OK (with body) or 204 No Content
# DELETE success → 204 No Content (no body)

# Validation failed       → 422 Unprocessable Entity
# Malformed JSON          → 400 Bad Request
# Not logged in           → 401 Unauthorized
# Logged in, no access    → 403 Forbidden
# Resource does not exist → 404 Not Found
# Duplicate/conflict      → 409 Conflict
# Rate limited            → 429 Too Many Requests

6. Authentication and Authorization

Authentication verifies who the client is. Authorization determines what the client is allowed to do. Every production API needs both. There are four common authentication mechanisms for REST APIs.

API Keys

An API key is a unique string assigned to each client application. It is the simplest authentication method but provides the least security because the key is a shared secret that can be leaked.

# API key in header (preferred)
GET /api/v1/users
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0

# API key in query parameter (less secure — visible in logs)
GET /api/v1/users?api_key=sk_live_a1b2c3d4e5f6g7h8i9j0

Best for: Server-to-server communication, public APIs with rate limiting, low-sensitivity data.

Not suitable for: User-facing applications where the key would be exposed in client-side code.

OAuth 2.0

OAuth 2.0 is the industry-standard authorization framework. It allows a user to grant a third-party application limited access to their data without sharing their password. OAuth 2.0 defines several grant types (flows):

# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
    response_type=code&
    client_id=YOUR_CLIENT_ID&
    redirect_uri=https://yourapp.com/callback&
    scope=read:users write:posts&
    state=random_csrf_token

# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
redirect_uri=https://yourapp.com/callback

# Step 3: Use the access token
GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

JWT (JSON Web Tokens)

JWT is a compact, URL-safe token format that contains encoded claims (data) about the user. A JWT has three parts separated by dots: header, payload, and signature.

# JWT structure: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSBDaGVuIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzM5MzU4ODAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# Decoded header
{
  "alg": "HS256",
  "typ": "JWT"
}

# Decoded payload
{
  "sub": "42",
  "name": "Alice Chen",
  "role": "admin",
  "exp": 1739358800
}

# The signature is computed as:
# HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Advantages: Stateless (server does not need a session store), self-contained (contains user data), can be verified without a database lookup.

Disadvantages: Cannot be revoked until expiry (use short-lived tokens + refresh tokens), payload is only encoded (not encrypted) so do not store sensitive data in it.

Bearer Tokens

Bearer tokens are sent in the Authorization header. The token can be a JWT, an opaque token, or any string that the server can validate.

# Using a bearer token
GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

# Server validates the token and extracts the user identity
# If invalid or expired → 401 Unauthorized

Authentication Comparison

Method Security Complexity Best For
API Keys Low-Medium Simple Server-to-server, public APIs
OAuth 2.0 High Complex Third-party access, user delegation
JWT Medium-High Medium Stateless auth, microservices
Bearer Tokens Medium-High Low Session replacement, mobile apps

Decode and Inspect JWT Tokens

Use the JWT Decoder to decode any JWT token and inspect its header, payload, and expiration time. All processing happens client-side — your tokens never leave your browser.

7. Pagination

Any endpoint that returns a collection must support pagination. Without it, a request for "all users" could return millions of records, overwhelming the client, the server, and the network.

Offset-Based Pagination

The simplest approach. The client specifies a page number and page size. The server skips the appropriate number of rows and returns the requested page.

# Request page 3 with 20 items per page
GET /api/v1/posts?page=3&per_page=20

# Response
{
  "data": [
    {"id": "41", "title": "Post 41"},
    {"id": "42", "title": "Post 42"},
    ...
  ],
  "meta": {
    "current_page": 3,
    "per_page": 20,
    "total_items": 487,
    "total_pages": 25
  },
  "links": {
    "first": "/api/v1/posts?page=1&per_page=20",
    "prev": "/api/v1/posts?page=2&per_page=20",
    "next": "/api/v1/posts?page=4&per_page=20",
    "last": "/api/v1/posts?page=25&per_page=20"
  }
}

Advantages: Simple to implement, allows jumping to any page, clients can display page numbers.

Disadvantages: Performance degrades on large datasets (SQL OFFSET scans and discards rows), inconsistent results when items are added or deleted between requests (items can be skipped or duplicated).

Cursor-Based Pagination

The server returns an opaque cursor (typically a base64-encoded value) that points to the last item in the current page. The client sends this cursor to get the next page.

# First request
GET /api/v1/posts?limit=20

# Response
{
  "data": [...],
  "meta": {
    "has_next": true,
    "next_cursor": "eyJpZCI6NDAsImNyZWF0ZWRfYXQiOiIyMDI2LTAyLTEwVDA4OjAwOjAwWiJ9"
  }
}

# Next page
GET /api/v1/posts?limit=20&cursor=eyJpZCI6NDAsImNyZWF0ZWRfYXQiOiIyMDI2LTAyLTEwVDA4OjAwOjAwWiJ9

Advantages: Consistent performance regardless of dataset size, handles real-time data changes gracefully, no skipped or duplicated items.

Disadvantages: Cannot jump to an arbitrary page, cannot display total page count (without a separate count query).

Keyset Pagination

Similar to cursor-based, but the cursor is a visible field value (such as an ID or timestamp) rather than an opaque token. This makes debugging easier.

# First request
GET /api/v1/posts?limit=20&sort=created_at:desc

# Next page (using the last item's values)
GET /api/v1/posts?limit=20&sort=created_at:desc&after_id=40&after_created_at=2026-02-10T08:00:00Z

Pagination Best Practices

8. Filtering, Sorting, and Field Selection

Beyond pagination, clients need to filter collections, sort results, and select only the fields they need.

Filtering

Use query parameters to filter collections. Keep parameter names consistent with your resource field names.

# Simple equality filters
GET /api/v1/users?role=admin&status=active

# Range filters
GET /api/v1/products?price_min=10&price_max=100
GET /api/v1/orders?created_after=2026-01-01&created_before=2026-02-01

# Multiple values (OR logic)
GET /api/v1/products?category=electronics,books,toys

# Search
GET /api/v1/users?q=alice           # Full-text search
GET /api/v1/users?name_like=ali     # Partial match

# Advanced filtering with operators
GET /api/v1/products?filter[price][gte]=10&filter[price][lte]=100
GET /api/v1/users?filter[created_at][gt]=2026-01-01

Sorting

Allow clients to sort by one or more fields. Use a consistent syntax for specifying sort order.

# Sort by a single field (ascending by default)
GET /api/v1/users?sort=name

# Sort descending (prefix with minus)
GET /api/v1/users?sort=-created_at

# Multi-field sort (comma-separated)
GET /api/v1/products?sort=-rating,price
# Sort by rating descending, then by price ascending

# Alternative syntax
GET /api/v1/users?sort_by=created_at&sort_order=desc

Field Selection (Sparse Fieldsets)

Allow clients to request only the fields they need. This reduces response size and improves performance, especially for mobile clients on slow networks.

# Select specific fields
GET /api/v1/users?fields=id,name,email

# Response includes only requested fields
{
  "data": [
    {"id": "1", "name": "Alice", "email": "alice@example.com"},
    {"id": "2", "name": "Bob", "email": "bob@example.com"}
  ]
}

# Field selection per resource type (JSON:API style)
GET /api/v1/posts?fields[posts]=title,body&fields[author]=name

# Include related resources (expansion)
GET /api/v1/posts?include=author,comments
GET /api/v1/orders/99?expand=customer,line_items

9. Versioning Strategies

APIs evolve over time. Fields are added, removed, or renamed. Response formats change. Versioning allows you to make breaking changes without disrupting existing clients.

URL Path Versioning

The most common and most visible approach. The version number is part of the URL path.

# Version in URL path
GET /api/v1/users/42
GET /api/v2/users/42

# Advantages:
# - Easy to understand and test in a browser
# - Clear which version is being used
# - Can run multiple versions simultaneously
# - Works with API gateways and load balancers

# Disadvantages:
# - Changes the resource URI (purists argue this violates REST)
# - Requires maintaining multiple codepaths or deployments

Custom Header Versioning

The version is specified in a custom HTTP header. The URL remains the same across versions.

# Version in custom header
GET /api/users/42
API-Version: 2

# Or using Accept header with media type versioning
GET /api/users/42
Accept: application/vnd.myapi.v2+json

# Advantages:
# - Clean URLs that do not change across versions
# - Follows REST principles more strictly

# Disadvantages:
# - Cannot test in a browser address bar
# - Harder to discover and debug
# - Requires documentation to know which versions exist

Query Parameter Versioning

The version is specified as a query parameter.

# Version as query parameter
GET /api/users/42?version=2

# Advantages:
# - Easy to add to existing APIs
# - Can default to latest version if omitted

# Disadvantages:
# - Can interfere with caching (different query = different cache key)
# - Clutters the URL with non-resource parameters

Versioning Best Practices

10. Error Handling

Good error handling is the difference between an API that developers love and one they curse. When something goes wrong, the error response should tell the client exactly what happened, why, and how to fix it.

Error Response Format

Use a consistent error response format across your entire API. Here is a recommended structure based on RFC 7807 (Problem Details for HTTP APIs):

# Standard error response
{
  "type": "https://api.example.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/api/v1/users",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 13 and 150."
    }
  ],
  "request_id": "req_a1b2c3d4e5",
  "timestamp": "2026-02-11T10:30:00Z"
}

Error Response Fields

Common Error Patterns

# 400 Bad Request — Malformed JSON
{
  "type": "https://api.example.com/errors/bad-request",
  "title": "Bad Request",
  "status": 400,
  "detail": "The request body contains invalid JSON. Expected ',' or '}' at line 3, column 15.",
  "request_id": "req_f6g7h8"
}

# 401 Unauthorized — Missing or invalid token
{
  "type": "https://api.example.com/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "The access token is expired. Please refresh your token and try again.",
  "request_id": "req_i9j0k1"
}

# 403 Forbidden — Insufficient permissions
{
  "type": "https://api.example.com/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "You do not have permission to delete this resource. Required role: admin.",
  "request_id": "req_l2m3n4"
}

# 404 Not Found
{
  "type": "https://api.example.com/errors/not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "No user found with ID 999.",
  "request_id": "req_o5p6q7"
}

# 409 Conflict — Duplicate resource
{
  "type": "https://api.example.com/errors/conflict",
  "title": "Conflict",
  "status": 409,
  "detail": "A user with email alice@example.com already exists.",
  "request_id": "req_r8s9t0"
}

# 429 Too Many Requests
{
  "type": "https://api.example.com/errors/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. You have made 101 requests in the last 60 seconds. Limit: 100.",
  "retry_after": 45,
  "request_id": "req_u1v2w3"
}

# 500 Internal Server Error
{
  "type": "https://api.example.com/errors/internal-error",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "An unexpected error occurred. Our team has been notified.",
  "request_id": "req_x4y5z6"
}

Error Handling Best Practices

11. Rate Limiting and Throttling

Rate limiting protects your API from abuse, prevents denial-of-service attacks, and ensures fair usage across all clients. Every production API should implement rate limiting.

Rate Limit Headers

Include rate limit information in response headers so clients can monitor their usage and avoid hitting limits:

# Response headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # Maximum requests per window
X-RateLimit-Remaining: 847       # Requests remaining in current window
X-RateLimit-Reset: 1739358800    # Unix timestamp when the window resets
X-RateLimit-Window: 3600         # Window duration in seconds (1 hour)

# When rate limited (429 response)
HTTP/1.1 429 Too Many Requests
Retry-After: 45                  # Seconds until the client can retry
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1739358800

Rate Limiting Strategies

Tiered Rate Limits

# Different limits by plan
Free tier:       100 requests/hour,    1,000/day
Basic tier:    1,000 requests/hour,   10,000/day
Pro tier:     10,000 requests/hour,  100,000/day
Enterprise:   Custom limits

# Different limits by endpoint
GET  /api/v1/search:  30 requests/minute   (expensive operation)
GET  /api/v1/users:  100 requests/minute   (standard read)
POST /api/v1/users:   10 requests/minute   (write operation)
POST /api/v1/emails:   5 requests/minute   (sending emails)

12. HATEOAS and Hypermedia

HATEOAS (Hypermedia As The Engine Of Application State) is one of the REST constraints that is most often overlooked. It means that API responses should include links (hypermedia controls) that tell the client what actions are available next.

In practice, this means that a client should be able to navigate the entire API starting from a single entry point, following links in responses, without hardcoding any URLs.

# A HATEOAS-enabled response
GET /api/v1/orders/99

{
  "id": "99",
  "status": "pending",
  "total": 149.99,
  "created_at": "2026-02-11T10:00:00Z",
  "customer": {
    "id": "42",
    "name": "Alice Chen"
  },
  "_links": {
    "self": {"href": "/api/v1/orders/99"},
    "customer": {"href": "/api/v1/customers/42"},
    "items": {"href": "/api/v1/orders/99/items"},
    "cancel": {"href": "/api/v1/orders/99/cancel", "method": "POST"},
    "pay": {"href": "/api/v1/orders/99/pay", "method": "POST"}
  }
}

# After the order is paid, the response changes:
GET /api/v1/orders/99

{
  "id": "99",
  "status": "paid",
  "total": 149.99,
  "_links": {
    "self": {"href": "/api/v1/orders/99"},
    "customer": {"href": "/api/v1/customers/42"},
    "items": {"href": "/api/v1/orders/99/items"},
    "refund": {"href": "/api/v1/orders/99/refund", "method": "POST"},
    "receipt": {"href": "/api/v1/orders/99/receipt"}
  }
  // Note: "cancel" and "pay" links are gone because they are no longer valid
}

Benefits of HATEOAS

HATEOAS in Practice

Full HATEOAS is rarely implemented because it adds complexity and most API clients hardcode URLs anyway. However, including basic links (self, next, prev, related resources) is a widely adopted best practice that improves API usability.

13. Caching Strategies

Caching is one of the REST constraints and one of the most impactful performance optimizations for any API. Proper caching reduces server load, decreases latency, and saves bandwidth.

Cache-Control Headers

The Cache-Control header tells clients and intermediaries (CDNs, proxies) how to cache a response.

# Cache for 1 hour (public — CDNs can cache)
Cache-Control: public, max-age=3600

# Cache for 5 minutes (private — only the client can cache)
Cache-Control: private, max-age=300

# No caching at all (sensitive data, user-specific responses)
Cache-Control: no-store

# Must revalidate with server before using cached version
Cache-Control: no-cache

# Cache but revalidate after max-age expires
Cache-Control: public, max-age=3600, must-revalidate

# Stale-while-revalidate: serve stale content while fetching fresh
Cache-Control: public, max-age=60, stale-while-revalidate=300

ETags (Entity Tags)

An ETag is a unique identifier for a specific version of a resource. The server generates it (typically a hash of the response body) and sends it with the response. The client can use it for conditional requests.

# Server sends ETag with response
GET /api/v1/users/42
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"

{
  "id": "42",
  "name": "Alice Chen",
  "email": "alice@example.com"
}

# Client sends conditional request with If-None-Match
GET /api/v1/users/42
If-None-Match: "a1b2c3d4e5f6"

# If the resource has NOT changed:
HTTP/1.1 304 Not Modified
# (no body — client uses its cached version)

# If the resource HAS changed:
HTTP/1.1 200 OK
ETag: "f7g8h9i0j1k2"
{
  "id": "42",
  "name": "Alice Chen-Smith",
  "email": "alice@example.com"
}

Conditional Requests

Conditional requests allow the client to avoid downloading data that has not changed. There are two mechanisms:

# Optimistic concurrency control with ETags
# Step 1: Client fetches resource and receives ETag
GET /api/v1/users/42
HTTP/1.1 200 OK
ETag: "a1b2c3d4"

# Step 2: Client updates with If-Match
PUT /api/v1/users/42
If-Match: "a1b2c3d4"
Content-Type: application/json

{"name": "Alice Chen-Smith", "email": "alice@example.com", "role": "admin"}

# If no one else modified it → 200 OK
# If someone else modified it → 412 Precondition Failed

Caching Best Practices

14. Security Best Practices

API security is not optional. A vulnerable API exposes your data, your users, and your infrastructure to attackers. Follow these practices to build secure APIs from the ground up.

Always Use HTTPS

Every API call must use HTTPS. HTTP transmits data in plain text, meaning anyone on the network can read authentication tokens, request bodies, and response data. There are no exceptions to this rule.

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

# HSTS header (tell browsers to always use HTTPS)
Strict-Transport-Security: max-age=31536000; includeSubDomains

CORS (Cross-Origin Resource Sharing)

CORS controls which web domains can call your API from a browser. Without proper CORS configuration, any website could make requests to your API using your users' cookies.

# CORS response headers
Access-Control-Allow-Origin: https://app.example.com    # Specific origin (not *)
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true                   # Allow cookies
Access-Control-Max-Age: 86400                            # Cache preflight for 24h

# NEVER use these in production:
# Access-Control-Allow-Origin: *                # Allows any origin
# Access-Control-Allow-Headers: *               # Allows any header

Input Validation

Never trust client input. Validate every field in every request.

# Example: Express.js input validation with express-validator
const { body, validationResult } = require('express-validator');

app.post('/api/v1/users', [
  body('name').isString().trim().isLength({ min: 1, max: 100 }),
  body('email').isEmail().normalizeEmail(),
  body('age').isInt({ min: 13, max: 150 }),
  body('role').isIn(['user', 'editor', 'admin']),
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({
      type: 'https://api.example.com/errors/validation-error',
      title: 'Validation Error',
      status: 422,
      errors: errors.array().map(e => ({
        field: e.path,
        message: e.msg
      }))
    });
  }
  // Process valid request...
});

Security Headers

# Essential security headers for API responses
X-Content-Type-Options: nosniff          # Prevent MIME-type sniffing
X-Frame-Options: DENY                    # Prevent clickjacking
Content-Security-Policy: default-src 'none'  # No loading of external resources
Referrer-Policy: no-referrer             # Do not send referrer header
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Request-Id: req_a1b2c3d4              # For tracking and debugging

Additional Security Measures

15. API Documentation with OpenAPI

API documentation is the user interface of your API. If developers cannot understand how to use your API from the documentation alone, the API has failed regardless of how well it is designed technically.

OpenAPI Specification (formerly Swagger)

The OpenAPI Specification (OAS) is the industry standard for describing REST APIs. It is a machine-readable format (YAML or JSON) that defines endpoints, request/response schemas, authentication, and examples.

# openapi.yaml — Example OpenAPI 3.1 specification
openapi: 3.1.0
info:
  title: Blog API
  version: 1.0.0
  description: A RESTful API for managing blog posts and comments.
  contact:
    email: api@example.com

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

paths:
  /posts:
    get:
      summary: List all posts
      operationId: listPosts
      tags: [Posts]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, published, archived]
      responses:
        '200':
          description: A paginated list of posts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PostList'
    post:
      summary: Create a new post
      operationId: createPost
      tags: [Posts]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePost'
            example:
              title: "REST API Design Guide"
              body: "This is a comprehensive guide..."
              status: draft
      responses:
        '201':
          description: Post created successfully
        '422':
          description: Validation error

  /posts/{id}:
    get:
      summary: Get a specific post
      operationId: getPost
      tags: [Posts]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The requested post
        '404':
          description: Post not found

components:
  schemas:
    Post:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        body:
          type: string
        status:
          type: string
          enum: [draft, published, archived]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreatePost:
      type: object
      required: [title, body]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        body:
          type: string
          minLength: 1
        status:
          type: string
          enum: [draft, published]
          default: draft

    PostList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Post'
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Documentation Tools

Documentation Best Practices

16. GraphQL vs REST

GraphQL is an alternative API architecture developed by Facebook (now Meta) in 2012 and released publicly in 2015. It is not a replacement for REST but a different approach with different tradeoffs.

Key Differences

Aspect REST GraphQL
Endpoints Multiple endpoints (one per resource) Single endpoint (/graphql)
Data fetching Server decides what to return Client specifies exactly what fields it needs
Over-fetching Common (returns all fields) Eliminated (client picks fields)
Under-fetching Common (requires multiple requests) Eliminated (nested queries in one request)
Caching HTTP caching works natively Requires custom caching (e.g., Apollo cache)
Versioning URL or header versioning No versioning needed (evolve schema)
Error handling HTTP status codes Always returns 200; errors in response body
Learning curve Lower (uses familiar HTTP concepts) Higher (new query language, schema, resolvers)
Tooling curl, Postman, any HTTP client Requires GraphQL-specific clients and tools
File uploads Native multipart support Requires workarounds (multipart request spec)

When to Use REST

When to Use GraphQL

Can You Use Both?

Yes. Many organizations use REST for their public API and GraphQL for their frontend applications. Some use a GraphQL gateway that sits in front of multiple REST microservices, aggregating them into a single graph. There is no rule that says you must choose one or the other.

17. Real-World API Design Examples

Theory is essential, but seeing complete examples is what solidifies understanding. Let us design two real-world APIs from scratch: a blog API and an e-commerce API.

Example 1: Blog API

# Blog API — Complete Endpoint Design

# Authentication
POST   /api/v1/auth/register          # Register a new user
POST   /api/v1/auth/login             # Login, returns JWT
POST   /api/v1/auth/refresh           # Refresh access token
POST   /api/v1/auth/logout            # Invalidate refresh token

# Users
GET    /api/v1/users                  # List users (admin only)
GET    /api/v1/users/:id              # Get user profile
PATCH  /api/v1/users/:id              # Update user profile
DELETE /api/v1/users/:id              # Delete user (admin only)
GET    /api/v1/users/:id/posts        # Get posts by user

# Posts
GET    /api/v1/posts                  # List published posts
POST   /api/v1/posts                  # Create a draft post
GET    /api/v1/posts/:id              # Get a specific post
PUT    /api/v1/posts/:id              # Replace a post
PATCH  /api/v1/posts/:id              # Update specific fields
DELETE /api/v1/posts/:id              # Delete a post
POST   /api/v1/posts/:id/publish      # Publish a draft
POST   /api/v1/posts/:id/unpublish    # Revert to draft

# Comments
GET    /api/v1/posts/:id/comments     # List comments on a post
POST   /api/v1/posts/:id/comments     # Add a comment to a post
PATCH  /api/v1/comments/:id           # Edit a comment
DELETE /api/v1/comments/:id           # Delete a comment

# Tags
GET    /api/v1/tags                   # List all tags
GET    /api/v1/tags/:slug/posts       # Get posts with a specific tag

# Search
GET    /api/v1/search?q=rest+api&type=posts   # Search posts
GET    /api/v1/search?q=alice&type=users       # Search users

Blog API: Create Post Request/Response

# Request
POST /api/v1/posts
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json

{
  "title": "Getting Started with REST APIs",
  "body": "REST APIs are the backbone of modern web development...",
  "tags": ["rest", "api", "tutorial"],
  "status": "draft"
}

# Response: 201 Created
# Location: /api/v1/posts/128
{
  "data": {
    "id": "128",
    "title": "Getting Started with REST APIs",
    "slug": "getting-started-with-rest-apis",
    "body": "REST APIs are the backbone of modern web development...",
    "excerpt": "REST APIs are the backbone of modern web...",
    "status": "draft",
    "author": {
      "id": "42",
      "name": "Alice Chen",
      "avatar_url": "https://cdn.example.com/avatars/42.jpg"
    },
    "tags": ["rest", "api", "tutorial"],
    "comment_count": 0,
    "read_time_minutes": 5,
    "created_at": "2026-02-11T10:00:00Z",
    "updated_at": "2026-02-11T10:00:00Z",
    "published_at": null
  },
  "_links": {
    "self": {"href": "/api/v1/posts/128"},
    "author": {"href": "/api/v1/users/42"},
    "comments": {"href": "/api/v1/posts/128/comments"},
    "publish": {"href": "/api/v1/posts/128/publish", "method": "POST"}
  }
}

Example 2: E-Commerce API

# E-Commerce API — Complete Endpoint Design

# Products
GET    /api/v1/products                     # List products (with filters)
GET    /api/v1/products/:id                  # Get product details
POST   /api/v1/products                     # Create product (admin)
PATCH  /api/v1/products/:id                  # Update product (admin)
DELETE /api/v1/products/:id                  # Delete product (admin)
GET    /api/v1/products/:id/reviews          # Get product reviews
POST   /api/v1/products/:id/reviews          # Add a review

# Categories
GET    /api/v1/categories                    # List categories
GET    /api/v1/categories/:slug/products     # Products in category

# Shopping Cart
GET    /api/v1/cart                          # Get current cart
POST   /api/v1/cart/items                    # Add item to cart
PATCH  /api/v1/cart/items/:id                # Update quantity
DELETE /api/v1/cart/items/:id                # Remove item from cart
DELETE /api/v1/cart                          # Clear entire cart

# Orders
POST   /api/v1/orders                       # Create order (checkout)
GET    /api/v1/orders                        # List user's orders
GET    /api/v1/orders/:id                    # Get order details
POST   /api/v1/orders/:id/cancel             # Cancel an order
GET    /api/v1/orders/:id/tracking           # Get shipping tracking

# Payments
POST   /api/v1/payments                     # Process payment
GET    /api/v1/payments/:id                  # Get payment status
POST   /api/v1/payments/:id/refund           # Request refund

# Addresses
GET    /api/v1/addresses                     # List saved addresses
POST   /api/v1/addresses                     # Add address
PATCH  /api/v1/addresses/:id                 # Update address
DELETE /api/v1/addresses/:id                 # Delete address

E-Commerce API: Product Listing with Filters

# Request: List products with filters, sorting, and pagination
GET /api/v1/products?category=electronics&price_min=50&price_max=500&sort=-rating&page=1&per_page=12&fields=id,name,price,rating,thumbnail_url

# Response: 200 OK
{
  "data": [
    {
      "id": "prod_001",
      "name": "Wireless Noise-Cancelling Headphones",
      "price": 299.99,
      "rating": 4.7,
      "thumbnail_url": "https://cdn.example.com/products/001-thumb.jpg"
    },
    {
      "id": "prod_002",
      "name": "Mechanical Keyboard",
      "price": 149.99,
      "rating": 4.5,
      "thumbnail_url": "https://cdn.example.com/products/002-thumb.jpg"
    }
  ],
  "meta": {
    "total": 87,
    "page": 1,
    "per_page": 12,
    "total_pages": 8
  },
  "links": {
    "self": "/api/v1/products?category=electronics&page=1&per_page=12",
    "next": "/api/v1/products?category=electronics&page=2&per_page=12",
    "last": "/api/v1/products?category=electronics&page=8&per_page=12"
  },
  "filters_applied": {
    "category": "electronics",
    "price_min": 50,
    "price_max": 500,
    "sort": "-rating"
  }
}

18. Testing REST APIs

A well-designed API is meaningless if it does not work correctly. API testing verifies that your endpoints return the correct data, status codes, and headers under all conditions.

Types of API Tests

Testing with curl

# GET request
curl -s https://api.example.com/v1/users | jq .

# POST request with JSON body
curl -s -X POST https://api.example.com/v1/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name": "Alice", "email": "alice@example.com"}' | jq .

# PUT request
curl -s -X PUT https://api.example.com/v1/users/42 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"name": "Alice Chen", "email": "alice@example.com", "role": "admin"}' | jq .

# PATCH request
curl -s -X PATCH https://api.example.com/v1/users/42 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"role": "admin"}' | jq .

# DELETE request
curl -s -X DELETE https://api.example.com/v1/users/42 \
  -H "Authorization: Bearer YOUR_TOKEN" -v

# Show response headers
curl -s -D - https://api.example.com/v1/users/42 -o /dev/null

Integration Testing with JavaScript (Jest + Supertest)

const request = require('supertest');
const app = require('../app');

describe('POST /api/v1/users', () => {
  it('should create a user and return 201', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${token}`)
      .send({
        name: 'Alice Chen',
        email: 'alice@example.com',
        role: 'editor'
      });

    expect(res.status).toBe(201);
    expect(res.body.data).toHaveProperty('id');
    expect(res.body.data.name).toBe('Alice Chen');
    expect(res.body.data.email).toBe('alice@example.com');
    expect(res.headers.location).toMatch(/\/api\/v1\/users\/\w+/);
  });

  it('should return 422 for invalid email', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${token}`)
      .send({
        name: 'Bob',
        email: 'not-an-email',
        role: 'editor'
      });

    expect(res.status).toBe(422);
    expect(res.body.errors[0].field).toBe('email');
  });

  it('should return 401 without authentication', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .send({ name: 'Eve', email: 'eve@example.com' });

    expect(res.status).toBe(401);
  });

  it('should return 409 for duplicate email', async () => {
    // First create a user
    await request(app)
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${token}`)
      .send({ name: 'Alice', email: 'alice@example.com', role: 'user' });

    // Try to create another with same email
    const res = await request(app)
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${token}`)
      .send({ name: 'Alice 2', email: 'alice@example.com', role: 'user' });

    expect(res.status).toBe(409);
  });
});

Integration Testing with Python (pytest + requests)

import pytest
import requests

BASE_URL = "https://api.example.com/v1"

class TestUsersAPI:
    def test_list_users_returns_200(self, auth_headers):
        response = requests.get(f"{BASE_URL}/users", headers=auth_headers)
        assert response.status_code == 200
        data = response.json()
        assert "data" in data
        assert "meta" in data
        assert isinstance(data["data"], list)

    def test_create_user_returns_201(self, auth_headers):
        payload = {
            "name": "Alice Chen",
            "email": "alice@example.com",
            "role": "editor"
        }
        response = requests.post(
            f"{BASE_URL}/users",
            json=payload,
            headers=auth_headers
        )
        assert response.status_code == 201
        assert "Location" in response.headers
        user = response.json()["data"]
        assert user["name"] == "Alice Chen"
        assert user["email"] == "alice@example.com"

    def test_get_nonexistent_user_returns_404(self, auth_headers):
        response = requests.get(
            f"{BASE_URL}/users/nonexistent",
            headers=auth_headers
        )
        assert response.status_code == 404
        error = response.json()
        assert error["status"] == 404
        assert "request_id" in error

    def test_invalid_json_returns_400(self, auth_headers):
        response = requests.post(
            f"{BASE_URL}/users",
            data="this is not json",
            headers={**auth_headers, "Content-Type": "application/json"}
        )
        assert response.status_code == 400

API Testing Checklist

Test Your APIs Right Now

Use the HTTP Request Tester to send requests to any endpoint, inspect response headers and status codes, and debug API issues. Use the JSON Formatter to validate and pretty-print response bodies, and the JWT Decoder to inspect authentication tokens.

Frequently Asked Questions

What is the difference between REST and RESTful APIs?

REST (Representational State Transfer) is an architectural style defined by Roy Fielding in his 2000 doctoral dissertation. It describes six constraints: client-server separation, statelessness, cacheability, uniform interface, layered system, and optional code-on-demand. A RESTful API is an API that adheres to these REST constraints. In practice, most APIs described as "RESTful" follow the conventions of using HTTP methods, resource-based URLs, and JSON responses, even if they do not strictly satisfy all six constraints.

Should I use PUT or PATCH to update a resource?

Use PUT when the client sends a complete replacement of the resource — every field must be included in the request body. Use PATCH when the client sends only the fields that need to change (a partial update). PUT is always idempotent: sending the same PUT request twice produces the same result. PATCH can be idempotent depending on the implementation. Most modern APIs prefer PATCH for updates because it reduces bandwidth and avoids accidentally overwriting fields the client did not intend to change.

How should I version my REST API?

The three main API versioning strategies are URL path versioning (e.g., /api/v1/users), custom header versioning (e.g., API-Version: 2), and query parameter versioning (e.g., /api/users?version=1). URL path versioning is the most common and easiest for developers to understand and test in a browser. Header versioning keeps URLs clean but is harder to test. Query parameter versioning is simple but can interfere with caching. Choose one strategy and apply it consistently across your entire API.

What is the best pagination strategy for REST APIs?

The best pagination strategy depends on your use case. Offset-based pagination (?page=2&limit=20) is simplest and allows jumping to any page, but performs poorly on large datasets. Cursor-based pagination uses an opaque token pointing to the last item, offering consistent performance regardless of dataset size. Keyset pagination uses a visible field value (like an ID). For most APIs, cursor-based pagination is recommended because it handles real-time data changes gracefully and scales well.

How do I secure a REST API?

To secure a REST API: always use HTTPS to encrypt data in transit; implement authentication using OAuth 2.0, JWT tokens, or API keys; use authorization to control access to specific resources; validate and sanitize all input to prevent injection attacks; implement rate limiting to prevent abuse; configure CORS headers to restrict which origins can call your API; use security headers like X-Content-Type-Options and Strict-Transport-Security; never expose sensitive data in URLs or error messages; log all access for auditing; and keep dependencies updated to patch known vulnerabilities.

Conclusion

REST API design is a skill that combines technical knowledge with practical experience. The principles in this guide — clear URL structures, correct HTTP methods and status codes, consistent error handling, proper authentication, sensible pagination, and thorough documentation — form the foundation of APIs that developers enjoy using.

The key takeaways from this guide:

Good API design is not about following every rule perfectly. It is about making consistent, well-documented choices that your consumers can rely on. When in doubt, look at how established APIs (Stripe, GitHub, Twilio) handle the same problem — they have invested thousands of engineering hours into getting the details right.

Build and Test Your APIs

Put this knowledge into practice with DevToolbox: test endpoints with the HTTP Request Tester, format responses with the JSON Formatter, and debug authentication with the JWT Decoder. All free, all client-side, no installation required.