REST API Cheat Sheet

This REST API cheat sheet covers 29 sections: HTTP methods and PUT vs PATCH, URL design, status codes and problem+json errors, pagination and filtering, versioning, OAuth and JWT, the OWASP API Top 10, CORS, idempotency keys, rate limits, webhooks and OpenAPI 3.2, with clients and servers in six languages. Lint your endpoints, convert curl commands, infer schemas and decode JWTs without leaving the page.

29sections
138snippets
3.2OpenAPI
8API tools

Updated October 14, 2026, checked against RFC 9110, RFC 9457, RFC 10008, OpenAPI 3.2.0 and the OWASP API Security Top 10. Print it for a PDF copy.

/
level

{"data": []}

Nothing matches that search. Try a shorter keyword such as PATCH, cursor or JWT. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.

01

Toolbox & First Request

lint endpoints, convert curl, infer schemas, decode JWTs and make a first call

REST Toolbox: Endpoint Linter, curl Converter, JSON to Schema, JWT Decoder

core
Ctrl+Enter lints. Lint endpoints takes one METHOD /path per line, or OpenAPI paths. curl to code turns a curl command into fetch, Python requests, HTTPie and PowerShell. JSON to schema infers an OpenAPI 3.1 and 3.2 schema, with formats, from a sample response. Decode JWT shows the header, the claims and the expiry; it never checks the signature and never sends the token anywhere.
output

          
Paste endpoints, a curl command, a JSON response or a JWT above, then pick an action.

Builders: OpenAPI Resource, Pagination, Webhook Signatures, CORS

everyday
openapi.yaml

          

Anatomy of a REST Call

core

Method, resource, headers, body

POST /v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...
Content-Type: application/json
Accept: application/json
Idempotency-Key: 7d1f3c2e-5b9a-4c11-9a0e-2f6c8d4b1e90

{"customerId": "cus_42", "items": [{"sku": "LAMP-01", "qty": 1}]}

HTTP/1.1 201 Created
Location: /v1/orders/ord_8812
Content-Type: application/json

{"id": "ord_8812", "status": "pending", "total": 49.0, "createdAt": "2026-10-14T09:12:03Z"}

A First Call

core

GET with query parameters

const res = await fetch("https://httpbingo.org/anything/orders?status=paid&page=2", {
  headers: { Accept: "application/json" },
});
const body = await res.json();
console.log(res.status, body.method, body.url.split("?")[0]);
console.log(body.args);

POST JSON with curl

curl -s https://api.example.com/v1/orders \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customerId": "cus_42", "items": [{"sku": "LAMP-01", "qty": 1}]}'

curl --json and HTTPie

curl --json '{"customerId": "cus_42"}' https://api.example.com/v1/orders
http POST api.example.com/v1/orders customerId=cus_42 Authorization:"Bearer $TOKEN"
02

REST Principles

the six constraints, resources and the Richardson maturity model

The Six Constraints

core

What makes an API RESTful, per Roy Fielding (2000)

ConstraintIn practice
client-serverthe UI and the data store evolve separately
statelessevery request carries what the server needs; no session state between calls
cacheableresponses say whether and how long they may be cached
uniform interfaceresources with URLs, standard methods, self-descriptive messages, links
layered systemproxies, gateways and CDNs can sit in between unnoticed
code on demandoptional: the server can send code, such as JavaScript

Resources, Not Actions

core

Model nouns; let the method be the verb

GET    /orders              list orders
POST   /orders              create an order
GET    /orders/8812         read one order
PATCH  /orders/8812         change some fields
DELETE /orders/8812         remove it

Richardson Maturity Model

everyday

Four levels of REST

LevelNameLooks like
0the swamp of POXone URL, everything POSTed, like XML-RPC or SOAP
1resourcesseparate URLs per resource, still one method
2HTTP verbsGET, POST, PUT, DELETE and status codes used properly
3hypermedia controlsresponses contain links to related actions (HATEOAS)

Statelessness in Practice

everyday

Every request stands on its own

GET /v1/cart HTTP/1.1
Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...

REST is not CRUD over tables

DatabaseAPI
an orders table and an order_items tableone order resource that embeds its items
a status columnan action resource, POST /orders/8812/cancellation
a join tablea sub-collection, /teams/7/members
internal flags and audit columnsnot exposed at all
03

HTTP Methods

safe and idempotent methods, PUT vs PATCH, and POST for actions

Method Properties

core

Safe, idempotent, body

MethodSafeIdempotentRequest bodyTypical use
GETyesyesno meaning definedread a resource or a collection
HEADyesyesnoGET without the body
OPTIONSyesyesrareallowed methods, CORS preflight
QUERYyesyesyesa read with a complex query in the body (RFC 10008)
POSTnonoyescreate in a collection, or run an action
PUTnoyesyescreate or replace at a known URL
PATCHnono, unless designed soyeschange part of a resource
DELETEnoyesrareremove a resource

PUT vs PATCH vs POST

core

Replace, merge or create

PUT /users/42
{"name": "Ana", "email": "ana@example.com", "role": "admin"}
# the whole user is replaced: a field you leave out is removed or reset

PATCH /users/42
Content-Type: application/merge-patch+json
{"role": "admin"}
# only role changes

POST /users
{"name": "Ana", "email": "ana@example.com"}
# the server picks the ID and answers 201 with Location

GET With a Body, and QUERY

advanced

A safe search with a complex filter

QUERY /orders HTTP/1.1
Content-Type: application/json
Accept: application/json

{"status": ["paid", "shipped"], "total": {"gte": 100}, "createdAt": {"after": "2026-01-01"}}

HEAD and OPTIONS

everyday

Ask without downloading

HEAD /files/report.pdf HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 4812044
Last-Modified: Tue, 13 Oct 2026 08:00:00 GMT

OPTIONS /orders HTTP/1.1

HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS

Actions That Are Not CRUD

core

POST to a noun that names the action

POST /orders/8812/cancellation
POST /invoices/77/payments
POST /users/42/password-reset
POST /reports/exports

Method override for old clients

POST /orders/8812 HTTP/1.1
X-HTTP-Method-Override: DELETE
04

URL & Resource Design

nouns, plurals, nesting, casing and identifiers

Naming Rules

core

The conventions most APIs follow

RuleGoodAvoid
nouns, not verbsPOST /ordersPOST /createOrder
plural collections/orders/8812/order/8812
lower case, hyphens/order-items/OrderItems, /order_items
no file extensionsAccept: application/json/orders.json
no trailing slash/orders/orders/ and /orders both working differently
shallow nesting/orders/8812/items/customers/7/orders/8812/items/3/notes

Path or Query Parameter

core

Identity in the path, options in the query

GET /orders/8812                       one resource, identified by ID
GET /customers/7/orders                orders that belong to customer 7
GET /orders?customerId=7&status=paid   a filtered view of the collection
GET /orders?sort=-createdAt&limit=20   sorting and page size

