Implementing RESTful APIs
1. Designing Resource-Based URLs
| Pattern | Good | Bad |
|---|---|---|
| Collection | GET /orders | GET /getOrders |
| Item | GET /orders/{id} | GET /order?id=1 |
| Sub-resource | GET /orders/{id}/items | GET /orderItems?orderId=1 |
| Action (non-CRUD) | POST /orders/{id}/cancel | POST /cancelOrder |
| Naming | Plural nouns, kebab-case | Verbs, snake_case |
2. Using HTTP Methods Correctly
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Read | Yes | Yes |
| POST | Create / non-idempotent action | No | No |
| PUT | Replace entire resource | Yes | No |
| PATCH | Partial update | Should be | No |
| DELETE | Remove resource | Yes | No |
| HEAD | Headers only | Yes | Yes |
| OPTIONS | Discover allowed methods/CORS | Yes | Yes |
3. Implementing HTTP Status Codes
| Code | Name | Use |
|---|---|---|
| 200 | OK | Successful GET/PUT/PATCH |
| 201 | Created | POST that created a resource (return Location) |
| 202 | Accepted | Async work started |
| 204 | No Content | Successful DELETE / empty body |
| 400 | Bad Request | Validation failure |
| 401 | Unauthorized | Missing/invalid auth |
| 403 | Forbidden | Auth ok, not allowed |
| 404 | Not Found | Resource missing |
| 409 | Conflict | Version/state conflict |
| 422 | Unprocessable Entity | Semantic validation failure |
| 429 | Too Many Requests | Rate limit hit |
| 500 | Internal Server Error | Unhandled server fault |
| 502/503/504 | Bad GW / Unavailable / Timeout | Upstream issues |
4. Designing Idempotent Operations
| Technique | How |
|---|---|
| Idempotency-Key Header | Client sends UUID; server stores result for replay |
| Natural Keys | PUT uses client-supplied ID |
| Conditional Writes | If-Match with ETag for optimistic concurrency |
| Dedup Window | TTL store of processed keys (Redis) |
Example: Idempotency-Key (Spring Boot 3)
@PostMapping("/payments")
public ResponseEntity<Payment> create(@RequestHeader("Idempotency-Key") String key,
@RequestBody PaymentRequest req) {
return idempotencyStore.findResult(key)
.map(prev -> ResponseEntity.ok((Payment) prev))
.orElseGet(() -> {
Payment p = paymentService.charge(req);
idempotencyStore.save(key, p, Duration.ofHours(24));
return ResponseEntity.status(201).body(p);
});
}
5. Implementing Content Negotiation
| Header | Purpose | Example |
|---|---|---|
| Accept | Client requests media type | application/json, application/vnd.api+json |
| Content-Type | Body format being sent | application/json; charset=utf-8 |
| Accept-Language | i18n preference | en-US, en;q=0.9 |
| Accept-Encoding | Compression | gzip, br |
| Vary | Tells caches which headers vary response | Accept, Accept-Encoding |
6. Using HATEOAS Principles
| Concept | Description |
|---|---|
| HATEOAS | Hypermedia As The Engine Of Application State |
| Links | Self, next, related resources embedded |
| Formats | HAL, JSON:API, Siren |
| Benefit | Discoverable APIs; client doesn't hard-code URLs |
Example: HAL response
{
"id": "ord_123",
"status": "PAID",
"_links": {
"self": { "href": "/orders/ord_123" },
"items": { "href": "/orders/ord_123/items" },
"cancel": { "href": "/orders/ord_123/cancel", "method": "POST" }
}
}
7. Implementing Pagination
| Style | Params | Trade-offs |
|---|---|---|
| Offset/Limit | ?page=2&size=20 | Easy; slow on deep pages |
| Cursor (Keyset) | ?after=xyz&limit=20 | O(1) deep paging; stable |
| Time-based | ?since=2026-01-01T00:00Z | Good for streams |
| Headers | X-Total-Count, Link: rel="next" | RFC 5988 navigation |
8. Implementing Filtering and Sorting
| Need | Pattern |
|---|---|
| Filter equality | ?status=PAID¤cy=USD |
| Filter range | ?createdAt[gte]=2026-01-01 |
| Multi-value | ?status=PAID,PENDING |
| Sorting | ?sort=-createdAt,total (- = desc) |
| Search | ?q=john (free text) |
| Standardized | RSQL/FIQL: ?filter=status==PAID;total=gt=100 |
9. Handling Partial Responses
| Pattern | Example |
|---|---|
| Field selection | ?fields=id,name,email |
| Sparse fieldsets (JSON:API) | ?fields[users]=name,email |
| GraphQL | Client picks fields natively |
| Embed | ?embed=address,orders to inline relations |
10. Using ETags for Caching
| Header | Purpose |
|---|---|
| ETag | Opaque resource version (e.g. "a1b2") |
| If-None-Match | GET; server returns 304 if unchanged |
| If-Match | PUT/PATCH; 412 if version mismatch |
| Last-Modified | Time-based alt; pair with If-Modified-Since |
| Strong vs Weak | "abc" = byte-equal; W/"abc" = semantically equal |
11. Implementing Bulk Operations
| Pattern | Description |
|---|---|
| Batch endpoint | POST /orders/batch with array |
| Per-item status | Return 207 Multi-Status with per-item codes |
| Atomicity | All-or-nothing flag; or partial success |
| Async batch | 202 Accepted + status URL |
| Bulk patch | JSON Patch (RFC 6902): [{op,path,value}] |
12. Implementing Request/Response Compression
| Algorithm | Header | Notes |
|---|---|---|
| gzip | Accept-Encoding: gzip | Universal support |
| br (Brotli) | Accept-Encoding: br | Better ratio, modern clients |
| zstd | Accept-Encoding: zstd | Fast, growing support |
| Tip | Skip compression for < 1 KB or already-compressed payloads (images) | |