Skip to main content

REST API Fundamentals

API Design Philosophy

A well-designed API is intuitive, self-documenting, and stable. Design APIs around resources (nouns), use HTTP methods correctly, and think about the developer experience. Poor API design creates friction and makes integration painful.

What is REST?

REST (Representational State Transfer) is an architectural style for distributed hypermedia systems, introduced by Roy Fielding in his 2000 doctoral dissertation. RESTful APIs use HTTP as their protocol and treat everything as a resource that can be created, read, updated, or deleted.

Core REST Principles

Resources: Everything is a resource (users, orders, products) identified by URIs. Resources represent business entities in your domain - they're nouns, not actions.

Stateless: Each request must contain all information needed to understand and process it. The server maintains no client session state between requests. All session state resides on the client.

Uniform Interface: Use standard HTTP methods (GET, POST, PUT, PATCH, DELETE) consistently across all resources. This uniformity makes APIs predictable and reduces learning curve.

Representations: Resources can have multiple representations (JSON, XML, CSV). Clients specify desired format via the Accept header, and servers indicate format with Content-Type.

Hypermedia (HATEOAS): Responses include links to related resources, enabling client navigation. While theoretically powerful, full HATEOAS is rarely implemented in practice.

Important: REST isn't a strict standard - it's a set of constraints and patterns. Real-world APIs often don't implement every principle (especially HATEOAS), focusing instead on resource-oriented design and proper HTTP semantics. Most practical REST APIs target what Roy Fielding calls "Level 2" maturity: resources + HTTP verbs.


Organize API Design Around Resources

Resources are the fundamental building blocks of REST APIs. Think of your API in terms of business entities (resources), not actions. Resources represent something meaningful in your domain.

Resource-Oriented URI Design

URIs identify resources, not actions. The HTTP method determines the action.

 GOOD: Resource-oriented
GET /orders # Get all orders
POST /orders # Create new order
GET /orders/123 # Get specific order
PUT /orders/123 # Update order 123
PATCH /orders/123 # Partially update order 123
DELETE /orders/123 # Delete order 123

BAD: Action-oriented (RPC-style)
POST /createOrder
POST /getOrderById
POST /updateOrder
POST /deleteOrder
GET /getUsers

Why this matters: Resource-oriented URIs are predictable. Once developers understand your pattern, they can guess endpoints without documentation. Action-oriented URIs require memorizing every endpoint. The HTTP verb (GET, POST, PUT, DELETE) already describes the action - the URI should describe the resource.

URI Structure

Use a hierarchical structure that reflects resource relationships:

/collection/{identifier}/sub-collection/{identifier}

Examples:

GET /customers/C123                    # Customer C123
GET /customers/C123/orders # Orders for customer C123
GET /customers/C123/orders/O456 # Specific order for customer C123
GET /customers/C123/orders/O456/items # Items in that order

Collections vs. singletons:

  • Collections are plural: /orders, /products, /users
  • Singletons reference a specific instance: /orders/123

Naming Conventions

Use plural nouns for collections:

 GOOD
GET /orders
GET /products
GET /payment-methods

BAD
GET /order # Singular
GET /getProducts # Verb in URI
GET /paymentMethod # Inconsistent casing

Use kebab-case for multi-word resources:

 GOOD
GET /payment-methods
GET /account-statements
GET /transaction-limits

BAD
GET /paymentMethods # camelCase
GET /AccountStatements # PascalCase
GET /payment_methods # snake_case
GET /PAYMENT-METHODS # uppercase

Rationale: URLs are case-sensitive. Lowercase with hyphens (kebab-case) is most readable, least error-prone, and aligns with DNS naming conventions.

Avoid Overly Complex URIs

Don't model every relationship in the URI. Beyond 3 levels, URIs become unwieldy and difficult to maintain.

 BAD: Too deep (5+ levels)
GET /organizations/org1/departments/d1/teams/t1/members/m1/tasks/t1

GOOD: BETTER: Flat with filtering
GET /tasks/t1
GET /tasks?memberId=m1
GET /tasks?memberId=m1&teamId=t1

Use query parameters for filtering, sorting, and searching rather than deeply nested URIs. Deep nesting creates brittle URLs that break when relationships change. See REST Patterns for filtering strategies.

Denormalization for Convenience

Sometimes it's useful to provide multiple ways to access the same resource for different contexts:

GET /orders/O456                  # Access by order ID (canonical)
GET /customers/C123/orders/O456 # Access in customer context

Both return the same resource, but the second provides context about the customer relationship. This denormalization improves developer experience by matching common access patterns.

Avoid File Extensions