Identifiers

everyday

IDs that do not leak or collide

KindExampleNotes
auto-increment8812short, but reveals volume and invites enumeration
UUIDv47d1f3c2e-5b9a-4c11-9a0e-2f6c8d4b1e90random, unguessable, sorts badly in indexes
UUIDv7019a2b3c-4d5e-7f60-8a1b-2c3d4e5f6a7btime-ordered (RFC 9562), index friendly
prefixedord_8812, cus_42the type is visible in logs and support tickets

Precision lost in JavaScript

const id = JSON.parse('{"id": 9007199254740993}').id;
console.log(id, Number.isSafeInteger(id));

Special Resources

everyday

Singletons, me and search

GET /users/me                      the authenticated user
GET /orders/8812/shipping-address  a singleton sub-resource, no ID
GET /search?q=lamp&type=product    a search across several resources
GET /health                        a status endpoint for load balancers

Long URLs and limits

LimitTypical value
browsers (Chrome)2 MB in the address bar, about 2,000 characters for sharing safely
nginx large_client_header_buffers8 KB per request line by default
AWS ALB16 KB request line
05

JSON & Data Conventions

field names, dates, money, nulls and envelopes

Field Names and Types

core

A response that is easy to consume

{
  "id": "ord_8812",
  "status": "paid",
  "total": {"amount": "49.00", "currency": "EUR"},
  "itemCount": 1,
  "isGift": false,
  "createdAt": "2026-10-14T09:12:03Z",
  "shippedAt": null
}

Dates and Times

core

RFC 3339 in UTC

const created = new Date(Date.UTC(2026, 9, 14, 9, 12, 3));
console.log(created.toISOString());
console.log(new Date("2026-10-14T11:12:03+02:00").toISOString());

null, Missing and Empty

everyday

Three different meanings

{"middleName": null}   the value is known to be empty
{}                     the field was not sent or not selected
{"tags": []}           an empty list; prefer it to null for collections

Envelopes

everyday

Wrap collections, not single resources

{"data": [{"id": "ord_8812"}, {"id": "ord_8813"}], "nextCursor": "eyJpZCI6ODgxM30"}

Enums, Booleans and Units

everyday

Values that read well and evolve safely

{
  "status": "partially_refunded",
  "isActive": true,
  "hasAttachments": false,
  "timeoutMs": 30000,
  "weightGrams": 1250,
  "country": "RO",
  "currency": "EUR",
  "locale": "ro-RO"
}

Field names across languages

StyleExampleCommon in
camelCasecreatedAtJavaScript, Java, most public JSON APIs
snake_casecreated_atPython, Ruby, PostgreSQL-backed APIs
kebab-casecreated-atURLs and headers, rarely JSON fields
06

Headers & Content Negotiation

Accept, Content-Type, Authorization, Prefer and custom headers

The Headers Every API Uses

core

Request and response headers

HeaderDirectionExample
Content-Typebothapplication/json
Acceptrequestapplication/json, application/problem+json
AuthorizationrequestBearer eyJhbGciOi...
Locationresponse/orders/8812
ETagresponse"v42"
If-Matchrequest"v42"
Retry-Afterresponse30
traceparentboth00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Content Negotiation

everyday

Ask for a format and a language

GET /reports/q3 HTTP/1.1
Accept: text/csv, application/json;q=0.8
Accept-Language: ro, en;q=0.7

HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Language: ro
Vary: Accept, Accept-Language

Prefer

advanced

Let the client choose how much comes back

PATCH /orders/8812 HTTP/1.1
Prefer: return=minimal

HTTP/1.1 204 No Content
Preference-Applied: return=minimal

Custom Headers

everyday

Name them without X-

Api-Version: 2026-10-01
Request-Id: req_7Hc2kQ

Request IDs

everyday

Return an ID the client can quote to support

HTTP/1.1 500 Internal Server Error
Request-Id: req_7Hc2kQf9
Content-Type: application/problem+json

{"title": "Internal Server Error", "status": 500, "instance": "/v1/orders", "requestId": "req_7Hc2kQf9"}

User-Agent for API Clients

advanced

Identify your integration

User-Agent: acme-billing-sync/2.3.1 (+https://acme.example/contact)
07

Status Codes in REST

the codes each method returns, at a glance

By Method

core

Success and the common failures

RequestSuccessCommon failures
GET /orders200400 bad filter, 401, 403
GET /orders/8812200, 304404, 410
POST /orders201 + Location, 202 if queued400, 409 duplicate, 415, 422
PUT /orders/8812200 or 204, 201 if created404, 409, 412, 422
PATCH /orders/8812200 or 204404, 409, 412, 415, 422
DELETE /orders/8812204404, 409 still in use
any request2xx as above429 rate limit, 500, 503

401 or 403, 400 or 422

core

The two debates that never end

QuestionAnswer
no token or an expired one401 with WWW-Authenticate
valid token, missing permission403
another tenant's resource404, so its existence is not revealed
body is not valid JSON400
valid JSON, invalid values422, or 400 if you prefer one code
08

Error Responses

RFC 9457 problem details, validation errors and error codes

Problem Details

core

The standard error body

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation",
  "title": "Your request is not valid.",
  "status": 422,
  "detail": "2 fields are invalid.",
  "instance": "/v1/orders",
  "errors": [
    {"pointer": "#/items/0/qty", "detail": "must be at least 1"},
    {"pointer": "#/customerId", "detail": "unknown customer"}
  ]
}

Machine-Readable Codes

everyday

Add a stable code for programs

{
  "type": "https://api.example.com/problems/card-declined",
  "title": "The card was declined.",
  "status": 402,
  "code": "card_declined",
  "declineCode": "insufficient_funds"
}

Error Handling Rules

everyday

What every error response should do

RuleWhy
use the right status codeclients, proxies and monitoring read the code first
same shape everywhereincluding 404 from the router and 500 from crashes
include a request IDsupport can find the log line
never include stack tracesthey leak internals; log them instead
list all validation errorsone round trip instead of one per field
document every typethe type URI should open a page explaining the fix

Read Errors in a Client

everyday

One handler for every problem+json response

import json

def describe(status, content_type, body):
    if content_type.startswith("application/problem+json"):
        p = json.loads(body)
        lines = [f"{status} {p.get('title', 'Error')}"]
        for e in p.get("errors", []):
            lines.append(f"  {e.get('pointer', '?')}: {e.get('detail', '')}")
        return "\n".join(lines)
    return f"{status} (no problem details)"

body = '{"type": "about:blank", "title": "Your request is not valid.", "status": 422, "errors": [{"pointer": "#/qty", "detail": "must be at least 1"}]}'
print(describe(422, "application/problem+json", body))
print(describe(502, "text/html", "<html>Bad gateway</html>"))
09

Pagination

offset, page and cursor pagination, Link headers and totals

Three Styles

core

Offset, page and cursor

