docs(P03): complete domain-derived-docs phase
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# Error Responses — Derived Rules
|
||||
|
||||
> Derives from `domains/api/first-principles.md` P9 (Error Transparency) and `domains/errors/first-principles.md`.
|
||||
|
||||
## The Error Contract
|
||||
|
||||
Every error response is a JSON object with:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "STRING_ERROR_CODE",
|
||||
"message": "Human-readable description",
|
||||
"details": {},
|
||||
"request_id": "uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `code`: machine-consumable, stable, UPPER_SNAKE_CASE. Never a free-text message.
|
||||
- `message`: human-readable, for logs and developers. Not for end users (see `domains/errors/` P8).
|
||||
- `details`: structured, typed additional context (which field, what value, what constraint).
|
||||
- `request_id`: correlation ID for tracing. Every error is traceable.
|
||||
|
||||
## Error Codes (P3 Predictability, P9)
|
||||
|
||||
- Codes are stable. Renaming an error code is a breaking change.
|
||||
- Codes are specific: `VALIDATION_FAILED` not `BAD_REQUEST`. `DUPLICATE_EMAIL` not `CONFLICT`.
|
||||
- Codes are namespaced: `USER_NOT_FOUND`, `ORDER_NOT_FOUND` — not just `NOT_FOUND`.
|
||||
|
||||
## Status Code Mapping (P1 Contract Fidelity)
|
||||
|
||||
| Code | Meaning | Error code example |
|
||||
|------|---------|-------------------|
|
||||
| 400 | Malformed request | `MALFORMED_REQUEST` |
|
||||
| 401 | Auth required | `AUTH_REQUIRED` |
|
||||
| 403 | Forbidden | `FORBIDDEN` |
|
||||
| 404 | Not found | `<RESOURCE>_NOT_FOUND` |
|
||||
| 409 | Conflict | `DUPLICATE_<RESOURCE>` |
|
||||
| 422 | Semantic invalid | `VALIDATION_FAILED` |
|
||||
| 429 | Rate limited | `RATE_LIMITED` |
|
||||
| 500 | Server bug | `INTERNAL_ERROR` |
|
||||
|
||||
- Never return 200 with an error body. The status code is the first signal.
|
||||
- Never return 500 for a client error. 500 means "the server has a bug."
|
||||
|
||||
## Information Disclosure (P8 Security, domains/security P9)
|
||||
|
||||
- Error messages do not leak internal state: no stack traces, no SQL fragments, no file paths.
|
||||
- A 401 does not say "user not found" vs "wrong password" — both say "invalid credentials."
|
||||
- A 404 does not confirm the resource exists but is forbidden — return 404, not 403, for unauthenticated requests to hidden resources.
|
||||
- Detailed errors are logged server-side with `request_id`; the client gets the safe version.
|
||||
|
||||
## Retryability (P6 Idempotency)
|
||||
|
||||
- Errors that are safe to retry: 409, 422 (if the fix is applied), 429 (after backoff), 5xx.
|
||||
- Errors that are not safe to retry: 400, 401 (without re-auth), 403.
|
||||
- The error body indicates retryability: `retryable: true/false` or via the code's known semantics.
|
||||
|
||||
## Partial Errors (GraphQL, see `domains/api/graphql.md`)
|
||||
|
||||
- GraphQL returns data and errors together. Do not conflate.
|
||||
- A null field with no error is a bug. A null field with an error is a partial failure.
|
||||
@@ -0,0 +1,53 @@
|
||||
# GraphQL — Derived Rules
|
||||
|
||||
> Derives from `domains/api/first-principles.md`. Applies P1–P10 to GraphQL specifically.
|
||||
|
||||
## Schema First (P1 Contract Fidelity)
|
||||
|
||||
- The schema is the contract. Every field has a type, a description, and a deprecation status.
|
||||
- The schema is versioned. Breaking schema changes (removing a field, changing a type) require a deprecation cycle.
|
||||
- Never expose raw database types in the schema. Map them to domain types.
|
||||
|
||||
## Query Design (P2 Clarity, P3 Predictability)
|
||||
|
||||
- Field names are nouns, camelCase: `userOrders`, not `UserOrders` or `user_orders`.
|
||||
- Arguments are descriptive: `first`, `after`, `orderBy` — not `arg1`, `arg2`.
|
||||
- Connections for lists: `users(first: 10, after: "cursor")` — never bare arrays.
|
||||
- Mutations are verbs: `createUser`, `deleteOrder` — not `userCreate`.
|
||||
|
||||
## N+1 Prevention (P7 Performance)
|
||||
|
||||
- Use a dataloader for every list field that resolves to another resource.
|
||||
- A resolver that does a database query per item is an N+1 bug.
|
||||
- Test resolvers under a list query, not just a single-item query.
|
||||
|
||||
## Authorization at the Field Level (P8 Security)
|
||||
|
||||
- Every resolver checks authorization. The query graph is not a trust boundary by default.
|
||||
- A user who can query `user { email }` is not automatically authorized to query `user { passwordHash }`.
|
||||
- Field-level authz is the floor, not an optimization.
|
||||
|
||||
## Deprecation (P5 Versioning, P10 Stability)
|
||||
|
||||
- Deprecate fields with `@deprecated(reason: "...")`. Never remove a field without deprecating first.
|
||||
- A deprecated field is removed in the next major schema version, not sooner.
|
||||
- Track field usage. A deprecated field with no usage can be removed sooner.
|
||||
|
||||
## Error Handling (P9 Error Transparency)
|
||||
|
||||
- Errors are partial by default: a query can return data and errors simultaneously.
|
||||
- Errors are structured: `{ message, path, extensions: { code, ... } }`.
|
||||
- Use `extensions.code` for machine-consumable error types, not free-text messages.
|
||||
- Never swallow a resolver error silently. A null field with no error is a bug.
|
||||
|
||||
## Complexity Budget (P7 Performance, P8 Security)
|
||||
|
||||
- Enforce a query complexity limit. Unbounded depth/breadth is a DoS vector.
|
||||
- Cost-based analysis (not just depth) catches expensive nested queries.
|
||||
- Reject queries over budget with a 400, not a 500.
|
||||
|
||||
## Federation (P6 Composability)
|
||||
|
||||
- A federated subgraph owns its entities. Cross-graph references use `@external` and `@requires`.
|
||||
- Never reach into another subgraph's database. The graph boundary is the contract.
|
||||
- The gateway composes; subgraphs do not know about each other.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Pagination — Derived Rules
|
||||
|
||||
> Derives from `domains/api/first-principles.md` P7 (Performance) and P3 (Predictability).
|
||||
|
||||
## Three Patterns
|
||||
|
||||
### 1. Offset/Limit (`?page=2&limit=20`)
|
||||
- Simple, supports jumping to a page.
|
||||
- Unstable under inserts: page 2 becomes page 1's content after an insert.
|
||||
- Slow for large offsets: `OFFSET 10000` scans 10000 rows.
|
||||
- Use for: small, stable collections, admin UIs.
|
||||
|
||||
### 2. Cursor (`?cursor=base64token&limit=20`)
|
||||
- Stable under inserts: the cursor points to a position, not a page number.
|
||||
- Fast: indexed lookup, no scan.
|
||||
- No random access (cannot jump to page 5).
|
||||
- Use for: infinite scroll, feeds, large collections, anything user-facing.
|
||||
|
||||
### 3. Keyset (`?after_id=123&limit=20`)
|
||||
- Like cursor but uses the actual sort key (e.g., `after_id=123`).
|
||||
- Most stable and fast. Requires a unique, monotonic sort key.
|
||||
- Use for: ordered collections with a natural unique key.
|
||||
|
||||
## Defaults (P3 Predictability)
|
||||
|
||||
- Default `limit`: 20 or 50. Never unbounded.
|
||||
- Max `limit`: 100 or 200. Reject `limit=10000` with 400.
|
||||
- Default sort: by created_at descending, or by the resource's natural order.
|
||||
- Always return the total count if cheap; never if it requires a separate COUNT query on a large table.
|
||||
|
||||
## Response Shape (P2 Clarity)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [...],
|
||||
"pagination": {
|
||||
"cursor": "next-base64-token",
|
||||
"has_more": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `cursor` is null when there is no next page.
|
||||
- `has_more` is the boolean convenience (some clients prefer it).
|
||||
- Never return `data` as a bare array — always wrap so you can add pagination without breaking.
|
||||
|
||||
## Link Header (alternative)
|
||||
|
||||
```
|
||||
Link: <https://api.example.com/users?cursor=X>; rel="next", <https://api.example.com/users?cursor=Z>; rel="prev"
|
||||
```
|
||||
|
||||
- Useful for HTTP-level clients (curl, browser fetch).
|
||||
- Less convenient for JSON-parsing clients.
|
||||
|
||||
## Consistency (P8 Consistency across endpoints)
|
||||
|
||||
- Every collection endpoint paginates the same way.
|
||||
- A client that learns pagination on `/users` should know it on `/orders`.
|
||||
- Mixed pagination (cursor here, offset there) is a tax on every consumer.
|
||||
|
||||
## What Violates Pagination
|
||||
|
||||
| Violation | Principle |
|
||||
|-----------|-----------|
|
||||
| Returning 10000 items by default | P7 Performance |
|
||||
| `limit` with no max | P8 Security (DoS) |
|
||||
| Page numbers on a frequently-inserted table | P3 Predictability |
|
||||
| Bare array response (no pagination wrapper) | P1 Contract Fidelity (can't add pagination later without breaking) |
|
||||
@@ -0,0 +1,68 @@
|
||||
# REST — Derived Rules
|
||||
|
||||
> Derives from `domains/api/first-principles.md`. Applies P1–P10 to REST specifically.
|
||||
|
||||
## Resource Naming (P2 Clarity, P3 Predictability)
|
||||
|
||||
- Nouns, not verbs: `/users`, `/orders`, not `/getUsers`.
|
||||
- Plural: `/users` (collection), `/users/{id}` (item).
|
||||
- Lowercase, hyphenated: `/order-items`, not `/OrderItems` or `/order_items`.
|
||||
- Nesting max 2 levels: `/users/{id}/orders`, not `/users/{id}/orders/{oid}/items/{iid}`.
|
||||
|
||||
## HTTP Methods (P1 Contract Fidelity, P6 Idempotency)
|
||||
|
||||
| Method | Semantics | Idempotent | Safe |
|
||||
|--------|-----------|------------|------|
|
||||
| GET | Read | Yes | Yes |
|
||||
| POST | Create | No | No |
|
||||
| PUT | Replace (full) | Yes | No |
|
||||
| PATCH | Update (partial) | No | No |
|
||||
| DELETE | Remove | Yes | No |
|
||||
|
||||
- PUT requires the full resource. PATCH requires only the delta. Never accept a partial PUT.
|
||||
- POST creates; never use POST for read operations. POST is not cacheable.
|
||||
|
||||
## Status Codes (P9 Error Transparency, P1 Contract Fidelity)
|
||||
|
||||
| Code | Meaning | When |
|
||||
|------|---------|------|
|
||||
| 200 | OK | Successful GET, PUT, PATCH |
|
||||
| 201 | Created | Successful POST that created a resource |
|
||||
| 204 | No Content | Successful DELETE, or empty response |
|
||||
| 400 | Bad Request | Malformed request (client error) |
|
||||
| 401 | Unauthorized | Authentication required or failed |
|
||||
| 403 | Forbidden | Authenticated but not permitted |
|
||||
| 404 | Not Found | Resource does not exist |
|
||||
| 409 | Conflict | State conflict (e.g., duplicate) |
|
||||
| 422 | Unprocessable | Well-formed but semantically invalid |
|
||||
| 429 | Too Many Requests | Rate limited |
|
||||
| 500 | Internal Error | Server bug — never leak stack trace |
|
||||
|
||||
- Never return 200 on an error. Never return 500 with a stack trace.
|
||||
- 401 vs 403: 401 = "who are you?", 403 = "I know who you are, but you can't."
|
||||
|
||||
## Idempotency (P6 Idempotency)
|
||||
|
||||
- POST: not idempotent. Use an idempotency key (`Idempotency-Key` header) for safe retry.
|
||||
- PUT: idempotent by definition — same PUT twice = same state.
|
||||
- DELETE: idempotent — deleting a non-existent resource is success (204).
|
||||
- PATCH: not idempotent by default; can be made idempotent with explicit versioning.
|
||||
|
||||
## Pagination (P7 Performance, see `pagination.md`)
|
||||
|
||||
- Default to cursor pagination for collections > 100 items.
|
||||
- Never return unbounded collections.
|
||||
- `Link` header or `cursor` field in response body.
|
||||
|
||||
## Versioning (P5 Versioning, P10 Stability, see `versioning.md`)
|
||||
|
||||
- Version in the URL (`/v1/users`) or in the header (`Accept: application/vnd.atelier.v1+json`).
|
||||
- Pick one. Be consistent across all endpoints.
|
||||
- Never make a breaking change without a new version and a deprecation cycle.
|
||||
|
||||
## Security (P8 Security, see `domains/security/`)
|
||||
|
||||
- HTTPS only. Redirect HTTP to HTTPS.
|
||||
- Authentication on every non-public endpoint. No opt-in auth.
|
||||
- Rate limiting on write endpoints (POST, PUT, PATCH, DELETE).
|
||||
- Validate every input against a schema. Never pass raw request body to the database.
|
||||
@@ -0,0 +1,54 @@
|
||||
# API Versioning — Derived Rules
|
||||
|
||||
> Derives from `domains/api/first-principles.md` P5 (Versioning) and P10 (Stability).
|
||||
|
||||
## The Default: No Breaking Changes
|
||||
|
||||
- A breaking change is a new version. There is no "minor" breaking change.
|
||||
- Breaking changes: removing a field, changing a field type, changing a field's semantics, changing required vs optional, changing error codes.
|
||||
- Non-breaking changes: adding a field, adding an endpoint, adding an optional parameter, loosening validation.
|
||||
|
||||
## Version Policies
|
||||
|
||||
### URL Versioning (`/v1/users`)
|
||||
- Simple, visible, cacheable.
|
||||
- Breaking changes bump the major version: `/v1` → `/v2`.
|
||||
- Old versions are supported in parallel during the deprecation window.
|
||||
|
||||
### Header Versioning (`Accept: application/vnd.atelier.v1+json`)
|
||||
- Invisible in the URL; harder to test.
|
||||
- Useful when the URL must stay stable (e.g., public webhooks).
|
||||
|
||||
### Semantic Versioning (for libraries/SDKs)
|
||||
- Major: breaking. Minor: additive. Patch: fix.
|
||||
- Follow semver strictly. A "minor" that breaks is a lie.
|
||||
|
||||
## Deprecation Cycle (P5 Reversibility)
|
||||
|
||||
1. **Announce**: mark the field/endpoint `@deprecated` with a sunset date.
|
||||
2. **Support**: keep the old version working until the sunset date.
|
||||
3. **Monitor**: track usage of the deprecated surface.
|
||||
4. **Retire**: when usage drops below threshold (or sunset passes), remove.
|
||||
5. **Never** remove without announcing. The cost of a silent break is paid by every consumer.
|
||||
|
||||
## Sunset Headers (P9 Error Transparency)
|
||||
|
||||
- Deprecated endpoints return `Sunset: <date>` header.
|
||||
- Deprecated endpoints return `Deprecation: <date>` header.
|
||||
- A consumer who reads headers knows when to migrate.
|
||||
|
||||
## Versioning vs Compatibility
|
||||
|
||||
- Versioning is the mechanism. Compatibility is the property.
|
||||
- Backward compatibility: old consumers work with the new version.
|
||||
- Forward compatibility: new consumers work with the old version (harder, rarer, usually not worth it).
|
||||
- Aim for backward compatibility. Forward compatibility is for protocols, not APIs.
|
||||
|
||||
## What Violates Versioning
|
||||
|
||||
| Violation | Principle |
|
||||
|-----------|-----------|
|
||||
| Removing a field without deprecation | P5, P10 |
|
||||
| Changing a field's type in a "minor" release | P1, P5 |
|
||||
| No sunset header on a deprecated endpoint | P9 |
|
||||
| Two versions with divergent semantics for the same field | P1 |
|
||||
Reference in New Issue
Block a user