Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

REST API Design

Designing APIs that developers love to use

REST (Representational State Transfer) is an architectural style for designing web APIs that was introduced by Roy Fielding in his 2000 doctoral dissertation. REST provides a set of constraints and principles that, when followed, create APIs that are scalable, maintainable, and easy to understand.

REST is built on the idea that web APIs should work like the web itself—using standard HTTP methods to manipulate resources identified by URLs. Instead of creating custom protocols or action-based endpoints, REST uses the existing HTTP infrastructure in a standardized way.

Key Insight: REST treats everything as a resource—a noun that can be identified by a URL. Actions are performed using standard HTTP methods (verbs). This separation of concerns makes APIs predictable and intuitive.

Diagram
  1. Stateless - Each request contains all information needed
  2. Resource-Based - Everything is a resource (noun)
  3. HTTP Methods - Use standard HTTP verbs (GET, POST, etc.)
  4. Uniform Interface - Consistent way to interact with resources
  5. Cacheable - Responses can be cached
  6. Client-Server - Separation of concerns

Resources are the core of REST. A resource is anything that can be identified and manipulated.

Good resources are:

  • Nouns, not verbs (/users, not /getUsers) - Resources represent entities, not actions
  • Hierarchical (/users/123/orders) - Reflect relationships between resources
  • Plural for collections (/users, not /user) - Collections are plural, individual resources are identified by ID
  • Consistent naming conventions - Use the same naming pattern throughout your API
Diagram
GoodBadWhy?
/users/getUsersVerbs in URL violate REST principles. Use GET method instead.
/users/123/user/123Inconsistent plural. Collections should be plural for clarity.
/users/123/orders/orders?userId=123Better hierarchy. Shows relationship between user and orders.
/products/456/product?id=456Resource identifier should be in path, not query parameter.

Diagram

HTTP methods define what action to perform on a resource.

OperationHTTP MethodIdempotent?Safe?Use Case
CreatePOSTNoNoCreate new resource
ReadGETYesYesRetrieve resource
Update (Full)PUTYesNoReplace entire resource
Update (Partial)PATCHNo*NoUpdate part of resource
DeleteDELETEYesNoRemove resource

*PATCH can be idempotent if designed correctly

GET is for reading data. It’s safe (no side effects) and idempotent.

GET /users/123
GET /users?status=active&page=1
GET /users/123/orders

Characteristics:

  • No request body (usually) - GET requests typically don’t have bodies, though HTTP spec allows it
  • Can be cached - GET responses are cacheable by default, improving performance
  • Should not modify data - GET is a safe method that should only retrieve data, never change server state
  • Idempotent (safe to retry) - Calling GET multiple times produces the same result without side effects

POST is for creating new resources. It’s not idempotent (calling twice creates two resources).

POST /users
Content-Type: application/json
{
"name": "John Doe",
"email": "[email protected]"
}

Characteristics:

  • Not idempotent (creates new resource each time) - Calling POST twice creates two resources. Use idempotency keys to make it idempotent.
  • Not safe (modifies server state) - POST changes server state by creating new resources
  • Returns 201 Created with Location header - Indicates successful creation and provides URL to new resource
  • Request body contains data - POST includes the resource data in the request body

Response:

HTTP/1.1 201 Created
Location: /users/456
Content-Type: application/json
{
"id": 456,
"name": "John Doe",
"email": "[email protected]"
}

PUT replaces an entire resource. It’s idempotent (calling twice has same effect as once).

PUT /users/123
Content-Type: application/json
{
"name": "Jane Doe",
"email": "[email protected]",
"status": "active"
}

Characteristics:

  • Idempotent (same result if called multiple times) - Calling PUT with same data multiple times has same effect as calling once
  • Creates resource if it doesn’t exist (some APIs) - PUT can be used for both create and update, though POST is more common for creation
  • Replaces entire resource - PUT replaces all fields. Missing fields may be set to null/default values
  • Use when you have full resource data - PUT requires complete resource representation, not partial updates

PATCH updates part of a resource. Should be idempotent if designed correctly.

PATCH /users/123
Content-Type: application/json
{
"name": "Jane Doe"
}

Characteristics:

  • Can be idempotent (should be designed that way) - PATCH can be made idempotent by including version numbers or using conditional updates
  • Only updates specified fields - PATCH updates only the fields provided, leaving others unchanged
  • More efficient than PUT (less data) - Sends only changed fields, reducing bandwidth
  • Use when you have partial data - PATCH is ideal when you only know some fields to update

DELETE removes a resource. It’s idempotent (deleting twice = same as once).

DELETE /users/123

Characteristics:

  • Idempotent (deleting non-existent = same result) - Deleting an already-deleted resource has the same effect as deleting it once
  • Usually returns 204 No Content - Successful deletion typically returns no body, just status code
  • Can return 404 if already deleted (still idempotent) - Some APIs return 404 for already-deleted resources, which is still idempotent behavior

Diagram

Status codes communicate the result of the request. Use them correctly!

CodeMeaningUse Case
200 OKRequest succeededGET, PUT, PATCH
201 CreatedResource createdPOST (with Location header)
204 No ContentSuccess, no bodyDELETE, PUT (sometimes)
CodeMeaningUse Case
400 Bad RequestInvalid requestMalformed JSON, missing fields
401 UnauthorizedNot authenticatedMissing/invalid token
403 ForbiddenNot authorizedValid token, but no permission
404 Not FoundResource doesn’t existInvalid ID, wrong URL
409 ConflictResource conflictDuplicate email, version conflict
429 Too Many RequestsRate limitedToo many requests
CodeMeaningUse Case
500 Internal Server ErrorServer errorUnexpected exception
502 Bad GatewayUpstream errorDownstream service failed
503 Service UnavailableService downMaintenance, overloaded