GET /orders?offset=100&limit=50
GET /orders?page=3&per_page=50
GET /orders?limit=50&after=eyJpZCI6ODgxMn0

Compare them

Offset or pageCursor
jump to any pageyesno
stable when data changesnoyes
deep pagesslow (database scans skipped rows)fast with an index
total counteasy, but costly on big tablesusually omitted
used bymany admin UIs and older APIsStripe (starting_after), Slack (cursor), GitHub issues

Cursors Under the Hood

everyday

Keyset pagination with an opaque cursor

const last = { createdAt: "2026-10-14T09:12:03Z", id: "ord_8812" };
const cursor = Buffer.from(JSON.stringify(last)).toString("base64url");
console.log(cursor);
console.log(JSON.parse(Buffer.from(cursor, "base64url").toString()));

Link Headers

everyday

Navigation links in a header (RFC 8288)

HTTP/1.1 200 OK
Link: <https://api.example.com/orders?page=4&per_page=50>; rel="next", <https://api.example.com/orders?page=25&per_page=50>; rel="last", <https://api.example.com/orders?page=1&per_page=50>; rel="first", <https://api.example.com/orders?page=2&per_page=50>; rel="prev"

Parse a Link header

const header = '<https://api.example.com/orders?page=4>; rel="next", <https://api.example.com/orders?page=25>; rel="last"';
const links = Object.fromEntries(
  header.split(",").map((part) => {
    const [, url, rel] = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
    return [rel, url];
  })
);
console.log(links.next);
console.log(Object.keys(links));

Follow Pages in Code

everyday

requests parses Link headers for you

import requests

r = requests.get(
    "https://api.github.com/repos/nodejs/node/issues",
    params={"per_page": 2, "state": "closed"},
    headers={"Accept": "application/vnd.github+json", "User-Agent": "cheat-sheet-demo"},
    timeout=10,
)
print(r.status_code, len(r.json()))
print(sorted(r.links))
print(r.links["next"]["url"])

Fetch every page in JavaScript

async function* pages(url) {
  while (url) {
    const res = await fetch(url, { headers: { Accept: "application/json" } });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const body = await res.json();
    yield body.data;
    url = body.next ?? null;
  }
}

for await (const batch of pages("https://api.example.com/orders?limit=100")) {
  console.log(batch.length);
}

Page Size Limits

core

Defaults and maximums

GET /orders                 returns the default page, for example 20 items
GET /orders?limit=100       allowed
GET /orders?limit=100000    capped at the maximum, or rejected with 400
10

Filtering, Sorting & Fields

query parameter conventions for searching, ordering and sparse fieldsets

Filtering

core

Simple filters as plain query parameters

GET /orders?status=paid
GET /orders?status=paid,shipped
GET /orders?createdAfter=2026-01-01&totalMin=100
GET /orders?q=lamp

Bracket and operator styles

GET /orders?filter[status]=paid&filter[total][gte]=100
GET /orders?total[gte]=100&total[lt]=500

Sorting and Fields

core

Sort, select and expand

GET /orders?sort=-createdAt,total
GET /orders?fields=id,status,total
GET /orders/8812?expand=customer,items.product

Validate Filters

everyday

Reject what you do not support

GET /orders?statsu=paid HTTP/1.1

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{"title": "Unknown query parameter", "status": 400, "detail": "statsu is not a supported filter; did you mean status?"}

Dates, Ranges and Sorting Rules

everyday

Conventions worth writing down

NeedConvention
since a time?updatedAfter=2026-10-01T00:00:00Z
a closed range?createdAfter=...&createdBefore=...
several values?status=paid,shipped
sort descending?sort=-createdAt
stable sortalways add a unique tie-breaker such as id
11

Partial Updates

JSON Merge Patch, JSON Patch and when to use each

JSON Merge Patch (RFC 7396)

core

Send only what changes; null removes a field

PATCH /users/42 HTTP/1.1
Content-Type: application/merge-patch+json

{"email": "ana@new.example", "nickname": null, "address": {"city": "Cluj"}}

The algorithm, in eight lines

function mergePatch(target, patch) {
  if (patch === null || typeof patch !== "object" || Array.isArray(patch)) return patch;
  const out = target && typeof target === "object" && !Array.isArray(target) ? { ...target } : {};
  for (const [k, v] of Object.entries(patch)) {
    if (v === null) delete out[k];
    else out[k] = mergePatch(out[k], v);
  }
  return out;
}

const user = { email: "ana@example.com", nickname: "ana", address: { city: "Iasi", zip: "700000" }, tags: ["a", "b"] };
console.log(JSON.stringify(mergePatch(user, { email: "ana@new.example", nickname: null, address: { city: "Cluj" }, tags: ["c"] })));

JSON Patch (RFC 6902)

everyday

Explicit operations, including array edits

PATCH /users/42 HTTP/1.1
Content-Type: application/json-patch+json

[
  {"op": "test", "path": "/version", "value": 7},
  {"op": "replace", "path": "/email", "value": "ana@new.example"},
  {"op": "add", "path": "/tags/-", "value": "vip"},
  {"op": "remove", "path": "/nickname"}
]

PATCH Safely

everyday

Combine PATCH with If-Match

PATCH /orders/8812 HTTP/1.1
Content-Type: application/merge-patch+json
If-Match: "v41"

{"shippingAddress": {"city": "Cluj"}}

Which format to accept

FormatMedia typeGood for
JSON Merge Patchapplication/merge-patch+jsonmost updates of plain fields
JSON Patchapplication/json-patch+jsonarray edits, moves, test-and-set
plain JSONapplication/jsontreated as merge patch by many APIs; document it
12

Bulk & Batch Operations

creating many items, partial failures and batch endpoints

Bulk Create

everyday

One request, a result per item

POST /v1/products/bulk HTTP/1.1
Content-Type: application/json

{"items": [{"sku": "LAMP-01", "price": "49.00"}, {"sku": "LAMP-02", "price": "-1"}]}

HTTP/1.1 200 OK
Content-Type: application/json

{"results": [
  {"index": 0, "status": 201, "id": "prod_1"},
  {"index": 1, "status": 422, "error": {"pointer": "#/items/1/price", "detail": "must be positive"}}
]}

Batch Endpoints

advanced

Several operations in one request

POST /v1/batch HTTP/1.1
Content-Type: application/json

{"requests": [
  {"method": "GET", "url": "/v1/orders/8812"},
  {"method": "PATCH", "url": "/v1/orders/8813", "body": {"status": "shipped"}}
]}
14

Versioning & Deprecation

URL, header and date versions, breaking changes and sunset headers

Where the Version Lives

core

Four common strategies

GET /v1/orders                                   in the path
GET /orders  Api-Version: 2026-10-01              in a header, date based
GET /orders  Accept: application/vnd.example.v2+json   in the media type
GET /orders?api-version=2026-10-01               in the query (Azure style)

What Breaks Clients

core

