DS DevShelfHub Projects · AI tools
Cheatsheets / REST API design
Cheatsheet · Dev tooling

REST API Design: Resources, Verbs, Pagination and Errors Reference Guide

By DevShelfHub

Resources, verbs, status codes, pagination, errors, versioning, auth, hypermedia — the design choices that age well.

103 items 7 min Resources Verbs Status

Start hereQuick start · 6 you’ll reach for daily

Resource URI/v1/orders/101
VerbsGET POST PUT PATCH DELETE
Created201 + Location: …
Errorsapplication/problem+json
IdempotentIdempotency-Key: …
CachingETag + If-None-Match

scope · standardsVersions

Standards: RFC 9110 (HTTP semantics) RFC 9457 (Problem Details) OpenAPI 3.1

Conventions on this page are framework-neutral. Modern defaults: JSON over HTTPS, OAuth 2 / Bearer tokens, OpenAPI 3.1 as the schema, RFC 9457 for errors. Where conventions split (e.g. URL vs header versioning) the row says so.

parts of an HTTP APIAnatomy

Base URLStable scheme + host. Pin https.
Version segment/v1/ or media-type version. Pick one.
Resource pathPlural nouns: /orders/101, not /getOrder?id=101.
Verb (method)Encodes intent. Body shape is per-resource.
HeadersAuth, content-type, caching, tracing, idempotency.
Query stringFilters, sort, pagination, projection.
BodyJSON object. Lists wrapped in { "data": […] }.
Status codeClass of outcome (2xx / 4xx / 5xx). Specific code in the response.
bash
### Collection — list + filter + paginate
GET /v1/orders?status=shipped&customer_id=42&sort=-created_at&limit=20

200 OK
Link: ; rel="next"
{
  "data":     [ { "id": 101, "status": "shipped", "total": "120.00" } ],
  "page_info": { "has_next": true, "cursor": "eyJpZCI6MTAwfQ" }
}

### Single resource — create
POST /v1/orders
Idempotency-Key: 4f1f...
Content-Type: application/json

{ "customer_id": 42, "items": [ { "sku": "ABC", "qty": 2 } ] }

201 Created
Location: /v1/orders/101
{ "id": 101, "status": "pending", ... }

### Validation error
422 Unprocessable Entity
Content-Type: application/problem+json
{
  "type":   "https://api.example.com/problems/validation",
  "title":  "Validation failed",
  "status": 422,
  "errors": [ { "field": "items[0].qty", "code": "min", "message": "must be >= 1" } ]
}

noun-shaped URLsResources

/orders & /orders/{id}Plural collections, singular item by id.
/customers/{id}/ordersNested for parent-child. Cap nesting at two levels.
/orders/{id}/cancelPreferred for actions that don’t fit CRUD verbs.
kebab-case-pathsLowercase, hyphens, no underscores or camelCase in URLs.
Resource ids: stable, opaqueUUID / snowflake. Avoid leaking sequence / counts.
Singletons: /me, /current-userFor obviously-single resources scoped to caller.
No verbs in URLs (mostly)Reach for them only when truly non-CRUD (search, cancel, restart).
Composite ids/orders/{order}/items/{sku} is fine. Prefer single id where possible.

methods & semanticsHTTP verbs

GETRead. Safe + idempotent. No body. Cacheable.
HEADGET headers only. Useful for existence checks.
POSTCreate or non-idempotent action. Returns new resource.
PUTFull replace. Idempotent. Server stores what you sent.
PATCHPartial update. JSON Patch (RFC 6902) or merge patch (RFC 7396).
DELETERemove. Idempotent. 204 on success.
OPTIONSCORS preflight + capability discovery.
Idempotent vs safeSafe = no side effects. Idempotent = same effect if repeated.
Content-Type: application/merge-patch+jsonSet this when sending PATCH with a partial JSON body.

use the right oneStatus codes

200 OKDefault success. Body present.
201 CreatedPOST returning a new resource. Set Location header.
202 AcceptedAsync / queued. Return a job URL in Location.
204 No ContentSuccess, no body. Common for DELETE.
301 / 302 / 308 / 307Redirects: permanent / temporary, GET-only / preserve-method.
304 Not ModifiedETag matched If-None-Match. No body.
400 Bad RequestMalformed request. Reserve for parse failures, not validation.
401 UnauthorizedNo / bad credentials. Set WWW-Authenticate.
403 ForbiddenAuthenticated but not allowed.
404 Not FoundResource doesn’t exist (or you can’t see it).
405 Method Not AllowedWrong verb on a known URI. Set Allow.
409 ConflictState conflict (e.g. already cancelled).
410 GoneResource existed, no longer available.
412 Precondition FailedIf-Match didn’t match. Concurrency control.
415 Unsupported Media TypeBody content-type not accepted.
422 Unprocessable EntityPreferred for validation failures.
429 Too Many RequestsRate limited. Set Retry-After.
500 / 502 / 503 / 504Server / gateway. Don’t leak stack traces.

query string conventionsQuerying & filtering

