Skip to main content

URI Design, HTTP Methods, and Status Codes

Master REST fundamentals with clear paths, meaningful verbs, and semantic status codes

TL;DR

REST APIs live at the intersection of URIs and HTTP semantics. URIs should identify resources (nouns: /users, /orders) not actions (avoid /createUser, /deleteOrder). HTTP methods communicate intent: GET retrieves, POST creates, PUT replaces, PATCH modifies, DELETE removes. Status codes signal outcomes: 2xx for success, 4xx for client errors, 5xx for server errors. Consistent application of these fundamentals makes APIs predictable and discoverable.

Learning Objectives

  • Design hierarchical, discoverable URI schemes
  • Apply HTTP methods correctly to prevent confusion and caching bugs
  • Choose appropriate status codes that guide client behavior
  • Understand idempotency and its implications
  • Recognize anti-patterns that undermine REST design

Motivating Scenario

A poorly designed API uses /createUser, /deleteUser, /updateUser endpoints, all via POST. Clients can't predict the URI pattern. Caches fail because POST is non-cacheable. Later, you add /getUser—inconsistency spreads.

A RESTful approach uses /users with GET (list), POST (create), and /users/{id} with GET (detail), PUT (replace), PATCH (modify), DELETE (remove). Patterns become obvious. Caches work. HTTP libraries and proxies understand your API automatically.

Core Concepts

URI Design Principles

Resources as nouns, not verbs: /users (collection), /users/123 (instance), /users/123/orders (nested resource). Never /createUser, /getOrder, /deleteComment.

Hierarchy reflects relationships: /users/123/addresses means addresses of user 123. /teams/456/projects/789/tasks shows nested ownership clearly.

Consistent singular/plural: Choose one style (/users or /user) and stick to it. Most modern APIs use plural for collections.

Query parameters for filters/options: /users?status=active&role=admin&limit=20 isolates concerns from resource paths.

HTTP Methods and Idempotency

MethodPurposeIdempotent?Safe?Cacheable?
GETRetrieve resourceYesYesYes
POSTCreate new resourceNoNoNo
PUTReplace entire resourceYesNoOnly 201/204
PATCHPartial updateNo*NoNo
DELETERemove resourceYesNoNo
HEADLike GET, no bodyYesYesYes
OPTIONSDescribe communication optionsYesYesYes

*PATCH can be idempotent if designed carefully.

Idempotency: Repeating a request produces the same result as a single request. GET, PUT, DELETE are idempotent. POST and PATCH are not. This matters for retries and network failures.

Status Code Families

2xx Success:

  • 200 OK: General success with response body
  • 201 Created: Resource created; body contains new resource
  • 202 Accepted: Request accepted but processing asynchronously
  • 204 No Content: Success but no response body

4xx Client Error:

  • 400 Bad Request: Malformed syntax or invalid parameters
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Authenticated but lacks permissions
  • 404 Not Found: Resource does not exist
  • 409 Conflict: Request conflicts with current state (e.g., optimistic locking)
  • 422 Unprocessable Entity: Semantically invalid (e.g., validation errors)

5xx Server Error:

  • 500 Internal Server Error: Unexpected condition
  • 503 Service Unavailable: Temporarily unable to handle requests

Practical Example

# Verbs in URIs, confusing conventions
POST /api/createUser { "name": "Alice" }
POST /api/getUser?id=123
POST /api/deleteUser?id=123
POST /api/updateUserEmail?id=123&email=new@example.com

# Client can't predict URIs or know if requests are repeatable
# Caches don't work (all POST)
# No standard status codes used

When to Deviate

POST for complex operations: If a resource action doesn't fit CRUD, use POST to /users/456/send-password-reset. The POST body describes the operation.

Batch operations: POST /users/bulk-import with array payload is clearer than individual POSTs.

Search/filtering complexity: When filters get complex, POST /search with rich query body beats URL parameter limits.

Patterns and Pitfalls

Pitfall: Using PUT for partial updates. PUT replaces the entire resource; use PATCH for partial updates.

Pitfall: Returning 200 for POST create. Use 201 Created with Location header. Clients expect this convention.

Pitfall: Not using 204 No Content. After DELETE or successful PATCH with no response body, send 204, not 200.

Pitfall: Ignoring status code semantics. 404 means "resource not found," not "endpoint doesn't exist." Use 403 when resource exists but user lacks permission.

