---
name: house-api-conventions
source: https://app.decimal.ai/s/house-api-conventions@1/SKILL.md
source_sha256: 5c892c12b401
---

# House API Conventions

## Contract

Enforce this project's REST conventions on every endpoint you design, write, or review: the
`{data, error, meta}` response envelope, plural kebab-case versioned URLs, and the exact
status-code map below. These are arbitrary house standards, not language defaults — apply them
even when a more common convention exists.

## Rules

### 1. Response envelope (every response, no exceptions)

Every response body is a JSON object with **exactly three** top-level keys, in this order:

- `data` — the business payload on success; `null` on failure.
- `error` — `null` on success; on failure an object `{ "code", "message", "details" }` with all
  three keys present.
- `meta` — pagination and metadata. List endpoints MUST populate it; single-resource and error
  responses may set it to an empty object `{}` or omit values, but the key stays.

No extra top-level keys (no bare arrays, no `success: true`, no `status`, no top-level `items`).
Both `data` and `error` are always present together; one is always `null`.

### 2. `meta` for list endpoints

Any endpoint returning a collection MUST include `meta` with exactly these keys:

- `page` — the 1-based current page.
- `limit` — items per page; the **default is 20** when the client does not specify.
- `total` — the total number of matching records across all pages.

### 3. URL naming

- Resources are **plural nouns**: `/users`, `/orders`, `/products` (never `/user`, never `/getUsers`).
- Multi-word resources use **kebab-case**: `/order-items`, `/user-profiles` (never `orderItems`,
  `order_items`, or `orderitems`).
- belongsTo relations **nest**: `/users/{id}/orders`.
- **Maximum two levels of nesting.** Past two, switch the extra ancestor to a query parameter.
- **Filtering, sorting, and pagination are query parameters**, never path segments:
  `/orders?status=active&sort=-created_at&limit=20&page=1`.

### 4. Versioning

- **Every** path is prefixed with the API version: `/api/v1/...`.
- A breaking change ships under a **new path version** (`/api/v2/...`); the version lives in the
  URL path, not a header or query string. The old version keeps serving until retired.

### 5. HTTP status codes (this exact map)

| Code | Meaning |
|------|---------|
| `200` | success returning data |
| `201` | resource created |
| `400` | malformed request / bad or missing parameters / schema validation failure |
| `401` | not authenticated (missing or invalid token) |
| `403` | authenticated but not permitted |
| `404` | resource does not exist |
| `422` | **business-logic** failure (insufficient balance, duplicate signup, declined card) |
| `500` | internal server error |

`422` is the one most often guessed wrong: it covers any request that is well-formed and
authorized but violates a domain rule. It is **not** `400` (that is for malformed input) and
**not** `409`.

### 6. Authentication

- Every endpoint requires the header `Authorization: Bearer <jwt-token>` unless it is explicitly
  public.
- A public endpoint is marked with the **`@public`** annotation in code/spec.

## Worked examples

Each shows the base model's natural default (BEFORE) and the conforming form (AFTER).

### Envelope — list response (Rule 1, 2)
BEFORE
```json
[ { "id": 1 }, { "id": 2 } ]
```
AFTER
```json
{ "data": [ { "id": 1 }, { "id": 2 } ], "error": null,
  "meta": { "page": 1, "limit": 20, "total": 57 } }
```

### Envelope — single resource (Rule 1)
BEFORE
```json
{ "id": 7, "name": "Widget", "price": 9.99 }
```
AFTER
```json
{ "data": { "id": 7, "name": "Widget", "price": 9.99 }, "error": null, "meta": {} }
```

### Envelope — error (Rule 1)
BEFORE
```json
{ "message": "Invalid email" }
```
AFTER
```json
{ "data": null,
  "error": { "code": "INVALID_EMAIL", "message": "Email address is malformed", "details": {} },
  "meta": {} }
```

### Plural kebab-case resource (Rule 3)
BEFORE: `GET /api/v1/orderItems`
AFTER:  `GET /api/v1/order-items`