Breaking and non-breaking changes

ChangeBreaking?
add an optional field to a responseno, if clients ignore unknown fields
add an optional request parameterno
add a new endpointno
remove or rename a fieldyes
change a field's type or formatyes
make an optional parameter requiredyes
add a value to an enumoften: strict clients reject unknown values
change a status code or error typeyes

Deprecation

everyday

Announce the end in headers

HTTP/1.1 200 OK
Deprecation: @1798761600
Sunset: Sat, 01 May 2027 00:00:00 GMT
Link: <https://api.example.com/docs/migrate-v2>; rel="deprecation"; type="text/html"

Clients That Survive Changes

everyday

Tolerant reading of enums

const LABELS = { pending: "Waiting", paid: "Paid", shipped: "On its way" };

function statusLabel(status) {
  return LABELS[status] ?? "Updated";
}

Changelogs

everyday

What every API change log entry needs

FieldExample
date2026-10-01
version2026-10-01 or v1.14
typeadded, changed, deprecated, removed, fixed, security
whatGET /orders accepts sort=-total
action needednone, or a migration note with a deadline
15

OpenAPI 3.2

describing an API: paths, schemas, security and what 3.2 added

A Minimal Document

core

One resource, described completely

openapi: 3.2.0
info:
  title: Orders API
  version: 1.4.0
servers:
  - url: https://api.example.com/v1
paths:
  /orders/{orderId}:
    get:
      operationId: getOrder
      summary: Get one order
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The order
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "404":
          description: No such order
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
components:
  schemas:
    Order:
      type: object
      required: [id, status]
      properties:
        id: { type: string, examples: ["ord_8812"] }
        status: { type: string, enum: [pending, paid, shipped] }
        createdAt: { type: string, format: date-time }
    Problem:
      type: object
      properties:
        type: { type: string, format: uri-reference }
        title: { type: string }
        status: { type: integer }

What 3.1 and 3.2 Changed

everyday

Versions you will meet

VersionReleasedKey changes
3.02017components, requestBody, servers, links, callbacks
3.1February 2021full JSON Schema 2020-12, webhooks, type: [string, "null"] instead of nullable
3.2September 2025query operation for the QUERY method, additionalOperations, $self, in: querystring, itemSchema for streams such as SSE and JSON Lines, nested tags with parent and kind, OAuth device authorization flow, oauth2MetadataUrl

Security Schemes

everyday

Bearer tokens and API keys

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
security:
  - bearerAuth: []

Tooling

everyday

Lint, mock, generate, document

npx @redocly/cli lint openapi.yaml
npx @stoplight/prism-cli mock openapi.yaml
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o ./client
docker run -p 8080:8080 -e SWAGGER_JSON=/spec/openapi.yaml -v "$PWD":/spec swaggerapi/swagger-ui

Reuse With $ref

everyday

Shared parameters and responses

components:
  parameters:
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
  responses:
    NotFound:
      description: Not found
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
paths:
  /orders:
    get:
      parameters:
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: OK }
        "404": { $ref: "#/components/responses/NotFound" }

Webhooks in OpenAPI

advanced

Describe what you send, not only what you receive

webhooks:
  paymentSucceeded:
    post:
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentEvent" }
      responses:
        "204":
          description: Received

Generate the Document From Code

everyday

Frameworks that write OpenAPI for you

StackHow
FastAPIbuilt in, at /openapi.json and /docs
Spring Bootspringdoc-openapi, at /v3/api-docs
ASP.NET Core 9 and laterbuilder.Services.AddOpenApi() and app.MapOpenApi()
NestJS@nestjs/swagger
Expresszod-openapi or tsoa, or write the YAML by hand
16

Design Checklist

the review list before an endpoint ships

Before an Endpoint Ships

core

Twelve questions

AreaQuestion
namingplural nouns, consistent casing, no verbs in the path?
methodsright method, safe and idempotent where they should be?
status codesspecific codes, 201 with Location, 204 without a body?
errorsproblem+json, every validation error listed?
authownership checked on every object ID?
inputvalidated against a schema, writable fields allow-listed?
paginationdefault and maximum page size, stable ordering?
formatsRFC 3339 dates, string IDs, money with currency?
compatibilityadditive change only, or a new version?
limitsrate limits and payload limits in place and documented?
docsin the OpenAPI document, with examples?
observabilityrequest ID returned, errors logged with context?

Common Mistakes

core

Seen in most API reviews

MistakeFix
verbs in URLsnouns plus methods, or action resources
200 for errorsreal status codes plus problem+json
no paginationdefault and maximum page sizes from day one
numeric IDs over 2^53 as numberssend IDs as strings
no ownership checksauthorize every object access
no timeouts in clientsset connect and read timeouts
breaking changes in placeadditive changes, or a new version with a sunset date
17

Authentication

API keys, bearer tokens, OAuth 2.0 flows and mutual TLS

Choose an Auth Method

core

Who is calling decides the method

CallerMethod
your own server-to-server jobOAuth 2.0 client credentials, or an API key
third-party developer acting for themselvesAPI key or personal access token
app acting for a userOAuth 2.0 authorization code with PKCE
single page app or mobile appauthorization code with PKCE, no client secret
partner with strict requirementsmutual TLS, alone or binding tokens to the certificate

Sending Credentials

core

Headers, never the URL

GET /v1/orders HTTP/1.1
Authorization: Bearer eyJhbGciOiJFUzI1NiIsImtpZCI6ImsxIn0...

GET /v1/orders HTTP/1.1
X-Api-Key: sk_live_a1b2c3...

Client Credentials Flow

everyday

A server gets a token for itself

POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=

grant_type=client_credentials&scope=orders:read

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{"access_token": "eyJhbGciOiJFUzI1NiJ9...", "token_type": "Bearer", "expires_in": 3600, "scope": "orders:read"}

Authorization Code With PKCE

advanced

Make the verifier and the challenge

const crypto = require("crypto");
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
console.log(verifier.length, challenge.length, /^[A-Za-z0-9_-]+$/.test(challenge));

Basic Authentication

everyday

What the header really contains

const header = "Basic " + Buffer.from("client_id:client_secret").toString("base64");
console.log(header);
console.log(Buffer.from(header.slice(6), "base64").toString());

Storing API Keys

advanced

Keep only a hash on the server

StepDo
creategenerate 32 random bytes, show the full key to the user once
storekeep a SHA-256 hash and a short prefix such as sk_live_a1b2 for display
checkhash the incoming key and look up the hash
rotateallow two active keys per client so they can switch without downtime

Mutual TLS

advanced

A client certificate instead of a secret

curl --cert client.pem --key client-key.pem https://partner-api.example.com/v1/orders
18

JWT & Access Tokens

structure, claims, validation and common mistakes

JWT Structure

core

Three base64url parts, separated by dots

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const [header, payload] = token.split(".").slice(0, 2).map((p) => JSON.parse(Buffer.from(p, "base64url").toString()));
console.log(header);
console.log(payload);