Pattern: Use Location header in 201 responses. Clients immediately know the resource URI without parsing the body.

Design Review Checklist

  • All URIs use nouns (resources), not verbs (actions)
  • Collection endpoints use plural form consistently
  • Hierarchical URIs reflect resource relationships
  • GET requests are idempotent and safe
  • POST creates new resources (201 Created with Location header)
  • PUT replaces entire resources (idempotent)
  • PATCH modifies partial resources
  • DELETE removes resources (idempotent)
  • Status codes used semantically (200, 201, 204, 4xx, 5xx)
  • Query parameters used for filtering/pagination, not in resource paths

HTTP Status Code Deep Dive

2xx Success Codes

200 OK: General success, request succeeded with response body.

GET /users/123 → 200 OK (user returned)
POST /users → 201 (not 200, different meaning)

201 Created: Resource created successfully.

POST /users → 201 Created
Location: /users/456
{
"id": 456,
"name": "New User"
}

Always include Location header with new resource URI.

202 Accepted: Request accepted but processing asynchronously.

POST /long-running-task → 202 Accepted
Location: /tasks/789
{ "status": "processing" }

Client can poll status endpoint later.

204 No Content: Success but no response body.

DELETE /users/456 → 204 No Content
PUT /users/456 → 204 No Content (if not returning updated resource)

4xx Client Error Codes

400 Bad Request: Malformed syntax or invalid parameters.

POST /users
{ "name": "" } // Empty name invalid
→ 400 Bad Request
{ "error": "name is required" }

401 Unauthorized: Authentication required (no credentials or invalid credentials).

GET /users/456 (no auth header)
→ 401 Unauthorized
WWW-Authenticate: Bearer

