REST API Reference
See also: GraphQL API Reference and REST vs GraphQL.
Endpoint Convention
Section titled “Endpoint Convention”REST endpoints follow the pattern {path}/{resource} where the path defaults to /rest/v1. The path is configurable via the [rest] path field.
Resources are derived at compile time from your schema’s CQRS naming conventions. Override auto-derived routes with explicit rest_path and rest_method annotations on any query or mutation.
| Annotation | Resolved endpoint |
|---|---|
rest_path="/posts" | GET /rest/v1/posts |
rest_path="/posts/{id}" | GET /rest/v1/posts/{id} |
rest_path="/users/{userId}/posts" | GET /rest/v1/users/{userId}/posts |
ID Parameter Detection
Section titled “ID Parameter Detection”The REST handler uses id: UUID as the URL path parameter for single-resource routes. The detection heuristic is:
- First required non-nullable argument of type
Int,ID, orUUIDwhose name matchesid,pk, orpk_* - If no match, fall back to
pk_*fields on the return type
When your path parameter uses a different name, match it in the function signature:
# Default: "id" argument auto-detected@fraiseql.query(rest_path="/posts/{id}", rest_method="GET")def post(id: UUID) -> Post: ...
# Custom: use "slug" — just match the path placeholder to the function arg@fraiseql.query(rest_path="/posts/{slug}", rest_method="GET")def post_by_slug(slug: str) -> Post: ...Request Formats
Section titled “Request Formats”GET requests
Section titled “GET requests”Path parameters are extracted from {param} placeholders in the path. Query string parameters map to the operation’s function arguments.
GET /rest/v1/posts?limit=10&offset=0Maps to posts(limit: 10, offset: 0).
GET /rest/v1/posts/a1b2c3d4-e5f6-7890-abcd-ef1234567890Maps to post(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890").
POST, PUT, PATCH requests
Section titled “POST, PUT, PATCH requests”Path parameters are extracted from the URL. Additional parameters come from the JSON request body. Content-Type: application/json is required.
curl -X POST https://api.example.com/rest/v1/posts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "Hello", "body": "World"}'PATCH requests also accept Content-Type: application/merge-patch+json (RFC 7396) — treated identically to application/json.
PUT vs PATCH: mutations with full coverage of all writable fields generate both PUT and PATCH routes on the canonical resource path. Mutations with partial coverage generate PATCH only, mounted as an action sub-resource. PUT requests must include all writable fields — missing fields return 422.
DELETE requests
Section titled “DELETE requests”Path parameters only. No request body.
curl -X DELETE https://api.example.com/rest/v1/posts/a1b2c3d4-... \ -H "Authorization: Bearer $TOKEN"Response Format
Section titled “Response Format”All REST responses use a JSON envelope with data, meta, and links fields.
Single resource (200 OK)
Section titled “Single resource (200 OK)”{ "data": { "id": "a1b2c3d4-...", "title": "Hello", "body": "World" }}Collection — offset pagination (200 OK)
Section titled “Collection — offset pagination (200 OK)”{ "data": [ { "id": "a1b2c3d4-...", "title": "Hello" }, { "id": "e5f6g7h8-...", "title": "Second post" } ], "meta": { "limit": 10, "offset": 0 }, "links": { "self": "/rest/v1/posts?limit=10&offset=0", "next": "/rest/v1/posts?limit=10&offset=10", "prev": null, "first": "/rest/v1/posts?limit=10&offset=0" }}meta.total is opt-in via Prefer: count=exact. When not requested, meta.total and links.last are omitted and no count query is issued.
Collection — relay/cursor pagination (200 OK)
Section titled “Collection — relay/cursor pagination (200 OK)”{ "data": [ ... ], "meta": { "hasNextPage": true, "hasPreviousPage": false }, "links": { "self": "/rest/v1/users?first=10", "next": "/rest/v1/users?first=10&after=eyJpZCI6NDJ9" }}Created resource (201 Created)
Section titled “Created resource (201 Created)”{ "data": { "id": "c3d4e5f6-...", "name": "Charlie" }}Includes Location: /rest/v1/users/c3d4e5f6-... header.
Deleted resource
Section titled “Deleted resource”Configurable via delete_response in [rest]. Per-request override via Prefer header:
| Config / Prefer Header | Status | Body |
|---|---|---|
delete_response = "no_content" (default) | 204 No Content | empty |
delete_response = "entity" | 200 OK | deleted entity in data envelope |
Prefer: return=minimal | 204 No Content | empty |
Prefer: return=representation | 200 OK | deleted entity in data envelope |
The Prefer header overrides the server default. Response includes Preference-Applied when the header was honored. DELETE functions that do not populate the entity field in mutation_response gracefully degrade: return=representation falls back to 204 with X-Preference-Fallback: entity-unavailable.
Error Response Format
Section titled “Error Response Format”Errors use a structured error envelope with code, message, and details fields:
{ "error": { "code": "NOT_FOUND", "message": "User 999 not found", "details": {} }}Validation errors
Section titled “Validation errors”{ "error": { "code": "VALIDATION_ERROR", "message": "Request body validation failed", "details": { "fields": [ { "field": "email", "reason": "required for PUT (missing writable field)" }, { "field": "age", "reason": "expected integer, got string" } ] } }}Unknown parameter errors
Section titled “Unknown parameter errors”{ "error": { "code": "UNKNOWN_PARAMETER", "message": "Unknown query parameter 'nme'", "details": { "parameter": "nme", "available": ["name", "email", "status", "limit", "offset", "sort", "select", "filter"] } }}Unknown operator errors
Section titled “Unknown operator errors”{ "error": { "code": "UNKNOWN_OPERATOR", "message": "Unknown operator 'beginsWith' on field 'name'", "details": { "field": "name", "operator": "beginsWith", "available": ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "contains", "icontains", "startswith", "endswith", "like", "ilike", "is_null"] } }}Error details are structured for AI-agent consumption — agents can use the available field to self-correct without documentation.
HTTP Status Codes
Section titled “HTTP Status Codes”| Code | Meaning |
|---|---|
| 200 | Success (GET, PUT, PATCH, custom action, DELETE with return=representation) |
| 201 | Created (POST Insert mutations) |
| 204 | No Content (DELETE with return=minimal or default no_content) |
| 304 | Not Modified (ETag match via If-None-Match) |
| 400 | Bad Request — malformed request, unknown parameter, unknown operator |
| 401 | Unauthorized — missing or invalid token |
| 403 | Forbidden — authenticated but not authorized |
| 404 | Not found — resource does not exist |
| 405 | Method Not Allowed — includes Allow header with valid methods |
| 409 | Conflict — unique constraint violation |
| 422 | Unprocessable Entity — type mismatch, missing required fields for PUT |
| 429 | Too Many Requests — see Retry-After header |
| 500 | Internal Server Error |
Query Parameters
Section titled “Query Parameters”Filtering
Section titled “Filtering”Three filtering syntaxes are supported:
| Syntax | Example | Description |
|---|---|---|
| Simple equality | ?name=Alice&status=active | Converted to {"name": {"eq": "Alice"}} |
| Bracket operators | ?name[icontains]=Ali&age[gte]=18 | Operator specified in brackets |
| JSON filter DSL | ?filter={"name":{"startsWith":"A"}} | Full FraiseQL where DSL |
Bracket operators:
| Operator | SQL | Example |
|---|---|---|
eq | = | ?name[eq]=Alice |
neq | != | ?status[neq]=archived |
gt | > | ?age[gt]=18 |
gte | >= | ?age[gte]=18 |
lt | < | ?price[lt]=100 |
lte | <= | ?price[lte]=100 |
in | IN | ?status[in]=active,pending (comma-separated) |
nin | NOT IN | ?status[nin]=archived,deleted |
contains | @> (JSONB) | ?tags[contains]=rust |
icontains | ILIKE '%...%' | ?name[icontains]=ali |
startswith | LIKE '...%' | ?name[startswith]=A |
endswith | LIKE '%...' | ?email[endswith]=@example.com |
like | LIKE | ?name[like]=A%25ce |
ilike | ILIKE | ?name[ilike]=a%25ce |
is_null | IS NULL / IS NOT NULL | ?deleted_at[is_null]=true |
The ?filter= JSON escape hatch accepts the full 50+ operator set from the FraiseQL operator registry, including JSONB containment, vector distance, full-text search, ltree, network/IP, and range operators.
Rich filter operators from semantic scalar types are also available via bracket notation. For example, ?email[domainEq]=example.com.
Logical Operators
Section titled “Logical Operators”Combine filters with or=(), and=(), and not=():
GET /rest/v1/users?or=(status[eq]=active,status[eq]=pending)GET /rest/v1/users?not=(role[eq]=admin)GET /rest/v1/users?or=(and=(age[gte]=18,active[eq]=true),name[eq]=admin)Logical groups support nesting and combine with regular filter parameters.
Sorting
Section titled “Sorting”GET /rest/v1/users?sort=name # ascendingGET /rest/v1/users?sort=-name # descending (- prefix)GET /rest/v1/users?sort=name,-age # multi-columnSort fields are validated against the resource’s type definition.
Pagination
Section titled “Pagination”Offset-based (standard queries):
GET /rest/v1/users?limit=10&offset=20Cursor-based (relay queries):
GET /rest/v1/users?first=10&after=eyJpZCI6NDJ9Relay vs offset is determined at compile time from the query definition. Cross-pagination guards apply: a relay query receiving ?limit=/?offset= returns 400, and vice versa.
limit values are clamped to max_page_size (default: 100). When no limit is specified, default_page_size (default: 20) is used.
Field Selection and Embedding
Section titled “Field Selection and Embedding”GET /rest/v1/users?select=id,name,emailOmitting select returns all fields. Field names are validated against the type definition.
Parenthesized syntax embeds related resources:
GET /rest/v1/users?select=id,name,posts(id,title,comments(id,body))Embedding supports rename (author:fk_user(id,name)) and count (posts.count). Maximum nesting depth is configurable via max_embedding_depth (default: 3).
| Parameter | Description |
|---|---|
sort | Comma-separated fields. Prefix with - for descending. |
select | Comma-separated field names. Parentheses for embedding: posts(id,title) |
limit | Maximum rows to return (offset pagination) |
offset | Rows to skip (offset pagination) |
first | Maximum rows to return (cursor pagination) |
after | Cursor for forward pagination |
filter | JSON filter object (full FraiseQL where DSL) |
or, and, not | Logical operator groups: or=(field[op]=val,field[op]=val) |
search | Full-text search query (PostgreSQL websearch_to_tsquery). Only on types with searchable fields. |
Full-Text Search
Section titled “Full-Text Search”GET /rest/v1/posts?search=graphql+tutorialUses PostgreSQL websearch_to_tsquery() — supports natural-language operators (OR, -exclude, "exact phrase"). Multiple searchable fields are ORed. Results auto-sort by relevance unless ?sort= is specified.
Returns 400 if the type has no searchable fields.
SSE Streaming
Section titled “SSE Streaming”GET /rest/v1/{resource}/streamAccept: text/event-streamSubscribes to real-time entity changes. Events follow SSE wire format:
| SSE field | Value |
|---|---|
event | insert, update, delete, custom, ping |
id | Event UUID (use with Last-Event-ID for reconnection) |
data | JSON entity payload |
Requires the observers feature. Returns 501 without it.
Request Headers
Section titled “Request Headers”| Header | Description |
|---|---|
Authorization | Bearer <jwt> or via configured API key header |
Content-Type | application/json or application/merge-patch+json (PATCH) |
Accept | application/json (default), application/x-ndjson for NDJSON streaming, or text/event-stream for SSE |
Prefer | Request preferences (see below) |
If-None-Match | ETag value for conditional requests (returns 304 if unchanged) |
Idempotency-Key | UUID for idempotent POST mutations (replays stored response on retry). See below. |
Prefer Header
Section titled “Prefer Header”Multiple preferences can be combined: Prefer: return=representation, count=exact
| Preference | Values | Description |
|---|---|---|
return | representation, minimal | Controls response body for mutations. representation returns the entity; minimal returns empty body. |
count | exact | Include meta.total in collection responses. Issues a parallel COUNT(*) query. |
resolution | merge-duplicates, ignore-duplicates | Upsert conflict resolution for bulk inserts. |
max-affected | integer | Safety guard for bulk PATCH/DELETE — returns 400 if more rows would be affected. |
tx | rollback | Dry-run: execute in a transaction then roll back. Returns what would have happened. |
Response always includes Preference-Applied listing honored preferences. Unknown preferences are silently ignored per RFC 7240.
Response Headers
Section titled “Response Headers”| Header | When | Value |
|---|---|---|
ETag | GET 200 responses (when etag = true) | W/"<xxhash64-hex>" (weak validator) |
Location | 201 Created | Full resource URL |
Preference-Applied | When Prefer header was honored | Applied preferences |
X-Preference-Fallback | When Prefer was partially honored | Explains degradation (e.g., entity-unavailable) |
X-Rows-Affected | Bulk operations | Number of rows inserted/updated/deleted |
Cache-Control | GET responses | public, max-age={ttl} or private, max-age={ttl} (when authenticated) |
Vary | GET responses | Authorization, Accept, Prefer |
Allow | 405 Method Not Allowed | Comma-separated allowed HTTP methods |
X-Request-Id | All responses | Echoed from request or generated UUID |
Content-Type | All non-204 responses | application/json or application/x-ndjson |
Content-Type
Section titled “Content-Type”- Accepted:
application/json,application/merge-patch+json(PATCH),application/x-ndjson(Accept header for streaming) - Returned:
application/jsonorapplication/x-ndjson - Required for: POST, PUT, PATCH requests (rejected with 400 if absent)
Authentication
Section titled “Authentication”Authentication is shared with the GraphQL and gRPC transports. Pass credentials in the request header.
Bearer token (JWT/OIDC):
Authorization: Bearer <jwt-token>API key:
X-API-Key: <api-key>See Authentication for how to configure JWT validation and API keys.
Rate Limiting
Section titled “Rate Limiting”REST requests share rate limiting with the GraphQL transport. Per-transport metrics are tagged with transport=rest.
When a request is rate limited, the response includes:
| Header | Value |
|---|---|
X-RateLimit-Limit | Configured requests per window |
X-RateLimit-Remaining | Remaining requests in the current window |
Retry-After | Seconds until the window resets |
Rate limiting is configured via [security.rate_limiting] in fraiseql.toml. See Rate Limiting.
OpenAPI Specification
Section titled “OpenAPI Specification”The compiler generates an OpenAPI 3.0.3 specification from your schema at compile time. No manual YAML required.
Compile-time generation (canonical):
fraiseql-cli openapi schema.compiled.json -o openapi.jsonRuntime serving (convenience):
GET /rest/v1/openapi.json (configurable via openapi_path) when openapi_enabled = true in [rest].
curl https://api.example.com/rest/v1/openapi.jsonThe spec is cached in memory and invalidated on schema reload or config change. It reflects exactly what is derived from your schema — no undocumented endpoints.
Configuration Reference
Section titled “Configuration Reference”Key configuration fields in [rest]:
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable/disable REST transport |
path | string | "/rest/v1" | Base URL path for all REST routes |
require_auth | bool | false | Require authentication for all REST endpoints |
include | array | [] | Whitelist operations (empty = all) |
exclude | array | [] | Blacklist operations |
delete_response | string | "no_content" | Default DELETE response style |
max_page_size | integer | 100 | Maximum allowed limit value |
default_page_size | integer | 20 | Default limit when not specified |
etag | bool | true | Enable ETag / If-None-Match |
max_filter_bytes | integer | 4096 | Maximum ?filter= JSON size |
max_embedding_depth | integer | 3 | Maximum nesting depth for ?select= embedding |
openapi_enabled | bool | false | Serve OpenAPI spec |
openapi_path | string | "/rest/v1/openapi.json" | OpenAPI spec endpoint path |
title | string | "FraiseQL REST API" | OpenAPI info.title |
api_version | string | "1.0.0" | OpenAPI info.version |
For full configuration options, see TOML Configuration Reference.