T
DeveloperRESTAPIHTTP

REST API Design Guide for Developers (2026)

Build REST APIs that developers love to integrate with.

TechHowAI Team8 min read

A well-designed REST API feels predictable: the same URL patterns, status codes, and error shapes appear everywhere, so integrators spend time on business logic instead of guessing your conventions. This guide walks through resource modeling, HTTP semantics, pagination, versioning, and documentation patterns you can apply on your next endpoint.

What REST actually means in practice

REST (Representational State Transfer) is not a framework — it is a set of constraints that make APIs cacheable, scalable, and easy to reason about. In day-to-day engineering, that translates to:

  • Resources identified by URLs (/invoices/42, not /getInvoice?id=42)
  • Representations exchanged as JSON (or XML, but JSON dominates)
  • Stateless requests where the server does not rely on session memory between calls
  • Uniform interface using standard HTTP methods and status codes

You do not need HATEOAS hypermedia links on every response to call your API RESTful. What matters is consistency: if GET /users/7 returns a user object today, it should not suddenly require a POST tomorrow.

Naming resources and URLs

Treat URLs as nouns, not verbs. Collections are plural; individual items append an identifier.

http
GET    /v1/projects              # list projects
POST   /v1/projects              # create project
GET    /v1/projects/{id}         # read one
PATCH  /v1/projects/{id}         # partial update
DELETE /v1/projects/{id}         # remove

Nested resources work when the child cannot exist without a clear parent context:

http
GET  /v1/projects/{projectId}/tasks
POST /v1/projects/{projectId}/tasks

Avoid deep nesting beyond two levels (/a/{id}/b/{id}/c/{id}). Prefer flat URLs with query filters:

http
GET /v1/tasks?project_id=abc123&status=open

Use kebab-case in path segments (/payment-methods) and lowercase throughout. Query parameters use snake_case or camelCase — pick one and document it.

Actions that are not CRUD

When an operation does not map cleanly to PUT or PATCH, use a sub-resource with a verb-like noun:

http
POST /v1/invoices/{id}/send
POST /v1/accounts/{id}/password-reset

Reserve RPC-style paths for exceptional cases. If more than 20% of your endpoints are actions, reconsider your resource model.

HTTP methods and idempotency

MethodSafeIdempotentTypical use
GETYesYesRead
POSTNoNoCreate
PUTNoYesReplace entire resource
PATCHNoNo*Partial update
DELETENoYesRemove

*PATCH idempotency depends on implementation; design PATCH payloads so repeating the same request yields the same final state.

POST creates resources and returns 201 Created with a Location header pointing to the new URL:

http
POST /v1/customers HTTP/1.1
Content-Type: application/json

{"email": "ada@example.com", "plan": "pro"}
http
HTTP/1.1 201 Created
Location: /v1/customers/cust_8f2a
Content-Type: application/json

{"id": "cust_8f2a", "email": "ada@example.com", "plan": "pro", "created_at": "2026-07-16T08:00:00Z"}

Use PUT only when the client sends the full representation. Partial updates belong on PATCH with a documented merge strategy (JSON Merge Patch RFC 7396 or JSON Patch RFC 6902).

Status codes that communicate intent

Clients and proxies behave differently based on status families. Use them precisely:

2xx success

  • 200 OK — successful GET, PATCH, or action
  • 201 Created — successful POST that created a resource
  • 204 No Content — successful DELETE or update with no body

4xx client errors

  • 400 Bad Request — malformed JSON or failed validation
  • 401 Unauthorized — missing or invalid authentication (despite the name, this means "unauthenticated")
  • 403 Forbidden — authenticated but not permitted
  • 404 Not Found — resource does not exist (or is hidden for authorization)
  • 409 Conflict — duplicate email, optimistic locking failure
  • 422 Unprocessable Entity — syntactically valid JSON that fails business rules
  • 429 Too Many Requests — rate limited; include Retry-After

5xx server errors

  • 500 Internal Server Error — unexpected failure; log details server-side only
  • 503 Service Unavailable — maintenance or overload; use with Retry-After

Never return 200 OK with {"success": false} in the body. That breaks HTTP caching, monitoring, and client libraries.

Error response shape

Standardize errors so integrators can build one handler:

json
{
  "error": {
    "code": "validation_failed",
    "message": "Email is already registered",
    "details": [
      {"field": "email", "issue": "duplicate"}
    ],
    "request_id": "req_9xk2m"
  }
}

Include a stable code string (snake_case) for programmatic branching. Human-readable message text can change; code should not without a version bump. Always echo a request_id from your tracing system.

Paste sample responses into the /tools/json-formatter during design reviews to catch trailing commas and schema drift before they reach production.

Pagination, filtering, and sorting

Offset pagination is simple but degrades on large tables:

http
GET /v1/events?limit=50&offset=100

Cursor pagination scales better for feeds and audit logs:

http
GET /v1/events?limit=50&cursor=eyJpZCI6MTAwMH0

Response envelope:

json
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTA1MH0",
    "has_more": true
  }
}

Document filter operators explicitly. Prefer status=active,inactive over inventing a mini query language unless you have full-text search requirements.

Versioning strategies

Pick one approach and apply it everywhere:

  1. URL prefix/v1/... (most visible, easiest for logs and routing)
  2. HeaderAccept: application/vnd.myapp.v2+json (clean URLs, harder to test in browser)
  3. Query param/users?api_version=2 (generally avoid; caches treat query strings inconsistently)

When breaking changes are unavoidable, ship /v2 alongside /v1 with a published sunset date. Non-breaking additions (new optional fields) do not require a version bump if clients ignore unknown keys.

Authentication and authorization headers

Most APIs use bearer tokens:

http
GET /v1/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Document which endpoints accept API keys vs OAuth tokens. Never accept credentials in query strings — they end up in access logs and browser history.

For machine-to-machine calls, separate scopes from user permissions in documentation. A token with read:invoices should not silently grant write:invoices.

Content negotiation and headers

Default to Content-Type: application/json; charset=utf-8. Support If-None-Match and ETag on cacheable GET responses to save bandwidth:

http
HTTP/1.1 200 OK
ETag: "a1b2c3"
Cache-Control: private, max-age=60

Use Idempotency-Key on POST endpoints that create billable or side-effectful resources (payments, shipments):

http
POST /v1/charges HTTP/1.1
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

The server stores the key for 24 hours and returns the same response if the client retries.

Designing for integrators

Ship an OpenAPI 3 document generated from code or maintained as source of truth. Include:

  • Example requests and responses for every endpoint
  • Rate limit headers (X-RateLimit-Remaining, X-RateLimit-Reset)
  • Webhook payload schemas if you push events
  • Sandbox base URL with test API keys

Test flows manually with /tools/rest-client and /tools/api-request-tester before publishing docs — broken examples erode trust faster than missing features.

REST API design checklist

Before merging a new endpoint:

  • URL uses plural nouns and matches existing patterns
  • Correct HTTP method and idempotency documented
  • Status codes match outcome (no success flags in 200 bodies)
  • Error envelope includes code, message, and request_id
  • Pagination uses cursors for high-volume collections
  • Breaking changes gated behind a new API version
  • OpenAPI spec updated with runnable examples
  • Auth requirements stated per endpoint

Bulk and batch operations

When clients create or update many resources, avoid forcing N sequential POSTs. Offer a batch endpoint with partial success semantics:

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

{
  "operations": [
    {"method": "POST", "body": {"sku": "A1", "price_cents": 999}},
    {"method": "PATCH", "path": "/v1/products/prod_7", "body": {"price_cents": 1299}}
  ]
}

Return 207 Multi-Status or 200 with per-item results:

json
{
  "results": [
    {"status": 201, "id": "prod_9", "location": "/v1/products/prod_9"},
    {"status": 404, "error": {"code": "not_found", "message": "prod_7 missing"}}
  ]
}

Document maximum batch size (typically 25–100 items) and enforce it server-side to protect database connection pools.

Common mistakes to avoid

Leaking database internals. Exposing auto-increment integer IDs enables enumeration attacks. Prefer opaque IDs (usr_, UUIDs, or ULIDs).

Over-fetching by default. List endpoints should return summary objects; offer ?expand=line_items for detail when needed.

Inconsistent timestamps. Always store UTC, always serialize ISO-8601 with Z suffix: 2026-07-16T14:30:00Z.

Ignoring OPTIONS and CORS. Browser clients need explicit Access-Control-Allow-Methods and preflight handling on APIs meant for frontends.

Undocumented null vs omitted fields. State whether missing keys and explicit null mean the same thing on PATCH.

Conclusion

Great REST APIs optimize for the integrator's mental model: predictable URLs, honest status codes, and errors that explain what went wrong. Start with a small resource map, enforce conventions in code review, and publish examples you have actually executed.

Next steps: Draft an OpenAPI spec for one service, run every example through /tools/rest-client, and read the GraphQL vs REST comparison if you are deciding whether REST fits your client needs. For rate limiting patterns, see the API rate limiting guide.

Put what you learned into practice — no signup required.