Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions skills/rest-api-best-practices/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
name: rest-api-best-practices
description: REST API design guidance. Use when designing or reviewing REST endpoints, choosing HTTP methods and status codes, defining URL structure and naming conventions, formatting error responses, implementing pagination or filtering, versioning an API, securing endpoints with authentication or rate limiting, or reviewing whether an API exposes domain capabilities rather than raw CRUD operations.
license: MIT
metadata:
author: luckys
version: "1.0.0"
---

# REST API Best Practices

Use this skill when the main question is how to design, review, or improve a REST API — from URL structure to error formats to security.

## Working Style

1. Start from what the consumer needs, not from what the database looks like.
2. Use HTTP as designed — methods, status codes, and headers carry meaning.
3. Express domain capabilities, not data operations.
4. Make errors as informative as successes.
5. Design for change: version early, break compatibility deliberately.

## Design Workflow

1. **Define the resources.**
- What are the concepts the consumer needs to interact with?
- Name them with nouns, in plural form.
- Avoid verbs in URLs — the HTTP method is the verb.

2. **Choose the right HTTP method.**
- GET for retrieval (safe, idempotent, cacheable)
- POST for creation or non-idempotent operations
- PUT for full replacement (idempotent)
- PATCH for partial update
- DELETE for removal (idempotent)

3. **Design the URL.**
- Keep paths flat where possible — at most two levels of nesting.
- Use query parameters for filtering, sorting, and pagination.
- Use task URLs (`POST /orders/{id}/cancel`) for domain operations that aren't simple CRUD.

4. **Define response shapes.**
- Choose a consistent field naming convention (camelCase or snake_case) and stick to it.
- Return only what the consumer needs.
- Use HTTP headers for metadata (pagination, rate limits).

5. **Define error responses.**
- Use the right 4xx or 5xx status code — never return 200 for errors.
- Return a structured error body with a machine-readable code and a human-readable message.
- Follow RFC 9457 (Problem Details, which obsoletes RFC 7807) when the team needs a standard.

6. **Add versioning.**
- Default to URL versioning (`/v1/`).
- Treat a breaking change as a reason to bump the version, not to patch silently.

## Heuristics

### Resource Naming
Nouns, plural, lowercase, kebab-case for multi-word: `/user-profiles`, `/order-items`.

### HTTP Method Semantics
GET is safe and must never change state. POST creates or triggers actions. PUT replaces the entire resource. PATCH patches specific fields. DELETE removes.

### Status Code Choice
2xx = success. 4xx = client error. 5xx = server error. Never return 200 for a failed operation.

### Error Format
One consistent error envelope for all failures. Machine-readable `code` field, human-readable `message`, optional `errors` array for field-level validation details.

### Pagination
Default to cursor-based for large or real-time datasets. Offset-based is fine for small, bounded collections. Always include a `Link` header with navigation links.

### CRUD vs. Task-Based Design
Avoid exposing raw CRUD when the domain has real business operations. Prefer `POST /orders/{id}/cancel` over `PATCH /orders/{id}` with `{ "status": "cancelled" }`.

## Day-to-Day Rules

- Use plural nouns for collection endpoints.
- Never put verbs in URL paths — the HTTP method is the verb.
- Return 201 Created (not 200) after a successful POST that creates a resource.
- Include a `Location` header pointing to the new resource after 201.
- Return 204 No Content when there is nothing meaningful to return in the body.
- Never expose stack traces, SQL errors, or internal paths in error responses.
- Paginate every collection endpoint — never return an unbounded list.
- Use `Accept` and `Content-Type` headers for content negotiation.
- Put secrets in headers, never in query parameters.
- Always use HTTPS — never plain HTTP for API traffic.

## Good Signals

- URL paths read like a sentence: `GET /orders/{id}/items`.
- Every status code is intentional and precise.
- Errors have a machine-readable code the client can branch on.
- Pagination is consistent across all collection endpoints.
- Breaking changes require a version bump.
- The API surface reflects domain language, not table names.

## Warning Signs

