Ctrl K
ComunidadGuías…

docs/api-conventions.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# API Conventions

The HTTP contract lives in the code: route inventory in
`backend/internal/handler/handler.go` (`SetupRouter`), response shapes in
`backend/internal/model/model.go` (JSON tags), request bodies in the
handler files (`createFeedRequest`, `createBookmarkRequest`, ...), and the
frontend call surface in `frontend/src/lib/api/index.ts`. This page
records only the cross-cutting rules that are distributed across handlers,
so they do not need to be rediscovered per endpoint. If code and this page
disagree, the code wins — fix the page.

## Base path and authentication

- All endpoints are under `/api`. Two exceptions: the Fever compatibility
  API (`/fever*`, see `docs/fever-api.md`) and a bare `GET /oidc/callback`
  registered only when OIDC is configured, for deployments whose redirect
  URI omits `/api`.
- Authentication is a session cookie named `session` (`POST /api/sessions`
  with `{"password": ...}` sets it; `DELETE /api/sessions` clears it, 204).
- Anonymous mode: when both password and OIDC are unconfigured, the auth
  middleware lets every request through.
- Login attempts are rate limited: excess attempts get `429` with a
  `Retry-After` header (seconds).
- OIDC: `GET /api/oidc/enabled` always exists; `/api/oidc/login` and
  `/api/oidc/callback` are registered only when OIDC is configured. Login
  returns `{data: {auth_url}}` for the client to navigate to; the callback
  redirects (307) to `/` on success or `/login?error=oidc_failed` on failure.

## Response envelopes

- Single resource: `{"data": <object>}`
- Group and feed lists (small, fully returned): `{"data": [...], "total": n}`
- Item and bookmark lists (cursor-paginated):
  `{"data": [...], "total": n, "next_cursor": <string|null>}`;
  `next_cursor` is `null` when there are no more pages.

## Cursor pagination

- Pass the previous page's `next_cursor` as the `before` query parameter;
  omit it on the first page.
- Cursor format: `"<value>_<id>"` (two int64s, underscore-separated). The
  value is `pub_date` (unix) for items and `created_at` (unix) for bookmarks;
  `id` breaks ties.
- The items cursor is keyed on `pub_date`, so `before` is only valid with
  the default ordering: combining `order_by=created_at` with `before` is
  rejected with 400. Only the first page can use `created_at`.
- Ordering is always DESC. Items support `order_by=pub_date|created_at`
  (default `pub_date`); any other value silently falls back to `pub_date`.
- `limit` is capped at 100 on every endpoint that accepts one (items,
  bookmarks, search); `maxListLimit` in `backend/internal/handler/item.go`.

## Errors

Handlers report failures as `{"error": "<message>"}`:

- `400` — invalid input (message varies)
- `401` — `"unauthorized"`
- `404` — `"<resource> not found"`
- `429` — `"too many login attempts"` (plus `Retry-After`)
- `500` — `"internal server error"` (details are logged server-side only)

Three cases return the status with an empty body instead: unknown routes
(404), CORS origin rejection (403), and panic recovery (500).

## Status codes

- `200` — any response with a body: reads, creates, and updates
- `204` — body-less success: mark items read/unread, all deletes, logout,
  and the `GET /api/sessions` validity probe (valid → 204, invalid → 401)
- `202` — feed refresh requests (`/feeds/refresh`, `/feeds/{id}/refresh`):
  refresh happens asynchronously, no body

## Request quirks worth knowing

- `POST /api/bookmarks` has two modes: `{item_id}` snapshots the bookmark from
  that item server-side (preferred), or a full `{link, title, content,
  feed_name, pub_date?}` snapshot when there is no item. Sending `item_id`
  makes the server ignore all other fields.
- `PATCH /api/items/-/read` and `-/unread` accept 1–1000 ids
  (`maxBatchUpdateIDs`).
- `refresh_interval_seconds` (create and update feed): on **create**, absent
  or `0` means "use the global interval" (stored as `NULL`, never as `0`);
  on **update**, absent or `null` leaves the stored value unchanged and `0`
  clears it back to the global interval; any other value must be in the
  closed set returned by `GET /api/app` (`allowed_refresh_intervals`).
- `PATCH /api/settings` is a partial update: an absent field leaves the stored
  value unchanged, and there is deliberately no way to clear a preference
  back to null. Closed sets: locale `en|zh|de|fr|es|ru|pt|sv`, theme
  `light|dark|system`, `auto_mark_read` `off|open|5|10|30`,
  `article_page_size` 1–100.