Validate Every Token

core

The checks a server must make

CheckReject when
signatureit does not verify with the issuer's key
algnot the algorithm you expect; never accept none
expthe expiry time has passed (allow a minute of clock skew)
nbfthe token is not valid yet
issthe issuer is not the one you trust
audyour API is not the intended audience
scope or rolesthe token lacks the permission for this endpoint

Lifetimes and Storage

everyday

Short access tokens, rotated refresh tokens

TokenTypical lifetimeStore in
access token5 to 60 minutesmemory, or an HttpOnly cookie for web apps
refresh tokendays to weeks, rotated on useHttpOnly Secure SameSite cookie, or the OS keychain on mobile
API keyuntil revokeda secret manager, never in source code

Verify a Signature by Hand

advanced

HS256 is HMAC-SHA256 over header.payload

const crypto = require("crypto");
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const [h, p, sig] = token.split(".");
const expected = crypto.createHmac("sha256", "your-256-bit-secret").update(`${h}.${p}`).digest("base64url");
console.log(expected === sig);

Verify with the jose library

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));

const { payload } = await jwtVerify(token, JWKS, {
  issuer: "https://auth.example.com/",
  audience: "https://api.example.com",
  algorithms: ["ES256", "RS256"],
});
19

API Security

the OWASP API Security Top 10 and the checks that prevent each risk

OWASP API Security Top 10, 2023

core

The current list and the fix for each

IDRiskPrevent it with
API1Broken Object Level Authorizationcheck that the caller owns or may access every object ID
API2Broken Authenticationstandard token validation, rate limits on login and reset
API3Broken Object Property Level Authorizationallow-lists for readable and writable fields
API4Unrestricted Resource Consumptionrate limits, page size caps, timeouts, upload limits
API5Broken Function Level Authorizationdeny by default; check roles on every admin endpoint
API6Unrestricted Access to Sensitive Business Flowsbot protection and limits on flows such as checkout
API7Server Side Request Forgeryvalidate and allow-list URLs the API fetches
API8Security Misconfigurationsecure defaults, no verbose errors, current TLS, CORS allow-lists
API9Improper Inventory Managementdocument and retire old versions and test endpoints
API10Unsafe Consumption of APIsvalidate data from third-party APIs like user input

Broken Object Level Authorization

core

The most common API vulnerability

app.get("/orders/:id", requireAuth, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order || order.customerId !== req.user.customerId) {
    return res.status(404).json({ title: "Not Found", status: 404 });
  }
  res.json(order);
});

Mass Assignment

everyday

Never bind the request body straight to the model

const ALLOWED = ["name", "email", "address"];
const changes = Object.fromEntries(Object.entries(req.body).filter(([k]) => ALLOWED.includes(k)));
await db.users.update(req.user.id, changes);

Transport and Inputs

everyday

Baseline rules

AreaRule
TLSHTTPS only; redirect or reject plain HTTP, send Strict-Transport-Security
inputvalidate types, lengths and formats against a schema before using them
outputreturn only fields the caller may see, never whole database rows
limitscap page sizes, body sizes and request rates per client
secretskeys in a secret manager, rotated, never in Git or logs
logginglog who did what, never tokens or passwords

SSRF

advanced

Fetching a URL a client gave you

const allowed = new Set(["images.example-cdn.com", "uploads.example.com"]);

function checkUrl(input) {
  const url = new URL(input);
  if (url.protocol !== "https:" || !allowed.has(url.hostname)) {
    throw new Error("URL not allowed");
  }
  return url;
}

Rate Limit Authentication

everyday

Slow down guessing

POST /oauth/token HTTP/1.1

HTTP/1.1 429 Too Many Requests
Retry-After: 60
20

CORS

preflight requests, the headers and the mistakes behind CORS errors

How CORS Works

core

A preflight, then the real request

OPTIONS /v1/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: authorization, content-type

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin

The Rules People Trip Over

core

Why CORS errors happen

MistakeEffect
Access-Control-Allow-Origin: * with credentialsbrowsers refuse it; echo the specific origin instead
missing Vary: Origin when echoing originsa CDN serves one origin's header to another
preflight hits auth middleware and gets 401answer OPTIONS before authentication
header missing on error responsesthe browser hides the real 4xx or 5xx behind a CORS error
allowing any origin that contains example.comevil-example.com passes; compare exact origins

Simple and Preflighted Requests

core

Which requests trigger a preflight

RequestPreflight?
GET or HEAD with ordinary headersno
POST with form or text/plain bodyno
POST with Content-Type: application/jsonyes
any request with Authorization or a custom headeryes
PUT, PATCH or DELETEyes

Debug CORS With curl

everyday

Send a preflight by hand

curl -s -o /dev/null -D - -X OPTIONS https://httpbingo.org/anything -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: PATCH" -H "Access-Control-Request-Headers: authorization" | grep -i "^access-control"
21

Caching & Concurrency

Cache-Control for APIs, ETags, 304 and optimistic locking with If-Match

Cache-Control for APIs

core

Pick one per endpoint

ResponseCache-Control
public reference data, same for everyonepublic, max-age=3600
data for one userprivate, max-age=0, must-revalidate
tokens, secrets, personal datano-store
data that changes often, revalidate cheaplyno-cache, with an ETag

Optimistic Concurrency

everyday

Prevent lost updates with ETag and If-Match

GET /docs/7 HTTP/1.1

HTTP/1.1 200 OK
ETag: "v41"

PUT /docs/7 HTTP/1.1
If-Match: "v41"
Content-Type: application/json

{"title": "New title", "body": "..."}

HTTP/1.1 412 Precondition Failed

Generating ETags

everyday

A strong ETag from the body

const crypto = require("crypto");
const body = JSON.stringify({ id: "ord_8812", status: "paid" });
const etag = '"' + crypto.createHash("sha256").update(body).digest("base64url").slice(0, 16) + '"';
console.log(etag);

Conditional Requests From a Client

everyday

Revalidate instead of downloading again

let cached = null;

async function getConfig() {
  const res = await fetch("/api/config", {
    headers: cached ? { "If-None-Match": cached.etag } : {},
  });
  if (res.status === 304) return cached.data;
  cached = { etag: res.headers.get("etag"), data: await res.json() };
  return cached.data;
}
22

Idempotency & Retries

idempotency keys, safe retries and exactly-once effects

Idempotency Keys

core

Make POST safe to retry

POST /v1/payments HTTP/1.1
Idempotency-Key: 5c1d2a5e-3f0b-4d7e-9c8a-6b2f1e0d4c3b
Content-Type: application/json

{"amount": "49.00", "currency": "EUR", "source": "card_123"}

Server side, in short

const cached = await store.get(key);
if (cached) return res.status(cached.status).json(cached.body);
const result = await createPayment(req.body);
await store.set(key, { status: 201, body: result }, { ttlSeconds: 86400 });
res.status(201).json(result);

