Guide

REST API design explained: resources, HTTP verbs, and best practices

A REST API exposes your server's data and actions through URLs and standard HTTP methods so any client — mobile app, browser, cron job, or partner integration — can interact without bespoke protocols. Good REST design feels predictable: the same patterns repeat, errors are readable, and callers can retry safely. This guide walks through resource naming, verbs, status codes, pagination, versioning, and the details that separate polished APIs from accidental RPC tunnels.

Resources, not actions

REST models your domain as resources — nouns with stable identities. Each resource has a URL. Clients manipulate resources with HTTP verbs instead of encoding commands in path segments.

Prefer:

  • GET /users/42 — fetch user 42
  • POST /users — create a new user
  • PATCH /users/42 — update fields on user 42
  • DELETE /users/42 — remove user 42

Avoid RPC-style paths that smuggle verbs into URLs:

  • POST /createUser
  • GET /getOrderById?id=9
  • POST /users/42/activateAccount (use PATCH /users/42 with {"status":"active"} instead)

Collections use plural nouns: /orders, /transactions, /invoices. Nested resources express relationships: GET /customers/7/orders lists orders belonging to customer 7. Keep nesting shallow — more than two levels (/a/b/c/d) usually means you need a flatter model or a query parameter.

Use kebab-case in paths (/payment-intents) and lowercase throughout. Identifiers belong in the path, not the query string, when they name a single resource: /payments/pi_abc123 beats /payments?id=pi_abc123.

HTTP methods and what they mean

HTTP verbs carry semantics. Using them consistently lets caches, proxies, and client libraries do the right thing.

Method Safe Idempotent Typical use
GET Yes Yes Read a resource or collection. No request body. Cacheable.
POST No No Create a resource or trigger a non-idempotent action. Returns 201 Created with a Location header when appropriate.
PUT No Yes Replace an entire resource at a known URL. Uncommon for partial updates.
PATCH No Usually Partial update — send only changed fields. Prefer over PUT for most apps.
DELETE No Yes Remove a resource. Repeated deletes should return 404 or 204 consistently.
HEAD Yes Yes Like GET but no body — useful for existence checks and cache validators.

Safe means the method does not change server state. Idempotent means calling it multiple times has the same effect as calling it once — critical for retries after network timeouts. If a mobile client POSTs "charge card" twice because the first response was lost, you may double-charge unless you accept an Idempotency-Key header and deduplicate server-side.

Status codes that tell the truth

Status codes are your API's first sentence in an error message. Use the standard ones before inventing custom envelopes.

  • 200 OK — successful GET, PATCH, or PUT with a body.
  • 201 CreatedPOST created a resource; include Location: /resources/{id}.
  • 204 No Content — success with no body (DELETE, some updates).
  • 400 Bad Request — malformed JSON, missing required field, invalid enum value.
  • 401 Unauthorized — missing or invalid authentication (no valid token).
  • 403 Forbidden — authenticated but not allowed (wrong role, wrong tenant).
  • 404 Not Found — resource ID does not exist (or you hide existence for security).
  • 409 Conflict — duplicate unique key, optimistic-lock version mismatch.
  • 422 Unprocessable Entity — syntactically valid JSON but business-rule failure.
  • 429 Too Many Requests — rate limit exceeded; send Retry-After.
  • 500 Internal Server Error — unexpected server fault; never leak stack traces in production.
  • 503 Service Unavailable — maintenance or overload; also use Retry-After.

Do not return 200 OK with {"success": false} in the body. HTTP-aware clients, monitors, and CDNs rely on the status line. Put details in a consistent JSON error object:

{
  "error": {
    "code": "insufficient_balance",
    "message": "Account balance is 0.42 SOL; payment requires 1.00 SOL.",
    "request_id": "req_8f3a2c"
  }
}

