---
name: response-format
description: RESTful plural nouns, consistent error format, pagination, OpenAPI documentation
applies-to: claudetools-api, gururmm
---

# API Standards

## URL structure

- RESTful with plural nouns: `/api/users`, `/api/agents`, `/api/clients`
- Nested resources for sub-items: `/api/agents/:id/checks`, `/api/sites/:id/channel`
- Use kebab-case for multi-word segments: `/api/policy-assignments`, `/api/check-results`

## Error format

All error responses use a consistent envelope:

```json
{
  "detail": "Human-readable error description",
  "error_code": "MACHINE_READABLE_CODE",
  "status_code": 404
}
```

- `detail` — for humans; may be shown in UI
- `error_code` — for client code to switch on; use `UPPER_SNAKE` format
- `status_code` — redundant with HTTP status but helps clients that lose the HTTP layer

## Pagination

Paginate all list endpoints that can return more than ~50 items. Use cursor-based or offset-based pagination:

```json
{
  "items": [...],
  "total": 148,
  "page": 1,
  "per_page": 25
}
```

Do not return unbounded arrays from production endpoints.

## Documentation

- Document with OpenAPI — FastAPI generates this automatically from type annotations and docstrings
- For Axum/Rust endpoints, add route comments with request/response shapes until an OpenAPI generator is wired

## sqlx query style (GuruRMM server)

Use `sqlx::query()` (runtime) not `sqlx::query!()` (compile-time macro) for new queries. The compile-time macro requires `cargo sqlx prepare` after every schema change and rebuilding the `.sqlx/` cache. Runtime queries avoid this overhead.

The offline cache (`server/.sqlx/`) only needs updating when `query!()` macros change. When adding a new query, default to `sqlx::query()` unless there is a specific reason to use the proc macro.

## Migration discipline (GuruRMM)

- Never manually pre-apply migrations via psql without also recording the checksum in `_sqlx_migrations` — sqlx will fail on startup if it finds a missing row for a migration it doesn't recognize
- Use `ADD COLUMN IF NOT EXISTS` in all migrations so they are idempotent
- Apply migrations by letting the server binary run them on startup (`sqlx::migrate!()`)
- The correct sequence for a new migration: add SQL file → apply to DB (server startup) → `cargo sqlx prepare` → commit `.sqlx/` → rebuild