- Verbs in URL paths: `/getUser`, `/createOrder`, `/deleteItem`.
- 200 OK returned for validation errors or business failures.
- Error bodies expose stack traces or raw SQL.
- Collections returned without pagination.
- Inconsistent field naming: sometimes `userId`, sometimes `user_id`.
- Every endpoint maps directly to a database table with full CRUD.
- No versioning strategy — breaking changes deployed silently.
- API key or token passed as a query parameter.
- Response objects nested more than two levels deep.
- No request ID header — errors cannot be correlated in logs.
- No documentation or documentation that doesn't match reality.

## References

- Read `references/url-design.md` for URL structure, naming conventions, nesting rules, versioning, and task-based URLs.
- Read `references/http-semantics.md` for HTTP method semantics, idempotency, safety, status code selection, and key headers.
- Read `references/request-response-design.md` for filtering, sorting, pagination (offset and cursor), content negotiation, and response shape.
- Read `references/error-handling.md` for RFC 9457 Problem Details, centralized mapping, status policy, redaction, domain failure translation, and validation errors.
- Read `references/security.md` for authentication (JWT, API keys, OAuth 2.0), authorization, rate limiting, CORS, and HTTPS enforcement.
- Read `references/api-design-philosophy.md` for REST architectural constraints, the CRUD anti-pattern, task-based API design, and HATEOAS.
- Read `references/observability.md` for request IDs, structured logging, metrics, distributed tracing, health endpoints, and alerting.
- Read `references/documentation.md` for what to document per endpoint, working examples, OpenAPI 3.x spec structure, keeping docs executable with contract testing, and changelog practices.
- Read `references/caching.md` for Cache-Control directives, ETag/Last-Modified validation, conditional writes (optimistic concurrency), cache layers, and invalidation strategies.
- Read `references/testing-apis.md` for the API test pyramid, contract testing (OpenAPI, Pact), what to test beyond the happy path, and test isolation.
- Read `references/language-examples.md` for an index of language-specific implementation examples.
- Read `references/typescript-examples.md` for REST API implementation in TypeScript (Express, NestJS).
- Read `references/python-examples.md` for REST API implementation in Python (FastAPI, Django REST Framework).
- Read `references/go-examples.md` for REST API implementation in Go (net/http, Chi).
- Read `references/php-examples.md` for REST API implementation in PHP (Laravel, Symfony).
- Read `references/java-examples.md` for REST API implementation in Java (Spring Boot 3.x).
- Read `references/rust-examples.md` for REST API implementation in Rust (axum).

## Related Skills

- Use `ddd-best-practices` when the API surface should reflect a Bounded Context or align with domain language.
- Use `design-patterns-best-practices` for internal patterns within the API implementation (Repository, Service Layer, etc.).
- Use `oop-best-practices` for controller and service design within the API layer.

## Source Influences

This skill is synthesized from:

- *REST API Design Rulebook* by Mark Masse
- *RESTful Web APIs* by Leonard Richardson & Mike Amundsen
- RFC 9457 — Problem Details for HTTP APIs
- RFC 9110 — HTTP Semantics
- Fran Iglesias — API REST series (franiglesias.github.io)
- Derek Comartin — "CRUD APIs are Poor Design" (codeopinion.com)
- Postman REST API Best Practices
- *Building Microservices* by Sam Newman
133 changes: 133 additions & 0 deletions skills/rest-api-best-practices/references/api-design-philosophy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# API Design Philosophy

## REST Architectural Constraints

REST (Representational State Transfer) is defined by six architectural constraints. Breaking them does not make an API wrong, but it does forfeit the guarantees REST provides.

### 1. Uniform Interface

The most important constraint. It has four sub-constraints:

1. **Resource identification** — resources are identified by URI, independent of their representation.
2. **Manipulation through representations** — clients interact with resources via representations (JSON, XML), not directly with the resource.
3. **Self-descriptive messages** — each message includes enough information to describe how to process it (Content-Type, status codes, Cache-Control).
4. **HATEOAS** — representations include links to available next actions.

### 2. Statelessness

Each request must contain all information needed to process it. The server holds no client session state between requests.

- Benefits: horizontal scalability, fault tolerance, simplicity.
- Implication: authentication credentials must be sent with every request.
- Never store "current user state" on the server to resume across requests.

### 3. Cacheability

Responses must explicitly declare whether they are cacheable. GET responses are cacheable by default unless told otherwise. Proper caching reduces load and improves perceived latency.

