Prerequisite:

Overview

REST (Representational State Transfer) is an architectural style for web APIs where resources are identified by URIs, manipulated via standard HTTP verbs, and represented in self-describing formats - almost always JSON. Stateless request-response semantics allow horizontal scaling; HTTP caching reduces load; authentication is handled via API keys, JWTs, or OAuth 2.0. Understanding REST mechanics makes you a better API designer and a faster debugger.

  • Problem it solves: Before REST, RPC-style APIs (SOAP, XML-RPC) were complex and tightly coupled to implementation details; REST’s uniform interface enables decoupled, scalable, and widely understood API design.
  • Alternatives: GraphQL for flexible client-specified queries; gRPC for high-performance binary RPC; tRPC for type-safe full-stack TypeScript APIs; WebSockets for bidirectional streaming.
  • Pros: Simple, supported by every HTTP client; human-readable with curl; easy to cache GET responses; no special client library needed.
  • Cons: Over-fetching and under-fetching are endemic; versioning REST APIs gracefully is hard; no standard for real-time updates.

HTTP Methods and Their Semantics

Method Meaning Idempotent? Safe?
GET Read a resource Yes Yes
POST Create a new resource No No
PUT Replace a resource Yes No
PATCH Partially update a resource No No
DELETE Remove a resource Yes No

Idempotent means calling the operation N times produces the same result as calling it once. GET, PUT, and DELETE are idempotent; POST is not - sending the same POST twice may create two records.

HTTP Status Codes

Know these cold:

Code Meaning
200 OK Success with a body
201 Created Resource was created (include Location header pointing to new resource)
204 No Content Success with no body (common for DELETE)
400 Bad Request Malformed request - the client sent something wrong
401 Unauthorized Not authenticated - provide valid credentials
403 Forbidden Authenticated but not authorized
404 Not Found Resource does not exist
422 Unprocessable Entity Structurally valid but semantically wrong (validation error)
429 Too Many Requests Rate limit exceeded
500 Internal Server Error The server failed - not the client’s fault
502 Bad Gateway Upstream service failed (reverse proxy got a bad response)
503 Service Unavailable Server is overloaded or in maintenance

Resource-Oriented URL Design

REST organises APIs around resources (nouns), not actions (verbs):

# Good: resource-oriented
GET    /users            # list users
POST   /users            # create a user
GET    /users/42         # get user 42
PUT    /users/42         # replace user 42
PATCH  /users/42         # update fields on user 42
DELETE /users/42         # delete user 42
GET    /users/42/posts   # list posts belonging to user 42

# Bad: action-oriented (RPC style)
GET /getUser?id=42
POST /createUser
POST /deleteUser?id=42

Authentication

API keys are the simplest mechanism - a long random token passed in the Authorization header:

Authorization: Bearer sk-abcdef123456

JWT (JSON Web Token) is a compact, self-contained token with three Base64URL-encoded parts separated by dots: header.payload.signature. The header declares the algorithm (HS256, RS256); the payload contains claims (sub, exp, iat, custom fields); the signature is computed over header + payload.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiI0MiIsImV4cCI6MTcyNTM5MzYwMH0
.xHsE8KfW2kKqbNn_abc123

The server verifies the signature without a database lookup - the token is stateless. Expiry (exp) is validated client-side by the receiving server.

OAuth 2.0 delegates authorization to an identity provider. The most common flow - Authorization Code - redirects the user to the IdP, which returns a short-lived code, which the app exchanges for an access token. Use this when your app acts on behalf of a user on a third-party service.

Rate Limiting and Backoff

Well-behaved APIs return 429 Too Many Requests with a Retry-After header. Well-behaved clients implement exponential backoff with jitter:

import time, random

def request_with_backoff(func, max_retries=5):
    for attempt in range(max_retries):
        response = func()
        if response.status_code == 429:
            wait = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(wait)
            continue
        return response
    raise Exception("Max retries exceeded")

Pagination

Offset-based: GET /users?offset=100&limit=50. Simple but inconsistent if rows are inserted during pagination.

Cursor-based: GET /users?after=eyJpZCI6MTAwfQ&limit=50. The cursor encodes the position in the result set (often a Base64-encoded ID or timestamp). Consistent, efficient, and the right choice for high-volume feeds.

The response includes the next cursor:

{
  "data": [...],
  "next_cursor": "eyJpZCI6MTUwfQ",
  "has_more": true
}

Python requests and httpx

import requests

# GET with query params and auth header
resp = requests.get(
    "https://api.example.com/users",
    params={"limit": 50, "after": cursor},
    headers={"Authorization": f"Bearer {token}"},
    timeout=10,
)
resp.raise_for_status()   # raises HTTPError on 4xx/5xx
data = resp.json()

# POST with JSON body
resp = requests.post(
    "https://api.example.com/users",
    json={"name": "Megha", "email": "m@example.com"},
    headers={"Authorization": f"Bearer {token}"},
    timeout=10,
)

httpx is a modern alternative with async support:

import httpx, asyncio

async def fetch():
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://api.example.com/users", timeout=10)
        return resp.json()

Examples

Call a REST API with pagination:

import requests

def fetch_all_users(base_url, token):
    users = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["after"] = cursor
        resp = requests.get(
            f"{base_url}/users",
            params=params,
            headers={"Authorization": f"Bearer {token}"},
            timeout=10,
        )
        resp.raise_for_status()
        body = resp.json()
        users.extend(body["data"])
        if not body.get("has_more"):
            break
        cursor = body["next_cursor"]
    return users

Design URL scheme for a blog API:

GET    /posts                    # list posts (with ?tag=, ?author=, ?limit=)
POST   /posts                    # create a post
GET    /posts/{slug}             # get a specific post by slug
PATCH  /posts/{slug}             # update title/body/tags
DELETE /posts/{slug}             # delete a post
GET    /posts/{slug}/comments    # list comments on a post
POST   /posts/{slug}/comments    # add a comment
DELETE /posts/{slug}/comments/{id}  # delete a specific comment

Inspect a raw HTTP exchange with curl:

curl -v -X POST https://api.example.com/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Megha", "email": "m@example.com"}'
# > shows request headers
# < shows response headers
# response body follows

REST is a set of constraints, not a specification. The discipline of resource-oriented design, correct status codes, and idempotent operations is what makes an API predictable and easy for clients to build on top of.


Read Next: