DEV Community

Cover image for REST API Design: A Comprehensive Guide
AnkitDevCode
AnkitDevCode

Posted on • Edited on

REST API Design: A Comprehensive Guide

Understanding REST Fundamentals

REST works on top of the HTTP protocol, where each URI represents a resource. Because of this, endpoints should use nouns, not verbs. An RPC-style endpoint like /api/v1/getStudents becomes simply /api/v1/students in REST. The distinction between actions is handled by HTTP methods — GET, POST, PUT, PATCH, and DELETE — rather than the URL itself.

A REST endpoint is a unique URI that represents a resource. For example, https://demo.app/api/v1/students is a REST endpoint, where /api/v1/students is the path and students is the resource.

REST does not maintain server-side state — it only transfers state between server and client, which is the origin of the name REpresentational State Transfer. REST also leverages HTTP cache control, making responses cacheable because every representation is self-descriptive.

REST operates using three key components:

  • Resources and URIs — the nouns that identify what you're working with
  • HTTP methods — the verbs that define the action
  • HATEOAS — hypermedia links that guide clients dynamically

HTTP Methods

The five primary HTTP methods map to standard CRUD operations:

Method Operation Notes
POST Create (or search) Use for search when filter params exceed GET limits
GET Read Should be side-effect free
PUT Full update / replace Replaces the entire resource
PATCH Partial update Updates only the provided fields
DELETE Delete Should be idempotent

Some organisations also expose the HEAD method to retrieve only response headers without a body — useful for checking resource existence or metadata. For example:

curl --head https://api.github.com/users
Enter fullscreen mode Exit fullscreen mode

Note: REST does not strictly mandate which method maps to which operation, but the conventions above are widely adopted across the industry.

POST

POST is normally used for create operations. There are two notable exceptions where POST is acceptable for reads:

  1. Long filter criteriaGET query strings are limited to around 2,048 characters. When filter parameters exceed this, POST is a reasonable alternative.
  2. Sensitive parameters — if input parameters contain private data, POST with HTTPS keeps them out of the URL and server logs.

A successful create operation should return 201 Created.

GET

GET is used for read operations. It should never modify server state. Successful responses return 200 OK when data is present, or 204 No Content when there is nothing to return.

PUT

PUT is used for full update or replace operations. It replaces the entire resource with the provided representation. Successful responses return 200 OK with data or 204 No Content without.

DELETE

DELETE removes a resource. For example, DELETE /licenses/agpl-3.0 deletes the resource identified by the agpl-3.0 key. A successful delete returns 204 No Content.

PATCH

PATCH is used for partial updates — only the supplied fields are changed. A successful response returns 200 OK. Unlike PUT, you do not need to send the full resource representation.


HTTP Status Codes

Status codes fall into five categories:

Range Category
100–199 Informational
200–299 Success
300–399 Redirection
400–499 Client errors
500–599 Server errors

Commonly Used Codes

2xx — Success

  • 200 OK — Request succeeded. Used for GET, PUT, and PATCH responses with a body.
  • 201 Created — Resource was successfully created.
  • 202 Accepted — Request received but processing is deferred (e.g., async or batch jobs).
  • 204 No Content — Request succeeded with no response body.

3xx — Redirection

  • 304 Not Modified — Resource hasn't changed; client should use its cached copy.

4xx — Client Errors

  • 400 Bad Request — Missing, malformed, or invalid input parameters.
  • 401 Unauthorized — Request is unauthenticated (despite the misleading name).
  • 403 Forbidden — Authenticated but not authorised to perform the action.
  • 404 Not Found — The requested resource does not exist.
  • 405 Method Not Allowed — The HTTP method is not supported for this endpoint.
  • 409 Conflict — Duplicate create or a state conflict (e.g., version mismatch).
  • 429 Too Many Requests — Rate limit has been exceeded.

5xx — Server Errors

  • 500 Internal Server Error — A generic, unexpected server-side failure.
  • 502 Bad Gateway — An upstream dependency (e.g., a payment provider) returned an error.
  • 503 Service Unavailable — The server is temporarily unable to handle requests (overload or maintenance).

HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) means that a REST API dynamically provides links to related actions and resources within its responses — rather than requiring clients to construct URLs themselves.

A client starts from a single known URL and discovers everything else through the hypermedia links in each response. This has two key benefits:

  • Clients don't need hardcoded knowledge of the API's URL structure.
  • When endpoint paths change, clients pick up the new URLs automatically through the links.

Best Practices

1. Use Nouns for Resource Names

HTTP methods already supply the verb. Adding a verb to the URL is redundant and produces RPC-style paths:

# Wrong (RPC style)
GET /getLicenses
POST /createUser

# Correct (REST style)
GET /licenses
POST /users
Enter fullscreen mode Exit fullscreen mode

2. Use Plural Names for Collections

/licenses   ✓
/license    ✗
Enter fullscreen mode Exit fullscreen mode

A GET /licenses call returns a collection. Plural naming makes the intent clear and consistent regardless of whether you're fetching one or many.

3. Version Your APIs

APIs evolve over time, and existing clients may depend on older behaviour. Always include a version identifier so you can introduce breaking changes without affecting existing integrations.

Option A — Version in the path (most common):

https://demo.app/api/v1/students
Enter fullscreen mode Exit fullscreen mode

Clients always know exactly which version they're calling.

Option B — Version in the Accept header (GitHub's approach):

Accept: application/vnd.github.v3+json
Enter fullscreen mode Exit fullscreen mode

Allows a default version when no header is present, but requires clients to update the header when upgrading.

Either approach works — the important thing is to pick one and apply it consistently from day one.

4. Model Nested Resources

When a resource belongs to another, reflect that in the URL hierarchy:

GET    /customers/1/addresses       # all addresses for customer 1
GET    /customers/1/addresses/2     # address 2 of customer 1
POST   /customers/1/addresses       # add a new address
PUT    /customers/1/addresses/2     # replace address 2
PATCH  /customers/1/addresses/2     # partially update address 2
DELETE /customers/1/addresses/2     # delete address 2
Enter fullscreen mode Exit fullscreen mode

For resources that exist independently (e.g., payments in a microservices architecture), a top-level endpoint is often cleaner:

GET /payments/1    # instead of /orders/1/payments/1
Enter fullscreen mode Exit fullscreen mode

HATEOAS helps here — the order response can include a payment link rather than the client needing to know the URL structure upfront.

5. Secure Your APIs

  • Always use HTTPS — never expose REST APIs over plain HTTP.
  • Use JWT or OAuth 2.0 tokens for authentication. REST is stateless, so cookies and server-side sessions are not appropriate.
  • Regularly review OWASP's API Security Top 10 for current threats and mitigations.
  • Validate all inputs on the server side regardless of client-side validation.

6. Implement Caching

HTTP provides two standard mechanisms for cache validation:

ETag — the server returns a hash of the response body in the ETag header. The client sends it back as If-None-Match on subsequent requests. If the resource hasn't changed, the server returns 304 Not Modified and saves the bandwidth of resending the body.

Last-Modified — the server returns the resource's last modification timestamp. The client sends it back as If-Modified-Since. The server returns 304 if nothing has changed since that timestamp. This is less precise than ETag and should be used as a fallback.

7. Enforce Rate Limiting

Use 429 Too Many Requests when a client exceeds its allowed request quota. Communicate rate limit status via response headers so clients can adapt:

Header Meaning Example
X-Ratelimit-Limit Requests allowed per period 60
X-Ratelimit-Remaining Requests left in current period 55
X-Ratelimit-Used Requests used in current period 5
X-Ratelimit-Reset Seconds until the period resets 1601299930

8. Support Filtering, Sorting, and Pagination

For endpoints that return collections, always support query parameters to avoid returning unbounded result sets:

GET /products?category=electronics&sort=price       # filter + sort
GET /users?page=1&size=20                           # pagination
GET /orders?status=pending&sort=created_at&page=2  # combined
Enter fullscreen mode Exit fullscreen mode

9. Maintain Documentation

Good documentation is part of your API contract. It should:

  • Stay in sync with the current implementation, including version history.
  • Include code samples and example request/response pairs.
  • Clearly document deprecated endpoints and provide migration paths.
  • Be generated from the code where possible (e.g., OpenAPI/Swagger) to reduce drift.

10. Return Consistent Error Responses

Error responses should follow a predictable structure so clients can handle them programmatically:

{
  "status": 400,
  "error": "Bad Request",
  "message": "The 'email' field is required.",
  "path": "/api/v1/users"
}
Enter fullscreen mode Exit fullscreen mode

Consistency here dramatically reduces the integration effort for API consumers.


Quick Reference

Concern Recommendation
URL naming Nouns, plural, lowercase, hyphen-separated
HTTP methods Match semantics: GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes
Status codes Use the most specific code; avoid overusing 200 for everything
Versioning Include from day one (/api/v1/)
Auth JWT or OAuth 2.0 — no cookies or sessions
Caching Use ETag or Last-Modified headers
Rate limiting Return 429 with X-Ratelimit-* headers
Pagination ?page=&size= query params on collection endpoints
Documentation OpenAPI/Swagger, kept current, with examples
Error format Consistent JSON structure across all endpoints

Reference:

Top comments (0)