| name | rest-best-practices |
| description | REST conventions tuned for mobile clients -- resources, HTTP caching, idempotency, and versioning. Use when designing or reviewing RESTful endpoints. |
REST Best Practices for Mobile
Instructions
REST for mobile is less about purity and more about predictability. Mobile apps retry, reconnect, and live across months of OS and app updates. Follow these rules so the contract ages well.
1. Resource-Oriented URLs
Model real things. Avoid verbs in paths.
- Good:
POST /v1/orders, GET /v1/orders/{id}, POST /v1/orders/{id}/cancel
- Bad:
POST /v1/createOrder, GET /v1/getOrderById?id=...
Sub-actions that do not map to CRUD (e.g., cancel, retry, approve) get a sub-resource with POST: POST /v1/orders/{id}/cancel. The body may be empty or carry context (reason codes).
2. HTTP Method Semantics
| Method | Safe | Idempotent | Typical use |
|---|
| GET | yes | yes | read |
| HEAD | yes | yes | metadata |
| PUT | no | yes | full replace |
| PATCH | no | no (usually) | partial update |
| DELETE | no | yes | remove |
| POST | no | no | create / action |
Use PATCH with JSON Merge Patch (RFC 7396) for partial updates; document semantics explicitly.
3. Status Codes
Use a small, consistent set. Do not invent 4xx meanings.
200 OK -- successful read or action with body.
201 Created -- resource created; include Location header and the created entity in the body.
202 Accepted -- async work queued; body holds a job id / status URL.
204 No Content -- successful mutation with no body.
400 INVALID_REQUEST / 422 VALIDATION_FAILED -- bad shape vs bad values.
401 UNAUTHENTICATED / 403 FORBIDDEN -- identity vs authorization.
404 NOT_FOUND, 409 CONFLICT, 410 GONE.
412 PRECONDITION_FAILED with If-Match etag mismatch.
429 RATE_LIMITED with Retry-After.
5xx -- server bugs or dependencies.
4. Caching
Turn caching on, deliberately.
Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "v42-abc"
Last-Modified: Sun, 12 Apr 2026 10:00:00 GMT
Clients revalidate with If-None-Match: "v42-abc"; server returns 304 Not Modified without a body on miss. Save bytes and battery.
Server example (Go, net/http):
func getArticle(w http.ResponseWriter, r *http.Request) {
a, etag, err := store.Get(r.PathValue("id"))
if err != nil { httpError(w, err); return }
if match := r.Header.Get("If-None-Match"); match == etag {
w.WriteHeader(http.StatusNotModified); return
}
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "private, max-age=60")
json.NewEncoder(w).Encode(a)
}
Client (Swift, URLSession): the system cache honors ETag when URLRequest.cachePolicy is .useProtocolCachePolicy and URLSessionConfiguration.urlCache is set.
5. Idempotency
PUT and DELETE are idempotent by spec. POST is not -- add an Idempotency-Key header on every POST that creates or charges.
POST /v1/payments
Idempotency-Key: 01HX7MJ4Z7SD6N8Q4QB4J8YD3M
Content-Type: application/json
Server stores (key, request_hash) -> response with TTL ≥ 24 h. Replays return the stored response; body mismatch returns 409 IDEMPOTENCY_KEY_CONFLICT. See the token-strategy skill for UUID/ULID generation on clients.
6. Versioning
Use a URL major version (/v1/). Within a major, evolve additively: new optional fields, new endpoints, new enum values. Breaking changes get /v2/.
- Deprecation window ≥ 6 months.
- Emit
Deprecation: true and Sunset: <http-date> headers on deprecated endpoints.
- Document every change in a machine-readable OpenAPI diff.
7. Concurrency Control
For editable resources, round-trip the ETag:
GET /v1/profile -> ETag: "v7"
PATCH /v1/profile
If-Match: "v7"
On mismatch, reply 412 PRECONDITION_FAILED. The client fetches the latest version, re-applies the edit, and retries.
8. Long-Running Operations
Return 202 Accepted with a pointer:
{ "job_id": "job_01HX7...", "status_url": "/v1/jobs/job_01HX7..." }
Clients poll the status URL with exponential backoff (see rate-limiting skill) or subscribe via WebSocket if available.
Checklist