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.
{"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.
Toolbox & First Request
lint endpoints, convert curl, infer schemas, decode JWTs and make a first call
Builders: OpenAPI Resource, Pagination, Webhook Signatures, CORS
Anatomy of a REST Call
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
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"
REST Principles
the six constraints, resources and the Richardson maturity model
The Six Constraints
What makes an API RESTful, per Roy Fielding (2000)
| Constraint | In practice |
|---|---|
| client-server | the UI and the data store evolve separately |
| stateless | every request carries what the server needs; no session state between calls |
| cacheable | responses say whether and how long they may be cached |
| uniform interface | resources with URLs, standard methods, self-descriptive messages, links |
| layered system | proxies, gateways and CDNs can sit in between unnoticed |
| code on demand | optional: the server can send code, such as JavaScript |
Resources, Not Actions
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
Four levels of REST
| Level | Name | Looks like |
|---|---|---|
| 0 | the swamp of POX | one URL, everything POSTed, like XML-RPC or SOAP |
| 1 | resources | separate URLs per resource, still one method |
| 2 | HTTP verbs | GET, POST, PUT, DELETE and status codes used properly |
| 3 | hypermedia controls | responses contain links to related actions (HATEOAS) |
Statelessness in Practice
Every request stands on its own
GET /v1/cart HTTP/1.1 Authorization: Bearer eyJhbGciOiJFUzI1NiJ9...
REST is not CRUD over tables
| Database | API |
|---|---|
| an orders table and an order_items table | one order resource that embeds its items |
| a status column | an action resource, POST /orders/8812/cancellation |
| a join table | a sub-collection, /teams/7/members |
| internal flags and audit columns | not exposed at all |
HTTP Methods
safe and idempotent methods, PUT vs PATCH, and POST for actions
Method Properties
Safe, idempotent, body
| Method | Safe | Idempotent | Request body | Typical use |
|---|---|---|---|---|
| GET | yes | yes | no meaning defined | read a resource or a collection |
| HEAD | yes | yes | no | GET without the body |
| OPTIONS | yes | yes | rare | allowed methods, CORS preflight |
| QUERY | yes | yes | yes | a read with a complex query in the body (RFC 10008) |
| POST | no | no | yes | create in a collection, or run an action |
| PUT | no | yes | yes | create or replace at a known URL |
| PATCH | no | no, unless designed so | yes | change part of a resource |
| DELETE | no | yes | rare | remove a resource |
PUT vs PATCH vs POST
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
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
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
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
URL & Resource Design
nouns, plurals, nesting, casing and identifiers
Naming Rules
The conventions most APIs follow
| Rule | Good | Avoid |
|---|---|---|
| nouns, not verbs | POST /orders | POST /createOrder |
| plural collections | /orders/8812 | /order/8812 |
| lower case, hyphens | /order-items | /OrderItems, /order_items |
| no file extensions | Accept: 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
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
IDs that do not leak or collide
| Kind | Example | Notes |
|---|---|---|
| auto-increment | 8812 | short, but reveals volume and invites enumeration |
| UUIDv4 | 7d1f3c2e-5b9a-4c11-9a0e-2f6c8d4b1e90 | random, unguessable, sorts badly in indexes |
| UUIDv7 | 019a2b3c-4d5e-7f60-8a1b-2c3d4e5f6a7b | time-ordered (RFC 9562), index friendly |
| prefixed | ord_8812, cus_42 | the 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
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
| Limit | Typical value |
|---|---|
| browsers (Chrome) | 2 MB in the address bar, about 2,000 characters for sharing safely |
| nginx large_client_header_buffers | 8 KB per request line by default |
| AWS ALB | 16 KB request line |
JSON & Data Conventions
field names, dates, money, nulls and envelopes
Field Names and Types
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
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
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
Wrap collections, not single resources
{"data": [{"id": "ord_8812"}, {"id": "ord_8813"}], "nextCursor": "eyJpZCI6ODgxM30"}
Enums, Booleans and Units
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
| Style | Example | Common in |
|---|---|---|
| camelCase | createdAt | JavaScript, Java, most public JSON APIs |
| snake_case | created_at | Python, Ruby, PostgreSQL-backed APIs |
| kebab-case | created-at | URLs and headers, rarely JSON fields |
Headers & Content Negotiation
Accept, Content-Type, Authorization, Prefer and custom headers
The Headers Every API Uses
Request and response headers
| Header | Direction | Example |
|---|---|---|
| Content-Type | both | application/json |
| Accept | request | application/json, application/problem+json |
| Authorization | request | Bearer eyJhbGciOi... |
| Location | response | /orders/8812 |
| ETag | response | "v42" |
| If-Match | request | "v42" |
| Retry-After | response | 30 |
| traceparent | both | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 |
Content Negotiation
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
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
Name them without X-
Api-Version: 2026-10-01 Request-Id: req_7Hc2kQ
Request IDs
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
Identify your integration
User-Agent: acme-billing-sync/2.3.1 (+https://acme.example/contact)
Status Codes in REST
the codes each method returns, at a glance
By Method
Success and the common failures
| Request | Success | Common failures |
|---|---|---|
| GET /orders | 200 | 400 bad filter, 401, 403 |
| GET /orders/8812 | 200, 304 | 404, 410 |
| POST /orders | 201 + Location, 202 if queued | 400, 409 duplicate, 415, 422 |
| PUT /orders/8812 | 200 or 204, 201 if created | 404, 409, 412, 422 |
| PATCH /orders/8812 | 200 or 204 | 404, 409, 412, 415, 422 |
| DELETE /orders/8812 | 204 | 404, 409 still in use |
| any request | 2xx as above | 429 rate limit, 500, 503 |
401 or 403, 400 or 422
The two debates that never end
| Question | Answer |
|---|---|
| no token or an expired one | 401 with WWW-Authenticate |
| valid token, missing permission | 403 |
| another tenant's resource | 404, so its existence is not revealed |
| body is not valid JSON | 400 |
| valid JSON, invalid values | 422, or 400 if you prefer one code |
Error Responses
RFC 9457 problem details, validation errors and error codes
Problem Details
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
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
What every error response should do
| Rule | Why |
|---|---|
| use the right status code | clients, proxies and monitoring read the code first |
| same shape everywhere | including 404 from the router and 500 from crashes |
| include a request ID | support can find the log line |
| never include stack traces | they leak internals; log them instead |
| list all validation errors | one round trip instead of one per field |
| document every type | the type URI should open a page explaining the fix |
Read Errors in a Client
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>"))
Pagination
offset, page and cursor pagination, Link headers and totals
Three Styles
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 page | Cursor | |
|---|---|---|
| jump to any page | yes | no |
| stable when data changes | no | yes |
| deep pages | slow (database scans skipped rows) | fast with an index |
| total count | easy, but costly on big tables | usually omitted |
| used by | many admin UIs and older APIs | Stripe (starting_after), Slack (cursor), GitHub issues |
Cursors Under the Hood
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
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
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
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
Filtering, Sorting & Fields
query parameter conventions for searching, ordering and sparse fieldsets
Filtering
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
Sort, select and expand
GET /orders?sort=-createdAt,total GET /orders?fields=id,status,total GET /orders/8812?expand=customer,items.product
Validate Filters
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
Conventions worth writing down
| Need | Convention |
|---|---|
| since a time | ?updatedAfter=2026-10-01T00:00:00Z |
| a closed range | ?createdAfter=...&createdBefore=... |
| several values | ?status=paid,shipped |
| sort descending | ?sort=-createdAt |
| stable sort | always add a unique tie-breaker such as id |
Partial Updates
JSON Merge Patch, JSON Patch and when to use each
JSON Merge Patch (RFC 7396)
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)
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
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
| Format | Media type | Good for |
|---|---|---|
| JSON Merge Patch | application/merge-patch+json | most updates of plain fields |
| JSON Patch | application/json-patch+json | array edits, moves, test-and-set |
| plain JSON | application/json | treated as merge patch by many APIs; document it |
Bulk & Batch Operations
creating many items, partial failures and batch endpoints
Bulk Create
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
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"}}
]}
Links & Hypermedia
HATEOAS, HAL, JSON:API and links in headers
Links in the Body
HAL style
{
"id": "ord_8812",
"status": "pending",
"_links": {
"self": {"href": "/orders/8812"},
"customer": {"href": "/customers/42"},
"cancel": {"href": "/orders/8812/cancellation", "method": "POST"}
}
}
JSON:API
A complete specification for JSON APIs
{
"data": {
"type": "orders",
"id": "8812",
"attributes": {"status": "pending", "total": "49.00"},
"relationships": {"customer": {"data": {"type": "customers", "id": "42"}}}
},
"included": [{"type": "customers", "id": "42", "attributes": {"name": "Ana"}}]
}
Versioning & Deprecation
URL, header and date versions, breaking changes and sunset headers
Where the Version Lives
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
Breaking and non-breaking changes
| Change | Breaking? |
|---|---|
| add an optional field to a response | no, if clients ignore unknown fields |
| add an optional request parameter | no |
| add a new endpoint | no |
| remove or rename a field | yes |
| change a field's type or format | yes |
| make an optional parameter required | yes |
| add a value to an enum | often: strict clients reject unknown values |
| change a status code or error type | yes |
Deprecation
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
Tolerant reading of enums
const LABELS = { pending: "Waiting", paid: "Paid", shipped: "On its way" };
function statusLabel(status) {
return LABELS[status] ?? "Updated";
}
Changelogs
What every API change log entry needs
| Field | Example |
|---|---|
| date | 2026-10-01 |
| version | 2026-10-01 or v1.14 |
| type | added, changed, deprecated, removed, fixed, security |
| what | GET /orders accepts sort=-total |
| action needed | none, or a migration note with a deadline |
OpenAPI 3.2
describing an API: paths, schemas, security and what 3.2 added
A Minimal Document
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
Versions you will meet
| Version | Released | Key changes |
|---|---|---|
| 3.0 | 2017 | components, requestBody, servers, links, callbacks |
| 3.1 | February 2021 | full JSON Schema 2020-12, webhooks, type: [string, "null"] instead of nullable |
| 3.2 | September 2025 | query 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
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
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
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
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
Frameworks that write OpenAPI for you
| Stack | How |
|---|---|
| FastAPI | built in, at /openapi.json and /docs |
| Spring Boot | springdoc-openapi, at /v3/api-docs |
| ASP.NET Core 9 and later | builder.Services.AddOpenApi() and app.MapOpenApi() |
| NestJS | @nestjs/swagger |
| Express | zod-openapi or tsoa, or write the YAML by hand |
Design Checklist
the review list before an endpoint ships
Before an Endpoint Ships
Twelve questions
| Area | Question |
|---|---|
| naming | plural nouns, consistent casing, no verbs in the path? |
| methods | right method, safe and idempotent where they should be? |
| status codes | specific codes, 201 with Location, 204 without a body? |
| errors | problem+json, every validation error listed? |
| auth | ownership checked on every object ID? |
| input | validated against a schema, writable fields allow-listed? |
| pagination | default and maximum page size, stable ordering? |
| formats | RFC 3339 dates, string IDs, money with currency? |
| compatibility | additive change only, or a new version? |
| limits | rate limits and payload limits in place and documented? |
| docs | in the OpenAPI document, with examples? |
| observability | request ID returned, errors logged with context? |
Common Mistakes
Seen in most API reviews
| Mistake | Fix |
|---|---|
| verbs in URLs | nouns plus methods, or action resources |
| 200 for errors | real status codes plus problem+json |
| no pagination | default and maximum page sizes from day one |
| numeric IDs over 2^53 as numbers | send IDs as strings |
| no ownership checks | authorize every object access |
| no timeouts in clients | set connect and read timeouts |
| breaking changes in place | additive changes, or a new version with a sunset date |
Authentication
API keys, bearer tokens, OAuth 2.0 flows and mutual TLS
Choose an Auth Method
Who is calling decides the method
| Caller | Method |
|---|---|
| your own server-to-server job | OAuth 2.0 client credentials, or an API key |
| third-party developer acting for themselves | API key or personal access token |
| app acting for a user | OAuth 2.0 authorization code with PKCE |
| single page app or mobile app | authorization code with PKCE, no client secret |
| partner with strict requirements | mutual TLS, alone or binding tokens to the certificate |
Sending Credentials
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
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
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
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
Keep only a hash on the server
| Step | Do |
|---|---|
| create | generate 32 random bytes, show the full key to the user once |
| store | keep a SHA-256 hash and a short prefix such as sk_live_a1b2 for display |
| check | hash the incoming key and look up the hash |
| rotate | allow two active keys per client so they can switch without downtime |
Mutual TLS
A client certificate instead of a secret
curl --cert client.pem --key client-key.pem https://partner-api.example.com/v1/orders
JWT & Access Tokens
structure, claims, validation and common mistakes
JWT Structure
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
The checks a server must make
| Check | Reject when |
|---|---|
| signature | it does not verify with the issuer's key |
| alg | not the algorithm you expect; never accept none |
| exp | the expiry time has passed (allow a minute of clock skew) |
| nbf | the token is not valid yet |
| iss | the issuer is not the one you trust |
| aud | your API is not the intended audience |
| scope or roles | the token lacks the permission for this endpoint |
Lifetimes and Storage
Short access tokens, rotated refresh tokens
| Token | Typical lifetime | Store in |
|---|---|---|
| access token | 5 to 60 minutes | memory, or an HttpOnly cookie for web apps |
| refresh token | days to weeks, rotated on use | HttpOnly Secure SameSite cookie, or the OS keychain on mobile |
| API key | until revoked | a secret manager, never in source code |
Verify a Signature by Hand
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"],
});
API Security
the OWASP API Security Top 10 and the checks that prevent each risk
OWASP API Security Top 10, 2023
The current list and the fix for each
| ID | Risk | Prevent it with |
|---|---|---|
| API1 | Broken Object Level Authorization | check that the caller owns or may access every object ID |
| API2 | Broken Authentication | standard token validation, rate limits on login and reset |
| API3 | Broken Object Property Level Authorization | allow-lists for readable and writable fields |
| API4 | Unrestricted Resource Consumption | rate limits, page size caps, timeouts, upload limits |
| API5 | Broken Function Level Authorization | deny by default; check roles on every admin endpoint |
| API6 | Unrestricted Access to Sensitive Business Flows | bot protection and limits on flows such as checkout |
| API7 | Server Side Request Forgery | validate and allow-list URLs the API fetches |
| API8 | Security Misconfiguration | secure defaults, no verbose errors, current TLS, CORS allow-lists |
| API9 | Improper Inventory Management | document and retire old versions and test endpoints |
| API10 | Unsafe Consumption of APIs | validate data from third-party APIs like user input |
Broken Object Level Authorization
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
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
Baseline rules
| Area | Rule |
|---|---|
| TLS | HTTPS only; redirect or reject plain HTTP, send Strict-Transport-Security |
| input | validate types, lengths and formats against a schema before using them |
| output | return only fields the caller may see, never whole database rows |
| limits | cap page sizes, body sizes and request rates per client |
| secrets | keys in a secret manager, rotated, never in Git or logs |
| logging | log who did what, never tokens or passwords |
SSRF
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
Slow down guessing
POST /oauth/token HTTP/1.1 HTTP/1.1 429 Too Many Requests Retry-After: 60
CORS
preflight requests, the headers and the mistakes behind CORS errors
How CORS Works
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
Why CORS errors happen
| Mistake | Effect |
|---|---|
| Access-Control-Allow-Origin: * with credentials | browsers refuse it; echo the specific origin instead |
| missing Vary: Origin when echoing origins | a CDN serves one origin's header to another |
| preflight hits auth middleware and gets 401 | answer OPTIONS before authentication |
| header missing on error responses | the browser hides the real 4xx or 5xx behind a CORS error |
| allowing any origin that contains example.com | evil-example.com passes; compare exact origins |
Simple and Preflighted Requests
Which requests trigger a preflight
| Request | Preflight? |
|---|---|
| GET or HEAD with ordinary headers | no |
| POST with form or text/plain body | no |
| POST with Content-Type: application/json | yes |
| any request with Authorization or a custom header | yes |
| PUT, PATCH or DELETE | yes |
Debug CORS With curl
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"
Caching & Concurrency
Cache-Control for APIs, ETags, 304 and optimistic locking with If-Match
Cache-Control for APIs
Pick one per endpoint
| Response | Cache-Control |
|---|---|
| public reference data, same for everyone | public, max-age=3600 |
| data for one user | private, max-age=0, must-revalidate |
| tokens, secrets, personal data | no-store |
| data that changes often, revalidate cheaply | no-cache, with an ETag |
Optimistic Concurrency
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
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
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;
}
Idempotency & Retries
idempotency keys, safe retries and exactly-once effects
Idempotency Keys
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
What a client may retry
| Situation | Retry? |
|---|---|
| network error or timeout on GET, PUT, DELETE | yes, they are idempotent |
| same on POST or PATCH | only with an idempotency key |
| 429 or 503 | yes, after Retry-After |
| 502 or 504 | yes, with backoff |
| other 4xx | no, fix the request |
Generate Keys on the Client
One key per logical operation
const key = crypto.randomUUID();
console.log(key.length, /^[0-9a-f-]{36}$/.test(key), key.charAt(14));
Rate Limiting
algorithms, headers and what clients should do
Algorithms
Four common ways to count
| Algorithm | Behavior |
|---|---|
| fixed window | N requests per clock minute; allows bursts at window edges |
| sliding window | N requests in any 60 seconds; smoother, more state |
| token bucket | tokens refill at a steady rate, bursts up to the bucket size |
| leaky bucket | requests leave at a constant rate, extras queue or drop |
The 429 Response
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
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
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)
Async Jobs & Webhooks
202 and polling, webhook delivery and signature verification
Long-Running Operations
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
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
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
What providers and receivers agree on
| Rule | Typical value |
|---|---|
| receiver must answer within | 5 to 30 seconds |
| success | any 2xx |
| retries | exponential backoff over hours or days, such as 1 m, 5 m, 30 m, 2 h, 12 h |
| duplicates | possible: deduplicate on the event ID |
| ordering | not guaranteed: use timestamps or fetch the current state |
| security | HMAC signature, timestamp check, HTTPS only |
File Uploads & Downloads
multipart uploads, presigned URLs and downloads
Uploads
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
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
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
Large files over unreliable networks
| Approach | Notes |
|---|---|
| tus protocol | open protocol with clients and servers for many languages |
| IETF resumable uploads draft | standardizes the same idea, including the 104 status code |
| S3 multipart upload | parts of 5 MB to 5 GB, uploaded in parallel and completed in one call |
Calling APIs From Code
fetch, Python, Java, Go, C# and PowerShell side by side
POST JSON and Read the Response
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
Defaults that bite
| Client | Default timeout |
|---|---|
| fetch (browsers and Node.js) | none in the spec; Node.js stops waiting for headers after 300 seconds |
| Python requests | none |
| Python httpx | 5 seconds |
| Go http.Client | none |
| Java HttpClient | none |
Building APIs
a CRUD resource in six frameworks
A CRUD Resource
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(); }
}
Testing & Debugging
curl -v, HTTPie, Postman, mocks and contract tests
See Exactly What Was Sent
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
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
Pact in one paragraph
| Step | What happens |
|---|---|
| consumer test | the client's tests record the requests it makes and the responses it expects |
| pact file | those expectations are saved as a contract |
| provider verification | the API's CI replays the contract against the real service |
| broker | shares contracts and tells you whether a version can be deployed |
REST vs GraphQL vs gRPC
choosing an API style
Side by Side
Three API styles
| REST | GraphQL | gRPC | |
|---|---|---|---|
| transport | HTTP/1.1, 2 or 3 | HTTP, one endpoint | HTTP/2 |
| format | JSON, usually | JSON | Protocol Buffers, binary |
| contract | OpenAPI, optional | schema, required | .proto files, required |
| caching | HTTP caching works | mostly client side | none built in |
| over-fetching | common without fields | the client picks fields | fixed messages |
| browsers | native | native | needs gRPC-Web or a proxy |
| best for | public APIs, CRUD, caching | many clients with different data needs | internal service-to-service calls |
Older Styles You Still Meet
SOAP and RPC over HTTP
| Style | Traits |
|---|---|
| SOAP | XML envelopes, WSDL contracts, WS-Security; still common in banking, government and enterprise integrations |
| JSON-RPC | one endpoint, method names in the body; used by Ethereum nodes and the Language Server Protocol |
| XML-RPC | the 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.
- Start: the toolbox and a first request, REST principles and HTTP methods.
- Design: URLs and resources, JSON conventions, headers, status codes and error responses.
- Collections: pagination, filtering and sorting, partial updates, bulk operations and hypermedia.
- Lifecycle: versioning and deprecation, OpenAPI 3.2 and a design checklist.
- Security: authentication, JWT, the OWASP API Top 10 and CORS.
- Reliability: caching and concurrency, idempotency, rate limiting, async jobs and webhooks and file uploads.
- Code: clients and servers in six languages, testing, and REST compared with GraphQL and gRPC.
REST API Quick Reference
The decisions people look up most often, one line each.
| Task | Convention |
|---|---|
| List a collection | GET /orders |
| Read one item | GET /orders/8812 |
| Create | POST /orders, answer 201 plus Location |
| Replace | PUT /orders/8812 |
| Change some fields | PATCH with application/merge-patch+json |
| Delete | DELETE /orders/8812, answer 204 |
| An action | POST /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 |
| Authenticate | Authorization: Bearer <token> |
| Error body | application/problem+json (RFC 9457) |
| Safe retries of POST | Idempotency-Key header |
| Prevent lost updates | ETag plus If-Match, 412 on conflict |
| Rate limited | 429 plus Retry-After |
| Dates | RFC 3339 in UTC: 2026-10-14T09:12:03Z |
| Describe it | OpenAPI 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.
- The client builds the request: method, URL, headers such as Authorization and Content-Type, and a JSON body.
- A gateway or load balancer terminates TLS, applies rate limits and CORS, and routes by path or version.
- The framework matches the route, answering 404 or 405 when nothing matches.
- Middleware authenticates the token (401) and checks permissions and object ownership (403 or 404).
- The body is validated against a schema, and invalid input gets a problem+json response (400 or 422).
- The handler runs business logic, checks preconditions such as If-Match (412) and conflicts (409), and writes to the database.
- 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.