### Two-level nesting cap (Rule 3)
BEFORE: `GET /api/v1/posts/12/comments/88/reactions`  (three levels)
AFTER:  `GET /api/v1/comments/88/reactions`  (re-root at the nearest owner; or
`GET /api/v1/reactions?comment_id=88`)

### Filtering is a query parameter (Rule 3)
BEFORE: `GET /api/v1/orders/active`
AFTER:  `GET /api/v1/orders?status=active&limit=20&page=1`

### Versioned path (Rule 4)
BEFORE: `GET /users`
AFTER:  `GET /api/v1/users`

### Breaking change → new path version (Rule 4)
BEFORE: `GET /api/v1/users` with a changed shape, or `GET /api/users?version=2`
AFTER:  `GET /api/v2/users` (v1 stays live)

### Business-logic failure → 422 (Rule 5)
BEFORE: withdraw-over-balance returns `400 Bad Request`
AFTER:  returns `422 Unprocessable Entity` with the error envelope

### Created → 201 (Rule 5)
BEFORE: a successful `POST /api/v1/orders` returns `200 OK`
AFTER:  returns `201 Created`

### Auth header (Rule 6)
BEFORE: `X-Api-Key: abc123` or `?token=abc123`
AFTER:  `Authorization: Bearer <jwt-token>`

## Edge cases & exceptions

- **Single-resource and error responses still carry `meta`.** It may be `{}`, but the key is
  present so every response has the same three-key shape.
- **Empty list** is `data: []` with `meta.total: 0` — not `data: null`. `null` means failure.
- **Deeper-than-two relationships:** re-root the resource at its nearest owner and pass the further
  ancestor as a query parameter (`/reactions?comment_id=88`), or stop nesting at the two-level
  owner. Never emit a third path segment.
- **Validation failure vs. business-logic failure:** a missing/!malformed field is `400`; a
  well-formed request that breaks a domain rule (duplicate email, insufficient funds, declined
  card) is `422`.
- **Public endpoints** skip the Bearer requirement but everything else (envelope, versioning,
  naming) still applies; mark them `@public`.
- **Authenticated-but-forbidden is `403`, missing-token is `401`** — do not collapse both to `401`.

## Do / Don't

- DO start every path with `/api/v1/`. DON'T put the version in a header or query string.
- DO return exactly `{data, error, meta}`. DON'T return a bare array or add `success`/`status`.
- DO use `422` for domain-rule failures. DON'T use `400` or `409` for them.
- DO use `201` for creates. DON'T use `200` for a successful create.
- DO put filters/sorts in the query string. DON'T encode them as path segments.
- DO use plural kebab-case (`order-items`). DON'T use camelCase or snake_case in the path.
- DO cap nesting at two levels. DON'T emit a third path segment under two owners.
- DO send `Authorization: Bearer <token>`. DON'T use `X-Api-Key` or a `?token=` query.

## Common mistakes

- Returning a bare JSON array for a list instead of wrapping it in `{data, error, meta}`.
- Omitting `meta` (or its `page`/`limit`/`total` keys) on collection endpoints.
- Setting `data: null` for an empty list instead of `data: []`.
- Using `400` for a duplicate-signup / insufficient-balance failure that should be `422`.
- Returning `200` instead of `201` after creating a resource.
- camelCase or snake_case resource paths (`/orderItems`, `/order_items`) instead of kebab-case.
- Three-level nested URLs (`/posts/{id}/comments/{id}/reactions`).
- Filtering via a path segment (`/orders/active`) instead of `?status=active`.
- Dropping the `/api/v1` prefix.
- Collapsing `401` and `403` into one code.

## Quick checklist

1. Path starts with `/api/v1/`.
2. Resource is a plural, kebab-case noun.
3. Nesting ≤ 2 levels; filters/sorts/pagination in the query string.
4. Body is exactly `{data, error, meta}`.
5. Success: `data` set, `error: null`. Failure: `data: null`, `error: {code, message, details}`.
6. List endpoints: `meta` has `page`, `limit` (default 20), `total`.
7. Status: 200/201/400/401/403/404/422/500 mapped correctly; domain failures → 422; creates → 201.
8. `Authorization: Bearer <jwt-token>` required unless `@public`.