### 4. Client–Server Separation

The client and server are decoupled and can evolve independently. The only coupling is the API contract (URLs, methods, response shapes). The server should not know or care about the client's rendering or state.

### 5. Layered System

A client cannot tell whether it is talking to the origin server or an intermediary (load balancer, CDN, API gateway, caching proxy). APIs must behave identically regardless of intermediaries.

### 6. Code on Demand (optional)

Servers may optionally send executable code to clients (JavaScript). Rarely used in practice.

---

## CRUD APIs Are Poor Design

Exposing a direct CRUD interface over domain entities is the most common REST anti-pattern.

> "CRUD APIs and CRUD-driven systems — meaning systems built around Create, Read, Update, and Delete operations — are, in the long run, the hardest to change and evolve." — Derek Comartin

Problems with pure CRUD APIs:

**Couples clients to data structure.** A schema change (renaming a field, splitting a table) immediately breaks consumers. The API leaks the database model.

**Hides business intent.** `PATCH /orders/{id}` with `{ "status": "cancelled" }` and `{ "status": "shipped" }` look identical. The consumer must know what transitions are legal and what side effects they trigger (email sent? stock updated?).

**Misses invariants.** A rule like "an order cannot be cancelled after it ships" is invisible in a raw PATCH. The client must implement the guard — and different clients will implement it differently.

**Forces multi-step choreography.** A single business operation (place an order: validate stock, charge payment, send confirmation) becomes a sequence of CRUD calls that the client must orchestrate correctly.

**When pure CRUD is acceptable:**
- Truly data-centric resources with no business logic: configuration entries, reference data, admin panels on lookup tables.
- Internal tooling where the consumer controls both sides of the contract.

---

## Task-Based API Design

Design endpoints around business capabilities, not data rows.

```
# CRUD (poor for domain operations)
PATCH /orders/{id} { "status": "cancelled" }
PATCH /orders/{id} { "status": "shipped" }
PATCH /accounts/{id} { "active": false }

# Task-based (expresses domain intent)
POST /orders/{id}/cancel
POST /orders/{id}/ship
POST /accounts/{id}/deactivate
```

Benefits:
- The URL reads like the domain's ubiquitous language.
- Business rules live on the server: `cancel` enforces that you cannot cancel a shipped order.
- Clients express **intent**, not internal state mutations.
- Each action is independently versioned, documented, and tested.
- Side effects (events, notifications, stock updates) are encapsulated behind the action.

### Naming task URLs

Use the domain's verb in past tense or imperative:

```
POST /invoices/{id}/send
POST /invoices/{id}/void
POST /subscriptions/{id}/pause
POST /subscriptions/{id}/resume
POST /users/{id}/verify-email
POST /users/{id}/request-password-reset
```

---

## HATEOAS

Hypermedia As The Engine Of Application State: responses include links that guide the client to available next actions. The client discovers state transitions from the response rather than hard-coding them.

```json
{
"id": "ord_01J8X",
"status": "pending",
"total": { "amount": 120.00, "currency": "EUR" },
"_links": {
"self": { "href": "/orders/ord_01J8X" },
"cancel": { "href": "/orders/ord_01J8X/cancel", "method": "POST" },
"items": { "href": "/orders/ord_01J8X/items" },
"customer": { "href": "/users/usr_42" }
}
}
```

When the order is shipped, `cancel` disappears from `_links` — the client does not need to know the cancellation business rule; it just checks whether the link is present.

Full HATEOAS is rarely implemented in practice (the tooling and discipline cost is high). A pragmatic middle ground: include `_links` for navigation and action discovery without requiring full hypermedia compliance.

---

## API as a Product

- **Design for the consumer**, not for the implementation. The right surface is discovered by talking to consumers, not by mirroring the database.
- **Provide documentation before shipping** — an undocumented API is a broken API.
- **Treat breaking changes like database migrations**: they require coordination, a migration path, and a deadline.
- **Gather consumer feedback** early and continuously. The API surface you ship first is not the right one.
- **A stable contract is more valuable than an internally "correct" one.** Once consumers depend on a shape, stability trumps elegance.
- **Version proactively** — add versioning before the first consumer, not after the first breaking change.
Loading