Do:

  • Use 201 for successful creation - Clearly indicates resource was created
  • Use 204 for successful deletion - Indicates success with no content to return
  • Use 400 for client errors (bad input) - Client sent invalid data
  • Use 404 for not found - Resource doesn’t exist
  • Use 409 for conflicts - Resource conflict (e.g., duplicate email)

Don’t:

  • Return 200 for errors (use 4xx/5xx) - Always use appropriate error status codes
  • Return 500 for client errors (use 4xx) - 500 is for server errors, not client mistakes
  • Return 200 for creation (use 201) - Use 201 to clearly indicate creation success

Diagram

Versioning allows you to evolve your API without breaking existing clients.

Version in the URL path:

/api/v1/users
/api/v2/users

Pros:

  • Explicit and clear - Version is visible in URL, making it obvious which version is being used
  • Easy to route - Simple to route different versions to different handlers
  • Cacheable - URLs with versions can be cached independently
  • Most common approach - Widely understood and used across the industry

Cons:

  • URLs change - Clients must update URLs when upgrading versions
  • More maintenance - Need to maintain multiple version endpoints

Version in HTTP headers:

GET /users
Accept: application/vnd.api+json;version=1

Pros:

  • Clean URLs - URLs remain unchanged across versions
  • No URL changes - Clients don’t need to update URLs when upgrading

Cons:

  • Less discoverable - Version information hidden in headers, harder to see
  • Harder to cache - Caching must consider headers, making it more complex
  • More complex - Requires header parsing and version negotiation logic

Version as query parameter:

/api/users?version=1

Pros:

  • Simple - Easy to implement and understand
  • Optional - Can be omitted for default version

Cons:

  • Easy to forget - Clients might forget to include version parameter
  • Not RESTful (version isn’t a resource property) - Version is metadata, not a resource attribute

Diagram

Bad:

POST /users/123/delete
GET /users/create?name=John
POST /users/123/update

Why it’s bad: Uses verbs in URLs and wrong HTTP methods. Actions should be expressed through HTTP methods, not URL paths.

Good:

DELETE /users/123
POST /users (with body)
PUT /users/123 (with body)

Why it’s good: Uses standard HTTP methods correctly. DELETE for deletion, POST for creation, PUT for updates.

Bad:

/getUsers
/createUser
/updateUser
/deleteUser

Why it’s bad: URLs contain verbs, violating REST principles. The HTTP method already indicates the action.

Good:

GET /users
POST /users
PUT /users/123
DELETE /users/123

Why it’s good: URLs contain only nouns (resources). Actions are expressed through HTTP methods.

Bad:

/user
/order

Why it’s bad: Singular nouns for collections are inconsistent and confusing. Is /user a single user or collection?

Good:

/users
/orders

Why it’s good: Plural nouns clearly indicate collections. Individual resources are identified by ID: /users/123.

Bad:

/orders?userId=123

Why it’s bad: Uses query parameters to express relationships. Less intuitive and doesn’t show resource hierarchy.

Good:

/users/123/orders

Why it’s good: Hierarchical URL clearly shows that orders belong to a user. More intuitive and RESTful.

Bad:

/users
/customers
/clients

Why it’s bad: Inconsistent naming for the same concept. Confusing for API consumers who must remember different terms.

Good:

/users (consistent across API)

Why it’s good: Consistent naming throughout the API. Once developers learn the pattern, they can predict other endpoints.

Bad:

// Always returns 200, even for errors
{
"success": false,
"error": "User not found"
}

Why it’s bad: Always returning 200 makes it impossible to use HTTP status codes for error handling. Clients must parse response body to detect errors.

Good:

HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "User not found",
"code": "USER_NOT_FOUND"
}

Why it’s good: Uses appropriate HTTP status code (404) for not found. Clients can handle errors based on status codes. Response body provides additional context.

Bad:

GET /users // Returns 10,000 users

Why it’s bad: Returns all resources at once, causing performance issues, high memory usage, and slow response times.

Good:

GET /users?page=1&limit=20
GET /users?offset=0&limit=20
GET /users?cursor=abc123&limit=20

Why it’s good: Pagination limits response size, improves performance, and reduces memory usage. Supports different pagination strategies (page-based, offset-based, cursor-based).

Response:

{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 1000,
"hasNext": true
}
}
GET /users?status=active&role=admin&sort=name&order=asc
GET /users?search=john&limit=10

9. Use HATEOAS (Hypermedia as the Engine of Application State)

Section titled “9. Use HATEOAS (Hypermedia as the Engine of Application State)”

Include links to related resources:

{
"id": 123,
"name": "John Doe",
"links": {
"self": "/users/123",
"orders": "/users/123/orders",
"profile": "/users/123/profile"
}
}

Diagram

At the code level, REST APIs translate to controllers, services, and DTOs.


Make POST requests idempotent using idempotency keys:

POST /orders
Idempotency-Key: abc123-xyz789
Content-Type: application/json
{
"productId": 456,
"quantity": 2
}

Server behavior:

  • First request: Create order, store idempotency key
  • Duplicate request: Return same order (don’t create new one)

Support multiple formats:

GET /users/123
Accept: application/json
GET /users/123
Accept: application/xml

Let clients choose fields:

GET /users/123?fields=id,name,email

Resources are Nouns

Use nouns for resources, HTTP methods for actions. /users with GET, not /getUsers.

Proper Status Codes

Use appropriate status codes. 201 for creation, 404 for not found, 400 for bad requests.

Idempotency Matters

GET, PUT, DELETE are idempotent. Design POST/PATCH to be idempotent when possible.

Layered Architecture

Controllers handle HTTP, Services handle business logic, Repositories handle data access.