Retry Policy

everyday

What a client may retry

SituationRetry?
network error or timeout on GET, PUT, DELETEyes, they are idempotent
same on POST or PATCHonly with an idempotency key
429 or 503yes, after Retry-After
502 or 504yes, with backoff
other 4xxno, fix the request

Generate Keys on the Client

everyday

One key per logical operation

const key = crypto.randomUUID();
console.log(key.length, /^[0-9a-f-]{36}$/.test(key), key.charAt(14));
23

Rate Limiting

algorithms, headers and what clients should do

Algorithms

everyday

Four common ways to count

AlgorithmBehavior
fixed windowN requests per clock minute; allows bursts at window edges
sliding windowN requests in any 60 seconds; smoother, more state
token buckettokens refill at a steady rate, bursts up to the bucket size
leaky bucketrequests leave at a constant rate, extras queue or drop

The 429 Response

core

Tell the client how long to wait

HTTP/1.1 429 Too Many Requests
Retry-After: 21
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=0;t=21
Content-Type: application/problem+json

{"title": "Too many requests", "status": 429, "detail": "100 requests per minute allowed"}

A Token Bucket in a Few Lines

advanced

Bursts allowed, average rate enforced

function bucket(capacity, refillPerSec) {
  let tokens = capacity, last = 0;
  return (nowMs) => {
    tokens = Math.min(capacity, tokens + ((nowMs - last) / 1000) * refillPerSec);
    last = nowMs;
    if (tokens < 1) return false;
    tokens -= 1;
    return true;
  };
}

const allow = bucket(5, 1);
const burst = [0, 10, 20, 30, 40, 50, 60].map((t) => allow(t));
console.log(burst.join(" "));
console.log(allow(2000), allow(2010), allow(2020));

Handle 429 in a Client

everyday

Retry with urllib3 and requests

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(total=5, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504])
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))
r = session.get("https://api.example.com/v1/orders", timeout=10)
24

Async Jobs & Webhooks

202 and polling, webhook delivery and signature verification

Long-Running Operations

core

Accept, then let the client poll

POST /v1/exports HTTP/1.1

HTTP/1.1 202 Accepted
Location: /v1/exports/exp_31

GET /v1/exports/exp_31 HTTP/1.1

HTTP/1.1 200 OK
{"id": "exp_31", "status": "running", "progress": 0.6}

GET /v1/exports/exp_31 HTTP/1.1

HTTP/1.1 200 OK
{"id": "exp_31", "status": "done", "download": "https://files.example.com/exp_31.csv?sig=..."}

Webhooks

core

A delivery from the provider

POST /webhooks/payments HTTP/1.1
Content-Type: application/json
X-Signature: sha256=1f0e3a9c...
X-Timestamp: 1792056723

{"id": "evt_901", "type": "payment.succeeded", "data": {"paymentId": "pay_77"}}

Verify the HMAC signature

const crypto = require("crypto");
const secret = "whsec_test_123";
const body = '{"id":"evt_901","type":"payment.succeeded"}';
const timestamp = "1792056723";
const sent = "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");

function verify(rawBody, header, ts) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return header.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
console.log(verify(body, sent, timestamp));
console.log(verify(body.replace("succeeded", "failed"), sent, timestamp));

Progress With Server-Sent Events

advanced

Stream updates instead of polling

GET /v1/exports/exp_31/events HTTP/1.1
Accept: text/event-stream

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: progress
data: {"progress": 0.6}

event: done
data: {"download": "https://files.example.com/exp_31.csv"}

Webhook Delivery Rules

everyday

What providers and receivers agree on

RuleTypical value
receiver must answer within5 to 30 seconds
successany 2xx
retriesexponential backoff over hours or days, such as 1 m, 5 m, 30 m, 2 h, 12 h
duplicatespossible: deduplicate on the event ID
orderingnot guaranteed: use timestamps or fetch the current state
securityHMAC signature, timestamp check, HTTPS only
25

File Uploads & Downloads

multipart uploads, presigned URLs and downloads

Uploads

core

multipart/form-data

curl -F "file=@invoice.pdf;type=application/pdf" -F "orderId=ord_8812" https://api.example.com/v1/documents

Presigned URLs for large files

POST /v1/uploads HTTP/1.1
Content-Type: application/json

{"filename": "video.mp4", "size": 734003200, "contentType": "video/mp4"}

HTTP/1.1 201 Created
Content-Type: application/json

{"uploadUrl": "https://bucket.s3.amazonaws.com/u/91?X-Amz-Signature=...", "expiresIn": 900, "fileId": "file_91"}

Downloads

everyday

Name the file and support ranges

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice-8812.pdf"
Content-Length: 48211
Accept-Ranges: bytes

Upload From a Browser

everyday

FormData with fetch

const form = new FormData();
form.append("file", input.files[0]);
form.append("orderId", "ord_8812");

const res = await fetch("/v1/documents", { method: "POST", body: form });

Resumable Uploads

advanced

Large files over unreliable networks

ApproachNotes
tus protocolopen protocol with clients and servers for many languages
IETF resumable uploads draftstandardizes the same idea, including the 104 status code
S3 multipart uploadparts of 5 MB to 5 GB, uploaded in parallel and completed in one call
26

Calling APIs From Code

fetch, Python, Java, Go, C# and PowerShell side by side

POST JSON and Read the Response

core

fetch, in browsers and Node.js 18+

const res = await fetch("https://httpbingo.org/post", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({ customerId: "cus_42", qty: 1 }),
  signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const echo = await res.json();
console.log(res.status, echo.json);

requests

import requests

r = requests.post(
    "https://httpbingo.org/post",
    json={"customerId": "cus_42", "qty": 1},
    timeout=10,
)
r.raise_for_status()
print(r.status_code, r.json()["json"])

java.net.http.HttpClient, Java 11+

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.example.com/v1/orders"))
    .timeout(Duration.ofSeconds(10))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"customerId\":\"cus_42\",\"qty\":1}"))
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());

net/http

body := strings.NewReader(`{"customerId":"cus_42","qty":1}`)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post("https://api.example.com/v1/orders", "application/json", body)
if err != nil {
	return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
	return fmt.Errorf("unexpected status %d", resp.StatusCode)
}

HttpClient and System.Net.Http.Json

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var response = await http.PostAsJsonAsync("https://api.example.com/v1/orders", new { customerId = "cus_42", qty = 1 });
response.EnsureSuccessStatusCode();
var order = await response.Content.ReadFromJsonAsync<Order>();

Invoke-RestMethod