403 Forbidden: Authenticated but lacks permission (resource exists, user can't access).

GET /users/999 (you're not user 999 and not admin)
→ 403 Forbidden

404 Not Found: Resource doesn't exist.

GET /users/999999
→ 404 Not Found

409 Conflict: Request conflicts with current state (optimistic locking).

PUT /users/123 (version mismatch)
→ 409 Conflict
{ "error": "resource has been updated" }

422 Unprocessable Entity: Semantically invalid request.

POST /users
{ "name": "Alice", "age": -5 } // Negative age invalid
→ 422 Unprocessable Entity
{ "error": "age must be positive" }

5xx Server Error Codes

500 Internal Server Error: Unexpected server error.

Should rarely occur in production
Log the error, alert ops, return generic message to client

503 Service Unavailable: Temporarily unable to handle requests (maintenance, overload).

PUT /database → 503 Service Unavailable
Retry-After: 60 (server will be ready in 60 seconds)

Advanced URI Patterns

Hierarchical Resource Relationships

/teams/acme/projects/web-app/tasks/123
├─ /teams/acme (team resource)
├─ /teams/acme/projects (projects of team)
├─ /teams/acme/projects/web-app (specific project)
├─ /teams/acme/projects/web-app/tasks (tasks in project)
└─ /teams/acme/projects/web-app/tasks/123 (specific task)

Hierarchy shows ownership and scope

Query Parameters for Filtering

GET /products?status=active&category=electronics&min_price=100&max_price=1000&sort=price_asc&limit=20

Filters belong in query string, not resource path
Status: {active, inactive, draft}
Category: {electronics, clothing, ...}
Sort: {price_asc, price_desc, created_asc, created_desc}
Pagination: {offset, limit}

Resource Collections vs Items

POST /users → 201 Created (create new user)
GET /users → 200 OK (list users)
GET /users/123 → 200 OK (get specific user)
PUT /users/123 → 200 OK (replace user)
DELETE /users/123 → 204 No Content (delete user)

Never: POST /users/123, DELETE /users (ambiguous what happens)

Self-Check

  • What status code should a successful POST create endpoint return? What header must accompany it? (201 Created + Location header)
  • Why is DELETE /users/456 idempotent but POST /users/456/order is not? (DELETE removes same resource repeatedly; POST creates new resource each time)
  • When would you use PATCH instead of PUT? (PATCH for partial updates; PUT for full replacement)
  • How would you represent a complex action like "send password reset"? (POST /users/456/send-password-reset)
  • What status if request succeeds but processing continues asynchronously? (202 Accepted)
One Takeaway

URIs identify resources; HTTP methods express intent; status codes signal outcomes. Use them consistently, and HTTP becomes your API's ally, not an obstacle. Master status codes especially—they guide client behavior and retries.

Next Steps

  • Read Filtering, Sorting, Pagination for handling query parameters effectively
  • Study Error Formats & Problem Details for consistent error responses
  • Explore Versioning Strategies when you need to evolve your API

URI Design Evolution and Versioning

API Versioning Strategies

Strategy 1: URL Path Versioning

GET /v1/users/123
GET /v2/users/123 # Different response format

Pros: Explicit, easy to understand
Cons: Duplication, multiple endpoints to maintain

Strategy 2: Header-Based Versioning

GET /users/123
Accept: application/vnd.company.v1+json
Accept: application/vnd.company.v2+json

Pros: Single endpoint, cleaner URIs
Cons: Less discoverable, requires documentation

Strategy 3: Query Parameter Versioning

GET /users/123?api_version=1
GET /users/123?api_version=2

Pros: Simple, queryable
Cons: Can be overlooked, mixing concerns

Strategy 4: No Versioning (Backward Compatible Evolution)

GET /users/123 (always returns latest format)

Design rules:
- Add fields (don't remove)
- Keep field meanings consistent
- Use deprecation headers for removal

Header: Deprecation: true, Sunset: Sun, 25 Aug 2026 00:00:00 GMT

Pros: Cleanest, forces good design
Cons: Requires discipline, limited flexibility

Request/Response Examples in Detail

Example 1: Create Resource

POST /users
Content-Type: application/json

{
"name": "Alice",
"email": "alice@example.com"
}
---

HTTP/1.1 201 Created
Location: /users/456
Content-Type: application/json

{
"id": 456,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2025-09-10T14:00:00Z"
}

Key points:
- Method: POST (not POSTV, POST-create)
- Status: 201 Created (not 200)
- Location header: resource URI
- Response includes created resource

Example 2: Update Resource (Full Replacement)

PUT /users/456
Content-Type: application/json

{
"id": 456,
"name": "Alice Johnson",
"email": "alice.johnson@example.com",
"status": "active"
}
---

HTTP/1.1 200 OK

{
"id": 456,
"name": "Alice Johnson",
"email": "alice.johnson@example.com",
"status": "active"
}

Key points:
- Method: PUT (idempotent)
- Send complete resource
- 200 OK or 204 No Content acceptable
- PUT replaces entire resource

Example 3: Partial Update

PATCH /users/456
Content-Type: application/json

{
"email": "newemail@example.com"
}
---

HTTP/1.1 200 OK

{
"id": 456,
"name": "Alice Johnson",
"email": "newemail@example.com",
"status": "active"
}

Key points:
- Method: PATCH (partial update)
- Send only changed fields
- Server merges with existing data
- Idempotency depends on implementation

Example 4: Error Response

POST /users
Content-Type: application/json

{
"name": "",
"email": "not-an-email"
}
---

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
"error": "Validation Failed",
"message": "Request body validation failed",
"details": {
"name": ["cannot be empty"],
"email": ["invalid email format"]
}
}

Key points:
- 422 for validation errors (not 400)
- Include details about what failed
- Client knows what to fix

URI Query Parameter Best Practices

Filtering

GET /products?status=active&category=electronics

Multiple values:
GET /products?category=electronics&category=clothing&category=home

Or array syntax:
GET /products?categories[]=electronics&categories[]=clothing

Sorting

GET /products?sort=price_asc
GET /products?sort=created_desc,price_asc

Standard format:
GET /products?sort=created&order=asc
GET /products?sort=created&order=desc

Pagination

Limit/Offset:
GET /products?offset=100&limit=20 (page 6, items 100-119)

Cursor:
GET /products?cursor=abc123&limit=20 (opaque cursor, server maintains state)

Page Number:
GET /products?page=6&page_size=20

Sparse Fields

GET /products/123?fields=id,name,price

Server returns only requested fields (reduce bandwidth):
{
"id": 123,
"name": "Widget",
"price": 9.99
}

Search/Full-Text

GET /products?search=wireless+headphones

Or with OR/AND:
GET /products?search=headphones+OR+speakers

References

  • HTTP/1.1 Semantics and Content (RFC 7231)
  • RESTful Web Services (Leonard Richardson & Sam Ruby)
  • REST API Best Practices (httpwg.org)
  • OpenAPI Specification for URI standardization