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
| Method | Purpose | Idempotent? | Safe? | Cacheable? |
|---|---|---|---|---|
| GET | Retrieve resource | Yes | Yes | Yes |
| POST | Create new resource | No | No | No |
| PUT | Replace entire resource | Yes | No | Only 201/204 |
| PATCH | Partial update | No* | No | No |
| DELETE | Remove resource | Yes | No | No |
| HEAD | Like GET, no body | Yes | Yes | Yes |
| OPTIONS | Describe communication options | Yes | Yes | Yes |
*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
- ❌ Non-RESTful
- ✅ RESTful
# 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
# Resources as nouns, methods as verbs
POST /users
Content-Type: application/json
{ "name": "Alice" }
# Response: 201 Created, Location: /users/456
GET /users/456
# Response: 200 OK
{ "id": 456, "name": "Alice" }
GET /users?status=active&limit=10
# Response: 200 OK with paginated list
PUT /users/456
{ "id": 456, "name": "Alice Johnson", "email": "alice@example.com" }
# Response: 200 OK (full replacement)
PATCH /users/456
{ "email": "alice.johnson@example.com" }
# Response: 200 OK (partial update)
DELETE /users/456
# Response: 204 No Content
GET /users/456
# Response: 404 Not Found
Benefits: Predictable URIs. Cacheable requests. Standard behavior.
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/456idempotent butPOST /users/456/orderis 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)
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