---
title: "API conventions"
url: https://mdfy.app/LhYW7tc1
updated: 2026-05-16T14:17:17.691Z
source: "demo-seed"
---
# API conventions

## URL shape

`/api/v1/<resource>` for stable endpoints. Versioned via the path.
Sub-resources nest: `/api/v1/pages/<id>/blocks`.

## HTTP verbs

- `GET` — idempotent read
- `POST` — create
- `PATCH` — partial update (default for edits)
- `PUT` — full replace (rare, use sparingly)
- `DELETE` — soft delete (sets `deleted_at`); pass `?permanent=true` for hard delete

## Error shape

Every error response:

```json
{
  "error": "human-readable message",
  "code": "machine_code",
  "details": {}
}
```

Status codes used: 400 (bad input), 401 (no auth), 403 (auth but no permission), 404 (not found), 409 (conflict), 410 (gone — expired), 422 (validation), 429 (rate limit), 500 (us).

## Authentication

Two paths, in order of precedence:

1. `Authorization: Bearer <jwt>` — for signed-in users (preferred)
2. `x-edit-token: <token>` — for anonymous edits (per-doc capability)

Server middleware (`lib/verify-auth.ts`) returns `{userId, email}`
or null. Routes that allow either signed-in or token holders should
check both.

## Rate limiting

Sliding window, IP-based, 60 req/min default. Override per-route in
`middleware.ts`. Returns 429 with `Retry-After` header.

## Pagination

Cursor-based, not offset. `?cursor=<opaque>&limit=20`. Response
includes `nextCursor` (or null if last page).

## Things to avoid

- Don't put service-role key calls behind the public API. Use server-component reads or the cron path.
- Don't return more than 200 rows in one response. Paginate.
- Don't emit raw Postgres errors to clients — sanitize via `mapPgError`.