Don't use file extensions in URLs. Use content negotiation instead.

 GOOD
GET /orders/123
Accept: application/json

BAD
GET /orders/123.json
GET /users/456.xml

File extensions violate REST principles and complicate routing. Use the Accept header to specify desired response format.


Define Operations with HTTP Methods

HTTP methods define operations on resources. Use them according to their defined semantics to enable caching, idempotency, and predictable behavior.

Understanding Idempotency and Safety

These properties are foundational to HTTP's reliability and cacheability:

Safe methods do not modify server state. They are read-only operations that can be called repeatedly without side effects. This allows clients to safely retry requests and intermediary caches to serve responses without revalidating.

Idempotent methods produce the same result regardless of how many times they're called. Calling an idempotent method once or 100 times results in the same server state. This property is critical for reliability - if a client loses connection after sending a request, it can safely retry an idempotent operation without causing duplicate effects.

Why idempotency matters: Network requests can fail after being sent but before receiving a response. Idempotency allows clients to retry safely. Non-idempotent operations (POST) require special handling like idempotency keys to enable safe retries.

GET - Retrieve Resources

GET retrieves a resource or collection without side effects. It's safe (doesn't modify data) and idempotent (calling it multiple times produces the same result).

Single resource retrieval:

GET /orders/123
Accept: application/json

200 OK
Content-Type: application/json
Cache-Control: max-age=3600

{
"orderId": "123",
"customerId": "C456",
"total": 99.99,
"status": "shipped"
}

Collection retrieval:

GET /orders?status=pending&page=0&size=20

200 OK
Content-Type: application/json

{
"items": [
{ "orderId": "123", "status": "pending", "total": 99.99 },
{ "orderId": "124", "status": "pending", "total": 149.50 }
],
"page": {
"number": 0,
"size": 20,
"totalElements": 150
}
}

Why GET is safe and idempotent: Clients and intermediaries (CDNs, proxies, browsers) can cache GET responses aggressively. Browsers can prefetch GET requests. Search engine crawlers freely follow GET links.

Critical security rule: Never use GET for operations that modify data. This violates HTTP semantics and creates security vulnerabilities (CSRF attacks). Bank account transfers via GET /transfer?from=123&to=456&amount=1000 are trivially exploitable.

POST - Create Resources

POST creates a new resource or triggers an operation. The server assigns the resource identifier. POST is not idempotent - calling it twice creates two resources.

POST /orders
Content-Type: application/json

{
"customerId": "C456",
"items": [
{ "productId": "P789", "quantity": 2 }
]
}

201 Created
Location: /orders/125
Content-Type: application/json

{
"orderId": "125",
"customerId": "C456",
"status": "pending",
"createdAt": "2025-01-15T10:30:00Z",
"total": 199.98
}

Key behaviors:

  • Returns 201 Created on success
  • Includes Location header pointing to the created resource
  • Response body contains the created resource with server-generated fields (ID, timestamps, calculated fields)
  • Multiple identical POSTs create multiple resources (not idempotent)

POST for other operations: POST is also used for operations that don't fit other verbs: complex searches (search criteria in request body), batch operations, controller/action-style endpoints (e.g., /orders/123/cancel).

For idempotent creation, use PUT (if client controls the ID) or implement idempotency keys. See REST Patterns - Idempotency for implementation details.

PUT - Replace Resources

PUT replaces the entire resource with the provided representation. It's idempotent - sending the same PUT request multiple times results in the same state.

PUT /orders/125
Content-Type: application/json

{
"customerId": "C456",
"status": "confirmed",
"items": [
{ "productId": "P789", "quantity": 2 }
]
}

200 OK
Content-Type: application/json

{
"orderId": "125",
"customerId": "C456",
"status": "confirmed",
"items": [
{ "productId": "P789", "quantity": 2 }
],
"updatedAt": "2025-01-15T11:00:00Z"
}

PUT semantics: PUT requires the complete resource representation. If you omit a field, it should be removed or reset to default. This "replace everything" semantic ensures idempotency but can be inefficient when you only need to update one field.

PUT vs POST:

  • PUT replaces entire resource (full representation required)
  • PUT is idempotent; POST is not
  • PUT is typically for updates; POST for creation
  • PUT can create if resource doesn't exist (if client provides ID)

PATCH - Partial Updates

PATCH updates specific fields without replacing the entire resource. It's more efficient than PUT when changing only a few fields.

PATCH /orders/125
Content-Type: application/json

{
"status": "shipped"
}

200 OK
Content-Type: application/json

{
"orderId": "125",
"customerId": "C456",
"status": "shipped",
"items": [
{ "productId": "P789", "quantity": 2 }
],
"updatedAt": "2025-01-15T12:00:00Z"
}