Include a request_id (or echo the client's X-Request-Id) so support can grep logs without guessing which call failed.

Filtering, sorting, and pagination

Collections grow. Returning ten thousand rows in one response wastes bandwidth, times out mobile clients, and hammers your database. Paginate by default.

Offset pagination

GET /orders?limit=20&offset=40 skips the first 40 rows. Simple to implement with SQL LIMIT/OFFSET, but slow on large offsets because the database still scans skipped rows. Fine for admin dashboards with modest page counts.

Cursor pagination

GET /orders?limit=20&cursor=eyJpZCI6OTk5fQ returns the next page after an opaque cursor (often a base64-encoded sort key). Stable under concurrent inserts — new rows do not shift page boundaries. Preferred for public feeds, chat history, and blockchain indexers where new items arrive constantly.

Response shape should include navigation hints:

{
  "data": [ /* ... */ ],
  "pagination": {
    "limit": 20,
    "next_cursor": "eyJpZCI6MTAxOX0",
    "has_more": true
  }
}

Filtering and sorting belong in query parameters: ?status=open&sort=-created_at. Document which fields are filterable — undeclared filters surprise integrators and can become SQL-injection vectors if you pass them straight into queries.

Versioning without breaking clients

You will ship breaking changes. Plan for them on day one.

  • URL versioning/v1/users, /v2/users. Obvious in docs and logs; slightly noisier URLs.
  • Header versioningAccept: application/vnd.myapi.v2+json. Clean URLs; harder to test in a browser address bar.
  • Additive changes only — new optional JSON fields rarely break well-written clients. Renaming or changing types does.

Run old and new versions in parallel during a deprecation window. Return Sunset and Deprecation headers on legacy routes. Log traffic to v1 so you know when it is safe to retire.

Authentication, authorization, and CORS

Most APIs authenticate with a bearer token in the Authorization header or a session cookie for browser-only apps. Separate authentication (who are you?) from authorization (what may you do?). Return 401 when credentials are missing or invalid; 403 when credentials are valid but insufficient.

Browser-based clients calling your API from a different origin need correct CORS headers on preflight OPTIONS responses. Server-to-server integrations do not use CORS — they call your API directly.

For machine clients, document token lifetimes, refresh flows, and required scopes. OAuth 2.0 and OpenID Connect are the standard for delegated access; see our OAuth guide for authorization-code + PKCE patterns.

Reliability patterns integrators expect

Idempotency keys

For POST endpoints that create charges, orders, or on-chain payouts, accept Idempotency-Key: <uuid>. Store the first successful response keyed by that header; replay the same response on duplicates within a 24-hour window. Payment processors and Solana settlement services depend on this to survive flaky mobile networks.

Webhooks for async outcomes

Not every operation finishes inside one HTTP round trip. Return 202 Accepted with a status URL, then push completion via webhooks signed with HMAC. Clients poll only as a fallback.

Rate limits

Publish quotas and return 429 with Retry-After before you melt downstream databases. Token-bucket and sliding-window strategies are covered in our rate limiting guide.

Health and discovery

Expose GET /health (liveness) and optionally GET /ready (checks database connectivity). OpenAPI (Swagger) or JSON Schema docs at a stable URL reduce support tickets. If you verify blockchain payments server-side, document confirmation levels and idempotency the same way — see verify a Solana payment for an on-chain example.

Common mistakes to avoid

  • Using GET for operations that change state (breaks caching, enables CSRF via image tags).
  • Returning different JSON shapes for the same resource across endpoints.
  • Exposing internal database IDs without access control on sequential integers.
  • Ignoring Content-Type — reject non-JSON bodies on JSON endpoints with 415 Unsupported Media Type.
  • Silent truncation — if limit caps at 100, document it; do not return 10,000 rows anyway.
  • Leaking stack traces or SQL errors in 500 responses.
  • Changing field types in place instead of versioning.

Design checklist

  • URLs name resources (nouns), verbs live in HTTP methods.
  • Correct status codes; structured error JSON with request_id.
  • Cursor pagination for large or live collections.
  • Versioning strategy documented before v1 ships.
  • Idempotency keys on create/charge endpoints.
  • Rate limits, health endpoint, and published OpenAPI spec.
  • CORS configured for browser clients; bearer tokens for server clients.
  • Indexes on columns you filter and sort — see database indexing if list endpoints slow down under load.

Related reading