$body = @{ customerId = "cus_42"; qty = 1 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://api.example.com/v1/orders" `
  -ContentType "application/json" -Body $body `
  -Headers @{ Authorization = "Bearer $env:TOKEN" }

Timeouts and Retries

core

Defaults that bite

ClientDefault timeout
fetch (browsers and Node.js)none in the spec; Node.js stops waiting for headers after 300 seconds
Python requestsnone
Python httpx5 seconds
Go http.Clientnone
Java HttpClientnone
27

Building APIs

a CRUD resource in six frameworks

A CRUD Resource

core

Express 5

const express = require("express");
const app = express();
app.use(express.json());

app.get("/orders", async (req, res) => res.json({ data: await db.orders.list(req.query) }));
app.get("/orders/:id", async (req, res) => {
  const order = await db.orders.get(req.params.id);
  if (!order) return res.status(404).json({ title: "Not Found", status: 404 });
  res.json(order);
});
app.post("/orders", async (req, res) => {
  const order = await db.orders.create(req.body);
  res.status(201).location(`/orders/${order.id}`).json(order);
});
app.delete("/orders/:id", async (req, res) => { await db.orders.remove(req.params.id); res.sendStatus(204); });
app.listen(3000);

FastAPI

from fastapi import FastAPI, HTTPException, Response, status
from pydantic import BaseModel

app = FastAPI()

class OrderIn(BaseModel):
    customer_id: str
    qty: int = 1

@app.get("/orders/{order_id}")
def get_order(order_id: str):
    order = db.get(order_id)
    if order is None:
        raise HTTPException(status_code=404, detail="Order not found")
    return order

@app.post("/orders", status_code=status.HTTP_201_CREATED)
def create_order(order: OrderIn, response: Response):
    saved = db.create(order)
    response.headers["Location"] = f"/orders/{saved.id}"
    return saved

Spring Boot

@RestController
@RequestMapping("/orders")
class OrderController {
    private final OrderRepository repository;
    OrderController(OrderRepository repository) { this.repository = repository; }

    @GetMapping("/{id}")
    Order get(@PathVariable String id) {
        return repository.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @PostMapping
    ResponseEntity<Order> create(@Valid @RequestBody OrderRequest body) {
        Order saved = repository.save(body.toOrder());
        return ResponseEntity.created(URI.create("/orders/" + saved.id())).body(saved);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    void delete(@PathVariable String id) { repository.deleteById(id); }
}

ASP.NET Core minimal API

var orders = app.MapGroup("/orders");
orders.MapGet("/{id}", async (string id, AppDb db) =>
    await db.Orders.FindAsync(id) is { } order ? Results.Ok(order) : Results.NotFound());
orders.MapPost("/", async (OrderIn input, AppDb db) =>
{
    var order = db.Orders.Add(input.ToOrder()).Entity;
    await db.SaveChangesAsync();
    return TypedResults.Created($"/orders/{order.Id}", order);
});
orders.MapDelete("/{id}", async (string id, AppDb db) =>
{
    await db.Orders.Where(o => o.Id == id).ExecuteDeleteAsync();
    return Results.NoContent();
});

Go 1.22+ net/http

mux := http.NewServeMux()
mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
	order, ok := store.Get(r.PathValue("id"))
	if !ok {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(order)
})
mux.HandleFunc("DELETE /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
	store.Delete(r.PathValue("id"))
	w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", mux)

Laravel

Route::apiResource('orders', OrderController::class);

class OrderController extends Controller
{
    public function show(Order $order) { return $order; }

    public function store(StoreOrderRequest $request)
    {
        $order = Order::create($request->validated());
        return response()->json($order, 201)->header('Location', "/api/orders/{$order->id}");
    }

    public function destroy(Order $order) { $order->delete(); return response()->noContent(); }
}
28

Testing & Debugging

curl -v, HTTPie, Postman, mocks and contract tests

See Exactly What Was Sent

core

curl -v

curl -sv -o /dev/null "https://httpbingo.org/status/204" 2>&1 | grep -E "^(> [A-Z]{3,7} |> Accept|< HTTP)"

Collections and Contract Tests

everyday

Run a Postman collection in CI

newman run orders.postman_collection.json -e staging.postman_environment.json --reporters cli,junit

Fuzz an API against its own OpenAPI document

schemathesis run https://api.example.com/openapi.json

Consumer-Driven Contracts

advanced

Pact in one paragraph

StepWhat happens
consumer testthe client's tests record the requests it makes and the responses it expects
pact filethose expectations are saved as a contract
provider verificationthe API's CI replays the contract against the real service
brokershares contracts and tells you whether a version can be deployed
29

REST vs GraphQL vs gRPC

choosing an API style

Side by Side

core

Three API styles

RESTGraphQLgRPC
transportHTTP/1.1, 2 or 3HTTP, one endpointHTTP/2
formatJSON, usuallyJSONProtocol Buffers, binary
contractOpenAPI, optionalschema, required.proto files, required
cachingHTTP caching worksmostly client sidenone built in
over-fetchingcommon without fieldsthe client picks fieldsfixed messages
browsersnativenativeneeds gRPC-Web or a proxy
best forpublic APIs, CRUD, cachingmany clients with different data needsinternal service-to-service calls

Older Styles You Still Meet

advanced

SOAP and RPC over HTTP

StyleTraits
SOAPXML envelopes, WSDL contracts, WS-Security; still common in banking, government and enterprise integrations
JSON-RPCone endpoint, method names in the body; used by Ethereum nodes and the Language Server Protocol
XML-RPCthe predecessor of SOAP; WordPress still exposes xmlrpc.php

What Is a REST API Cheat Sheet

A REST API cheat sheet is a compact reference to the conventions behind web APIs: which HTTP method to use, how to name URLs, which status code and error body to return, how to paginate, version and secure an API, and how to describe it in OpenAPI. People also search for REST API best practices or a RESTful API design guide; they want the same thing, usually while designing a new endpoint or reviewing someone else's.

This cheat sheet covers 29 sections and 138 snippets, from the first curl call to OAuth flows, webhooks and OpenAPI 3.2. The output under the Node.js, Python and curl examples is what they printed when run on October 14, 2026, mostly against httpbingo.org, a public service that echoes requests back.

The toolbox at the top lints a list of endpoints against common URL design rules and suggests rewrites, converts a curl command into fetch, Python requests, HTTPie and PowerShell, infers an OpenAPI schema from a sample JSON response, and decodes a JWT with checks for its algorithm, expiry and claims. The builders write a complete OpenAPI resource with CRUD operations, pagination headers, webhook signature verification in four languages and CORS configuration for five servers. Everything runs in your browser, and nothing you paste is sent anywhere.

How It Differs From a Style Guide

Style guides from large API providers set rules for one company; this page collects the conventions that most of them share and shows the standard behind each one, from RFC 9457 error bodies to RFC 7396 merge patches. For every status code in detail, see the HTTP status codes cheat sheet; for the payload format, the JSON cheat sheet.


What This REST API Cheat Sheet Covers

The 29 sections follow the life of an API, and each link jumps to that section above.


REST API Quick Reference

The decisions people look up most often, one line each.

TaskConvention
List a collectionGET /orders
Read one itemGET /orders/8812
CreatePOST /orders, answer 201 plus Location
ReplacePUT /orders/8812
Change some fieldsPATCH with application/merge-patch+json
DeleteDELETE /orders/8812, answer 204
An actionPOST /orders/8812/refunds
Filter?status=paid&createdAfter=2026-01-01
Sort?sort=-createdAt
Paginate?limit=50&after=cursor plus Link: rel="next"
Version/v1/ in the path, or an Api-Version header
AuthenticateAuthorization: Bearer <token>
Error bodyapplication/problem+json (RFC 9457)
Safe retries of POSTIdempotency-Key header
Prevent lost updatesETag plus If-Match, 412 on conflict
Rate limited429 plus Retry-After
DatesRFC 3339 in UTC: 2026-10-14T09:12:03Z
Describe itOpenAPI 3.2 or 3.1

What REST Is

REST, short for Representational State Transfer, is the architectural style Roy Fielding described in his 2000 doctoral dissertation, distilled from the design of HTTP itself. A REST API exposes resources at URLs and lets clients read and change them through a small, uniform set of methods, exchanging representations of those resources, today almost always JSON.

REST is not a standard with a specification you can validate against. What exists are the HTTP RFCs, which define methods, status codes, caching and headers, and a set of conventions most public APIs share. That is why two REST APIs can look quite different, and why consistency within one API matters more than any single rule.

REST APIs power most public web services, mobile app backends and integrations between companies. They are the default boundary in back-end development, the usual subject of API integration work, and a common way for microservices to talk to each other.

Where It Fits

REST is the safe default for public APIs and CRUD-heavy services: every language has an HTTP client, every proxy understands it, and HTTP caching works out of the box. GraphQL fits apps whose screens need very different slices of data, and gRPC fits high-volume calls between internal services; the comparison section above has the details.


How a REST Request Is Handled

Following one request through a typical stack explains where each convention on this page is enforced.

  1. The client builds the request: method, URL, headers such as Authorization and Content-Type, and a JSON body.
  2. A gateway or load balancer terminates TLS, applies rate limits and CORS, and routes by path or version.
  3. The framework matches the route, answering 404 or 405 when nothing matches.
  4. Middleware authenticates the token (401) and checks permissions and object ownership (403 or 404).
  5. The body is validated against a schema, and invalid input gets a problem+json response (400 or 422).
  6. The handler runs business logic, checks preconditions such as If-Match (412) and conflicts (409), and writes to the database.
  7. The response gets a status code, headers such as Location, ETag and caching rules, and a JSON body.

Common REST API Mistakes

The same problems show up in most API reviews.

In Design

  • Verbs in URLs, such as /getUser or /createOrder, instead of methods on resources.
  • Returning 200 with an error in the body instead of a real status code.
  • Breaking clients by renaming or removing fields without a new version.
  • Numeric IDs above 2^53 sent as JSON numbers, which JavaScript clients corrupt.
  • Unbounded list endpoints without pagination or a maximum page size.

In Security and Operations

  • Checking that a user is logged in but not that they own the object they ask for.
  • Binding request bodies straight to database models, so clients can set fields such as role.
  • Putting API keys in query strings, where they end up in logs.
  • Retrying POST requests without an idempotency key and creating duplicates.
  • No timeouts in API clients, so one slow dependency hangs everything.

How to Build Good REST APIs

Design First

Write the OpenAPI document before the code and review it like code: naming, status codes, errors and pagination are cheap to change in YAML and expensive after clients depend on them. Lint the document in continuous integration and generate mock servers so front-end work can start early.

Then Test and Watch It

Cover every endpoint with tests that assert status codes, headers and response schemas, as the guide to types of software testing describes, and fuzz the API against its own OpenAPI document. Keep secrets out of your repositories, as the guide on hiding an API key in GitHub shows, and monitor error rates per endpoint after each deployment. The JavaScript cheat sheet and Python cheat sheet cover the languages most API clients are written in.


How This REST API Cheat Sheet Is Maintained

This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked on October 14, 2026 against RFC 9110, RFC 9457, RFC 7396, RFC 10008, the OpenAPI 3.2.0 specification and the OWASP API Security Top 10 2023. The output of the examples was produced by running them with Node.js 24, Python 3.14 and curl 8.19. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.


REST API Questions People Actually Ask

The questions developers search for most about REST APIs, answered in a few sentences each.

What is a REST API?

A REST API is a web API that follows the Representational State Transfer style described by Roy Fielding in 2000: resources identified by URLs, manipulated with standard HTTP methods such as GET, POST, PUT, PATCH and DELETE, stateless requests and cacheable responses. Most REST APIs exchange JSON over HTTPS.

What is the difference between PUT and PATCH?

PUT replaces the whole resource with the representation you send and is idempotent, so fields you leave out are removed or reset. PATCH changes only part of a resource; with JSON Merge Patch you send just the fields that change, and null deletes a field.

Which status code should a POST request return?

201 Created with a Location header when the POST creates a resource, 202 Accepted when the work is queued and finishes later, and 200 when the POST runs an action that returns a result. Validation failures return 400 or 422.

How should a REST API be versioned?

The most common choice is a version in the path, such as /v1/orders, because it is easy to see, route and cache. Header versioning with a date, as Stripe does, lets each client stay pinned to the behavior it was built against. Whatever you choose, prefer additive changes and announce removals with Deprecation and Sunset headers.

What is the best way to paginate an API?

Use cursor pagination for large or fast-changing collections and feeds: it is stable and fast at any depth. Use page or offset pagination when users need to jump to a specific page. In both cases return links to the next page, either in the body or in a Link header.

How do I secure a REST API?

Use HTTPS only, authenticate every request with OAuth 2.0 access tokens or API keys sent in headers, check that the caller may access each object it asks for, validate input against a schema, allow-list writable fields, rate limit per client and never return stack traces. The OWASP API Security Top 10 lists the most common failures.

What is the difference between REST and GraphQL?

REST exposes many resource URLs with fixed response shapes and works with standard HTTP caching. GraphQL exposes one endpoint where the client asks for exactly the fields it needs in a query. REST suits public and CRUD APIs; GraphQL suits apps with many clients that each need different data.

What is OpenAPI?

OpenAPI is the standard format for describing HTTP APIs in YAML or JSON: paths, operations, parameters, request and response schemas and security. Tools use the document to generate documentation, client libraries, mock servers and tests. The current version is 3.2.0, released in September 2025.

Is this REST API cheat sheet up to date?

Yes. It was checked on October 14, 2026 against RFC 9110, RFC 9457 for error responses, RFC 10008 for the QUERY method, OpenAPI 3.2.0 and the OWASP API Security Top 10 2023. The example outputs come from real Node.js 24, Python 3.14 and curl 8.19 runs.

Can I download this REST API cheat sheet as a PDF?

Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy with every output and explanation expanded and the navigation left out. The print core button prints only the core snippets for a short reference.