PATCH semantics: Only the fields in the request are modified. Other fields remain unchanged. PATCH is not inherently idempotent in the HTTP specification (RFC 5789), but can be implemented as idempotent.

PATCH vs PUT:

  • PATCH: Partial update, send only changed fields
  • PUT: Full replacement, send complete resource

When to use PATCH: Preferred over PUT for most updates since clients rarely need to replace entire resources. Reduces bandwidth and simplifies client code.

DELETE - Remove Resources

DELETE removes a resource. It's idempotent - deleting an already-deleted resource produces the same result (resource doesn't exist).

DELETE /orders/125

204 No Content

Common status codes:

  • 204 No Content: Successfully deleted, no response body (most common)
  • 200 OK: Successfully deleted, returns deleted resource representation
  • 404 Not Found: Resource already doesn't exist (still considered successful due to idempotency - goal achieved)
  • 202 Accepted: Deletion queued for async processing

Idempotency in practice: The first DELETE removes the resource (returns 204 or 200). Subsequent DELETEs to the same resource return 404 (resource not found) or 204 (deletion succeeded, resource doesn't exist). Both outcomes satisfy the delete intent, making DELETE idempotent.

Method Summary Table

MethodUse CaseSafeIdempotentRequest BodyResponse BodyCommon Status Codes
GETRead resourceNoYes200, 404
POSTCreate resourceYesYes (created)201, 400, 422
PUTReplace resourceYes (complete)Optional200, 204, 404
PATCHUpdate resourceYes (partial)Optional200, 204, 404
DELETERemove resourceNoOptional204, 200, 404

HTTP Status Codes

Status codes communicate the outcome of a request. Use the right code so clients can handle responses programmatically.

Success Codes (2xx)

Success codes indicate the request was received, understood, and processed successfully.

CodeMeaningUse CaseWhen to Use
200 OKSuccess with response bodyGET, PUT, PATCH successfulStandard success response with data
201 CreatedResource createdPOST created new resourceInclude Location header
202 AcceptedAccepted for async processingLong-running operationsReturn job status URL
204 No ContentSuccess with no response bodyDELETE, PUT/PATCH when no response neededClient doesn't need returned data

200 vs 204: Use 200 when returning updated resource data to client. Use 204 when client doesn't need confirmation data (saves bandwidth).

Client Error Codes (4xx)

Client errors are not retriable - the client must fix the request before retrying.

CodeMeaningUse CaseExample
400 Bad RequestMalformed requestInvalid JSON, wrong field types{"amount": "not-a-number"}
401 UnauthorizedNot authenticatedMissing or invalid credentialsNo Authorization header
403 ForbiddenAuthenticated but not authorizedUser lacks permissionUser can't delete other users
404 Not FoundResource doesn't existInvalid ID or deleted resource/orders/999 not found
405 Method Not AllowedHTTP method not supportedGET on create-only endpointGET on /orders (POST only)
409 ConflictState conflictDuplicate resource, version mismatchEmail already registered
422 Unprocessable EntityBusiness logic validation failedValid syntax, invalid business rulesInsufficient balance
429 Too Many RequestsRate limit exceededClient exceeded quotaToo many login attempts

401 vs 403: Use 401 when the client isn't authenticated (needs to log in). Use 403 when the client is authenticated but doesn't have permission (logged in, but not admin).

400 vs 422: Use 400 for structural/syntax errors (invalid JSON, wrong field types, malformed data). Use 422 for semantic/business logic failures (valid request structure, but business rules prevent processing - e.g., valid payment request but insufficient funds).

Server Error Codes (5xx)

Server errors are retriable - the client did nothing wrong, the server failed. Clients should implement exponential backoff for retries.

CodeMeaningUse CaseWhen to Return
500 Internal Server ErrorUnexpected server failureUnhandled exceptionsCatch-all for unexpected errors
502 Bad GatewayUpstream service returned invalid responseMicroservice dependency failureDependent API returned garbage
503 Service UnavailableTemporarily unavailableMaintenance, overload, circuit breaker openPlanned downtime
504 Gateway TimeoutUpstream service timeoutSlow dependency serviceDependent API didn't respond

Important security rule: Never expose internal error details in 5xx responses. Log the full error (including stack trace) server-side for debugging, but return a generic message to clients. Exposing internal details aids attackers.

// BAD: Exposes internal details
{
"error": "NullPointerException at PaymentService.java:142",
"query": "SELECT * FROM payments WHERE id = 'PAY-123'"
}

// GOOD: Generic message, log details internally
{
"error": "Internal Server Error",
"message": "An unexpected error occurred. Please contact support with request ID: req-abc-123",
"requestId": "req-abc-123",
"timestamp": "2025-01-15T10:30:00Z"
}

For detailed error handling patterns, see API Patterns - Error Handling.


Content Negotiation

Clients specify the desired response format using the Accept header. Servers indicate the actual format with the Content-Type header.

GET /orders/123
Accept: application/json

200 OK
Content-Type: application/json

{
"orderId": "123",
"total": 99.99
}

Multiple acceptable formats:

GET /orders/123
Accept: application/json, application/xml;q=0.9

200 OK
Content-Type: application/json

The q parameter (quality value) indicates preference. Higher values (max 1.0) are preferred.

406 Not Acceptable

Return 406 Not Acceptable if the requested format isn't supported:

GET /orders/123
Accept: application/pdf

406 Not Acceptable
Content-Type: application/json

{
"error": "Not Acceptable",
"message": "Supported media types: application/json, application/xml"
}

Recommendation: Support JSON (application/json) as the primary format for modern APIs. Add other formats (XML, CSV, PDF) only if there's a clear business need. Most APIs are JSON-only.


Richardson Maturity Model

The Richardson Maturity Model describes four levels of REST maturity, helping teams understand where their API falls on the REST spectrum.

Level 0: The Swamp of POX (Plain Old XML)

  • Single URI, single HTTP method (typically POST)
  • All operations tunneled through POST
  • Example: SOAP, XML-RPC
  • Not RESTful

Level 1: Resources

  • Multiple URIs, but still single HTTP method
  • Each resource has its own URI
  • Still using POST for everything
  • Improvement over Level 0, but not REST

Level 2: HTTP Verbs Most Common

  • Multiple URIs with correct HTTP methods
  • Use GET for retrieval, POST for creation, PUT/PATCH for updates, DELETE for removal
  • Use appropriate status codes
  • This is where most "REST APIs" actually are

Level 3: Hypermedia Controls (HATEOAS)

  • Include hypermedia links in responses
  • Clients discover available actions dynamically
  • Very few APIs reach this level (GitHub API, PayPal API are examples)

Practical advice: Target Level 2 for most APIs. Level 3 (HATEOAS) adds significant complexity that often doesn't provide proportional value for typical CRUD APIs. Focus on getting resources, HTTP verbs, and status codes right before considering hypermedia.


HATEOAS (Hypermedia)

HATEOAS (Hypermedia as the Engine of Application State) is the principle that responses should include links to related resources, enabling clients to discover available actions dynamically.

Example with hypermedia links:

{
"orderId": "123",
"status": "pending",
"total": 99.99,
"_links": {
"self": { "href": "/orders/123" },
"customer": { "href": "/customers/C456" },
"items": { "href": "/orders/123/items" },
"cancel": { "href": "/orders/123", "method": "DELETE" },
"confirm": { "href": "/orders/123/confirm", "method": "POST" }
}
}

Benefits:

  • Clients can navigate the API by following links rather than constructing URLs
  • Server can change URL structure without breaking clients (if clients use links)
  • Available actions are discoverable (e.g., "cancel" link only appears if order is cancellable)

Practical reality: Full HATEOAS is rarely implemented in REST APIs due to complexity. Most APIs use hypermedia selectively:

  • Navigation links (self, next, prev) for pagination
  • Related resource links (customer, items)
  • Document available operations in OpenAPI specs rather than response bodies

Recommendation: Include basic navigation links (self, pagination links). Document available operations in your API specification. Reserve full HATEOAS for public APIs where discoverability is critical.


Further Reading

Internal Documentation

External Resources


Summary

Key Takeaways:

  1. Design around resources - URIs represent nouns (business entities), HTTP methods are verbs
  2. Use HTTP correctly - GET is safe/idempotent, POST creates, PUT replaces, PATCH updates, DELETE removes
  3. Return meaningful status codes - 2xx success, 4xx client error (not retriable), 5xx server error (retriable)
  4. Resource naming - Plural nouns, kebab-case, 3 levels max
  5. Predictable patterns - Consistent URI structure across all endpoints
  6. Target Level 2 - Most practical APIs use resources + HTTP verbs, not full HATEOAS
  7. Understand idempotency - GET, PUT, DELETE are idempotent; POST is not
  8. Content negotiation - Use Accept and Content-Type headers, not file extensions
  9. Never expose internals in errors - Log details server-side, return generic messages to clients

Next Steps: Review REST Patterns for pagination, filtering, and searching strategies, then API Contracts for request/response modeling and validation.