?status=shipped&customer_id=42Equality filters. Combine with AND semantics.
?status=shipped,pendingComma-separated for IN filters.
?created_at[gte]=2026-01-01Operator suffix for ranges. Pick a syntax & stick with it.
?sort=-created_at,nameLeading - means descending. Multi-key support.
?fields=id,total,customer.idSparse fieldsets / projection.
?include=customer,itemsEmbed related resources. Cap depth.
?q=fooFree-text search. Document the matching rules.
Booleans: true / falseNot 1/0. Coerce on the server.
Dates: ISO 8601 / RFC 3339Always UTC, with offset. Document the expected format.
URL-encode everything user-suppliedAlways. Search terms, ids, anything.

cursor · keyset · pagePagination

Cursor-basedPreferred Opaque token. Stable under inserts.
Keyset (since_id / before_id)Fastest on large tables; needs a sort key.
Page-numberAvoid for hot data. Shifts under writes.
Link header (RFC 5988)rel="next" / "prev". Standards-friendly.
total / has_nextAlways say at least whether more exists. Skip total on huge sets.
Cap limit server-sideHard cap (e.g. 100) + reasonable default (e.g. 20).
Deterministic sortAlways include the id in the sort to break ties.
bash
### Cursor — preferred for streams + stable order
GET /v1/orders?limit=50&cursor=eyJpZCI6MTAwfQ
{
  "data":      [ ... ],
  "page_info": { "has_next": true, "cursor": "eyJpZCI6MTUwfQ" }
}

### RFC 5988 Link header — discoverable
Link: ; rel="next",
      ; rel="prev"

### Page-number — easy but bad with mutating data
GET /v1/orders?page=3&per_page=20
{ "data": [...], "page": 3, "total": 412, "per_page": 20 }

### Keyset — fastest under load (since_id / before_id)
GET /v1/orders?since_id=1500&limit=50

### Always:
- Cap `limit` server-side  (e.g. 100 hard, 20 default)
- Encode cursors opaquely  (base64(json)) and treat them as forward-compatible
- Document a deterministic sort order (e.g. -id) — pagination breaks without one

consistent shapeErrors

application/problem+json (RFC 9457)Preferred Standardised error body.
type, title, status, detail, instanceFive canonical fields.
Field-level errorsAdd an errors[] array with field + code + message.
Stable error codesStrings, not numbers. order.already_shipped beats 42.
Trace id in bodyLets users send you something useful when they report bugs.
Don’t leak internalsStrip stack traces, query plans, internal hostnames before sending.
Distinguish 4xx vs 5xx carefullyClient-correctable vs server bug. Mis-classifying ruins alerting.
json
### RFC 9457 (Problem Details for HTTP) — the canonical error format
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type":     "https://api.example.com/problems/order-already-shipped",
  "title":    "Order already shipped",
  "status":   409,
  "detail":   "Order 101 was shipped at 2026-05-17T12:00:00Z and can't be cancelled.",
  "instance": "/v1/orders/101/cancel",
  "trace_id": "abc123",
  "errors":   []
}

### Common shapes
- type     — stable URI per error class
- title    — short human-readable summary (same per type)
- status   — repeat of HTTP status (helps proxies / logs)
- detail   — human-readable, specific to this instance
- instance — URI of the failing request
- errors   — array for field-level validation faults

change without breakingVersioning

URL versioningPreferred for external APIs. /v1, /v2.
Header / media-typeKeeps URLs stable. Harder to test from a browser.
Date-basedStripe-style Stripe-Version: 2025-05-17. Continuous evolution.
Non-breaking changesAdding fields / new endpoints / optional params. Ship without bumping.
Breaking changesField renames / type changes / removals. New major version.
Deprecation: headerRFC 8594 Deprecation + Sunset headers.
Compatibility windowPromise N-1 (or N-2) versions on the docs page.
bash
### URL versioning — most common, easiest to debug
GET /v1/orders/101
GET /v2/orders/101

### Header / media-type versioning — keeps URLs stable
GET /orders/101
Accept: application/vnd.example.api+json;version=2

### Query versioning — fine for internal APIs, weak for caching
GET /orders/101?api_version=2

### Date-based versioning (Stripe-style)
GET /orders/101
Stripe-Version: 2025-05-17

### Rules of thumb
- Adding fields is non-breaking. Removing or changing types is breaking.
- Bumping versions costs the team weeks; do it for shape changes, not field renames.
- Deprecation: announce, ship the new version, keep the old for 6-12 months,
  serve `Deprecation: true` + `Sunset: ` headers, then remove.

identity & accessAuth & security

Authorization: Bearer …OAuth 2 access tokens. Standard for service-to-service + user-on-behalf.
Authorization: Basic …Legacy Username:password. Use over TLS only.
API keys in headers, never URLsURLs leak in logs, referers, browser history.
JWT vs opaque tokensJWT for stateless verify; opaque tokens for revocability.
Scopes / permissionsLeast-privilege per token. Document required scope per endpoint.
CORSWhitelist origins explicitly. Never * with credentials.
HTTPS onlyStrict-Transport-Security + redirect HTTP → HTTPS.
Rate limit + 429 + Retry-AfterPer token / per IP. Document the limits.
Audit log every writeActor, timestamp, resource, before/after. Non-negotiable for SaaS.

