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