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 42POST /users— create a new userPATCH /users/42— update fields on user 42DELETE /users/42— remove user 42
Avoid RPC-style paths that smuggle verbs into URLs:
POST /createUserGET /getOrderById?id=9POST /users/42/activateAccount(usePATCH /users/42with{"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, orPUTwith a body. - 201 Created —
POSTcreated a resource; includeLocation: /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 versioning —
Accept: 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
GETfor 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 with415 Unsupported Media Type. - Silent truncation — if
limitcaps at 100, document it; do not return 10,000 rows anyway. - Leaking stack traces or SQL errors in
500responses. - 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
- API rate limiting explained — token bucket, 429 responses, backoff
- Webhooks explained — push notifications, HMAC verification, retries
- JWT explained — bearer tokens, signing, validation
- All Solana Garden guides