REST API Fundamentals
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
| Method | Use Case | Safe | Idempotent | Request Body | Response Body | Common Status Codes |
|---|---|---|---|---|---|---|
| GET | Read resource | ✓ | ✓ | No | Yes | 200, 404 |
| POST | Create resource | ✗ | ✗ | Yes | Yes (created) | 201, 400, 422 |
| PUT | Replace resource | ✗ | ✓ | Yes (complete) | Optional | 200, 204, 404 |
| PATCH | Update resource | ✗ | ✓ | Yes (partial) | Optional | 200, 204, 404 |
| DELETE | Remove resource | ✗ | ✓ | No | Optional | 204, 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.
| Code | Meaning | Use Case | When to Use |
|---|---|---|---|
| 200 OK | Success with response body | GET, PUT, PATCH successful | Standard success response with data |
| 201 Created | Resource created | POST created new resource | Include Location header |
| 202 Accepted | Accepted for async processing | Long-running operations | Return job status URL |
| 204 No Content | Success with no response body | DELETE, PUT/PATCH when no response needed | Client 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.
| Code | Meaning | Use Case | Example |
|---|---|---|---|
| 400 Bad Request | Malformed request | Invalid JSON, wrong field types | {"amount": "not-a-number"} |
| 401 Unauthorized | Not authenticated | Missing or invalid credentials | No Authorization header |
| 403 Forbidden | Authenticated but not authorized | User lacks permission | User can't delete other users |
| 404 Not Found | Resource doesn't exist | Invalid ID or deleted resource | /orders/999 not found |
| 405 Method Not Allowed | HTTP method not supported | GET on create-only endpoint | GET on /orders (POST only) |
| 409 Conflict | State conflict | Duplicate resource, version mismatch | Email already registered |
| 422 Unprocessable Entity | Business logic validation failed | Valid syntax, invalid business rules | Insufficient balance |
| 429 Too Many Requests | Rate limit exceeded | Client exceeded quota | Too 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.
| Code | Meaning | Use Case | When to Return |
|---|---|---|---|
| 500 Internal Server Error | Unexpected server failure | Unhandled exceptions | Catch-all for unexpected errors |
| 502 Bad Gateway | Upstream service returned invalid response | Microservice dependency failure | Dependent API returned garbage |
| 503 Service Unavailable | Temporarily unavailable | Maintenance, overload, circuit breaker open | Planned downtime |
| 504 Gateway Timeout | Upstream service timeout | Slow dependency service | Dependent 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
- REST Patterns - Pagination, filtering, sorting, searching
- REST Versioning - API versioning strategies
- API Contracts - Request/response models, validation, contract-first development
- API Patterns - Error handling, caching, async operations
- OpenAPI Specifications - Writing OpenAPI specs
- Contract Testing - Validating API contracts
- Spring Boot API Design - Spring Boot implementation
External Resources
- REST Dissertation - Roy Fielding's original REST definition
- Microsoft REST API Guidelines
- Richardson Maturity Model - Martin Fowler
- RESTful Web Services - O'Reilly book
- RFC 7231 - HTTP Semantics - Official HTTP specification
Summary
Key Takeaways:
- Design around resources - URIs represent nouns (business entities), HTTP methods are verbs
- Use HTTP correctly - GET is safe/idempotent, POST creates, PUT replaces, PATCH updates, DELETE removes
- Return meaningful status codes - 2xx success, 4xx client error (not retriable), 5xx server error (retriable)
- Resource naming - Plural nouns, kebab-case, 3 levels max
- Predictable patterns - Consistent URI structure across all endpoints
- Target Level 2 - Most practical APIs use resources + HTTP verbs, not full HATEOAS
- Understand idempotency - GET, PUT, DELETE are idempotent; POST is not
- Content negotiation - Use
AcceptandContent-Typeheaders, not file extensions - 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.