retry-safe writes & conditional readsIdempotency & caching

Idempotency-KeyCaller-supplied UUID. Server dedupes retries within a window.
If-Match: "abc"Optimistic concurrency for PUT/PATCH/DELETE.
If-None-Match: "abc"Conditional GET. 304 saves bandwidth.
ETag: "abc"Strong validator. Hash of the representation.
Last-Modified / If-Modified-SinceWeaker, time-based variant.
Cache-Control: private, max-age=15Per-user cacheable for 15 s.
Cache-Control: public, max-age=300CDN-cacheable shared resource.
Vary: Authorization, Accept-LanguageTell caches what varies the response.

links in responsesHypermedia

Plain link fieldsMost pragmatic. { "url": "/v1/orders/101" }.
HAL (application/hal+json)_links + _embedded envelope.
JSON:API (application/vnd.api+json)Spec-driven envelope with relationships.
Siren / Collection+JSONOlder link-rich formats. Rare in new APIs.
Discoverability via Link headerPagination, schemas, profiles — cheap and standards-friendly.
OpenAPI > HATEOASFor most teams, a published OpenAPI doc beats runtime hypermedia.
Pure HATEOAS APIs are rare in practice. The pragmatic middle: include URLs for related resources in the response so clients don’t hand-assemble them, but document everything in OpenAPI.

a production-shaped resourceEnd-to-end · Order lifecycle

List with ETag-based caching, create with idempotency, conditional GET, partial update with optimistic concurrency, and a domain-error response. The pattern most APIs converge on.

bash
### A minimal but production-shaped order resource

# List with filters + pagination
GET /v1/orders?status=shipped&limit=20
Authorization: Bearer ...
200 OK
ETag: "abc123"
Cache-Control: private, max-age=15

# Create with idempotency
POST /v1/orders
Authorization: Bearer ...
Idempotency-Key: 8f3a-...
Content-Type: application/json
{ "customer_id": 42, "items": [...] }
201 Created
Location: /v1/orders/101
{ "id": 101, "status": "pending", ... }

# Read with conditional GET
GET /v1/orders/101
If-None-Match: "abc123"
304 Not Modified

# Partial update
PATCH /v1/orders/101
If-Match: "abc123"
Content-Type: application/merge-patch+json
{ "status": "cancelled" }
200 OK
ETag: "def456"

# Domain error
POST /v1/orders/101/cancel
409 Conflict
Content-Type: application/problem+json
{ "type": "...", "title": "Already shipped", "status": 409 }

Best practiceGood to know

Wrap collections in an envelope. { "data": […], "page_info": { … } } gives you a place to add pagination, errors, and meta later without breaking clients. Bare top-level arrays paint you into a corner.
Idempotency keys on every write. Cheap to add (Idempotency-Key header, server-side dedupe in a 24-hour window). Eliminates the worst class of duplicate-charge bugs in retry-happy clients.
Publish an OpenAPI doc and lint it in CI. Spectral or Redocly rules catch missing examples, inconsistent error shapes, and breaking changes before they ship. Half the value of REST design is uniformity.

Common trapsWatch out for

200 OK on errors. Returning { "error": "…" } with status 200 breaks every monitoring tool. Match the status to the outcome.
Auth in the URL. ?api_key=… ends up in proxy logs, browser history, referer headers. Use a header.
Inconsistent error shape across endpoints. Clients write a switch statement and ship a hidden coupling. Pick a single error format (RFC 9457 is the safe default) and enforce it via linting.

Go deeperSee also

REST API design FAQ

What is REST API design?

REST (Representational State Transfer) is an architectural style for designing networked APIs over HTTP. A REST API models data as resources identified by URLs, uses standard HTTP verbs (GET, POST, PUT, PATCH, DELETE) for operations, and returns responses in JSON or another media type.

What HTTP status codes should a REST API return?

200 OK for successful reads, 201 Created with a Location header for new resources, 204 No Content for successful deletes, 400 for invalid input, 401 when credentials are missing, 403 when access is denied, 404 Not Found, 409 Conflict for duplicates, and 422 for validation failures.

How should REST APIs handle errors?

Use RFC 9457 Problem Details format: return application/problem+json with a type URI, title, status code, and a detail message. Include a validation_errors array for field-level errors. Consistent error shapes let clients display and log errors without parsing free-text messages.

What are the best practices for REST API versioning?

The three common strategies are URL path versioning (/v1/orders), Accept header versioning (Accept: application/vnd.api+json;version=1), and query-parameter versioning (?version=1). URL versioning is the most visible and easiest to test in a browser; header versioning keeps URLs clean.

What is idempotency in REST APIs?

An idempotent operation produces the same result whether called once or many times. GET, PUT, and DELETE are idempotent by definition; POST is not. For non-idempotent POST endpoints, accept an Idempotency-Key header and cache the first response so retries do not cause duplicate side effects.