Skip to content

REST API Reference

See also: GraphQL API Reference and REST vs GraphQL.

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.

AnnotationResolved 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

The REST handler uses id: UUID as the URL path parameter for single-resource routes. The detection heuristic is:

  1. First required non-nullable argument of type Int, ID, or UUID whose name matches id, pk, or pk_*
  2. 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: ...

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=0

Maps to posts(limit: 10, offset: 0).

GET /rest/v1/posts/a1b2c3d4-e5f6-7890-abcd-ef1234567890

Maps to post(id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890").

Path parameters are extracted from the URL. Additional parameters come from the JSON request body. Content-Type: application/json is required.

Terminal window
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.

Path parameters only. No request body.

Terminal window
curl -X DELETE https://api.example.com/rest/v1/posts/a1b2c3d4-... \
-H "Authorization: Bearer $TOKEN"

All REST responses use a JSON envelope with data, meta, and links fields.

{
"data": { "id": "a1b2c3d4-...", "title": "Hello", "body": "World" }
}
{
"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"
}
}
{
"data": { "id": "c3d4e5f6-...", "name": "Charlie" }
}

Includes Location: /rest/v1/users/c3d4e5f6-... header.

Configurable via delete_response in [rest]. Per-request override via Prefer header:

Config / Prefer HeaderStatusBody
delete_response = "no_content" (default)204 No Contentempty
delete_response = "entity"200 OKdeleted entity in data envelope
Prefer: return=minimal204 No Contentempty
Prefer: return=representation200 OKdeleted 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.

Errors use a structured error envelope with code, message, and details fields:

{
"error": {
"code": "NOT_FOUND",
"message": "User 999 not found",
"details": {}
}
}
{
"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" }
]
}
}
}
{
"error": {
"code": "UNKNOWN_PARAMETER",
"message": "Unknown query parameter 'nme'",
"details": {
"parameter": "nme",
"available": ["name", "email", "status", "limit", "offset", "sort", "select", "filter"]
}
}
}
{
"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.

CodeMeaning
200Success (GET, PUT, PATCH, custom action, DELETE with return=representation)
201Created (POST Insert mutations)
204No Content (DELETE with return=minimal or default no_content)
304Not Modified (ETag match via If-None-Match)
400Bad Request — malformed request, unknown parameter, unknown operator
401Unauthorized — missing or invalid token
403Forbidden — authenticated but not authorized
404Not found — resource does not exist
405Method Not Allowed — includes Allow header with valid methods
409Conflict — unique constraint violation
422Unprocessable Entity — type mismatch, missing required fields for PUT
429Too Many Requests — see Retry-After header
500Internal Server Error

Three filtering syntaxes are supported:

SyntaxExampleDescription
Simple equality?name=Alice&status=activeConverted to {"name": {"eq": "Alice"}}
Bracket operators?name[icontains]=Ali&age[gte]=18Operator specified in brackets
JSON filter DSL?filter={"name":{"startsWith":"A"}}Full FraiseQL where DSL

Bracket operators:

OperatorSQLExample
eq=?name[eq]=Alice
neq!=?status[neq]=archived
gt>?age[gt]=18
gte>=?age[gte]=18
lt<?price[lt]=100
lte<=?price[lte]=100
inIN?status[in]=active,pending (comma-separated)
ninNOT IN?status[nin]=archived,deleted
contains@> (JSONB)?tags[contains]=rust
icontainsILIKE '%...%'?name[icontains]=ali
startswithLIKE '...%'?name[startswith]=A
endswithLIKE '%...'?email[endswith]=@example.com
likeLIKE?name[like]=A%25ce
ilikeILIKE?name[ilike]=a%25ce
is_nullIS 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.

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.

GET /rest/v1/users?sort=name # ascending
GET /rest/v1/users?sort=-name # descending (- prefix)
GET /rest/v1/users?sort=name,-age # multi-column

Sort fields are validated against the resource’s type definition.

Offset-based (standard queries):

GET /rest/v1/users?limit=10&offset=20

Cursor-based (relay queries):

GET /rest/v1/users?first=10&after=eyJpZCI6NDJ9

Relay 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.

GET /rest/v1/users?select=id,name,email

Omitting 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).

ParameterDescription
sortComma-separated fields. Prefix with - for descending.
selectComma-separated field names. Parentheses for embedding: posts(id,title)
limitMaximum rows to return (offset pagination)
offsetRows to skip (offset pagination)
firstMaximum rows to return (cursor pagination)
afterCursor for forward pagination
filterJSON filter object (full FraiseQL where DSL)
or, and, notLogical operator groups: or=(field[op]=val,field[op]=val)
searchFull-text search query (PostgreSQL websearch_to_tsquery). Only on types with searchable fields.
GET /rest/v1/posts?search=graphql+tutorial

Uses 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.

GET /rest/v1/{resource}/stream
Accept: text/event-stream

Subscribes to real-time entity changes. Events follow SSE wire format:

SSE fieldValue
eventinsert, update, delete, custom, ping
idEvent UUID (use with Last-Event-ID for reconnection)
dataJSON entity payload

Requires the observers feature. Returns 501 without it.

HeaderDescription
AuthorizationBearer <jwt> or via configured API key header
Content-Typeapplication/json or application/merge-patch+json (PATCH)
Acceptapplication/json (default), application/x-ndjson for NDJSON streaming, or text/event-stream for SSE
PreferRequest preferences (see below)
If-None-MatchETag value for conditional requests (returns 304 if unchanged)
Idempotency-KeyUUID for idempotent POST mutations (replays stored response on retry). See below.

Multiple preferences can be combined: Prefer: return=representation, count=exact

PreferenceValuesDescription
returnrepresentation, minimalControls response body for mutations. representation returns the entity; minimal returns empty body.
countexactInclude meta.total in collection responses. Issues a parallel COUNT(*) query.
resolutionmerge-duplicates, ignore-duplicatesUpsert conflict resolution for bulk inserts.
max-affectedintegerSafety guard for bulk PATCH/DELETE — returns 400 if more rows would be affected.
txrollbackDry-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.

HeaderWhenValue
ETagGET 200 responses (when etag = true)W/"<xxhash64-hex>" (weak validator)
Location201 CreatedFull resource URL
Preference-AppliedWhen Prefer header was honoredApplied preferences
X-Preference-FallbackWhen Prefer was partially honoredExplains degradation (e.g., entity-unavailable)
X-Rows-AffectedBulk operationsNumber of rows inserted/updated/deleted
Cache-ControlGET responsespublic, max-age={ttl} or private, max-age={ttl} (when authenticated)
VaryGET responsesAuthorization, Accept, Prefer
Allow405 Method Not AllowedComma-separated allowed HTTP methods
X-Request-IdAll responsesEchoed from request or generated UUID
Content-TypeAll non-204 responsesapplication/json or application/x-ndjson
  • Accepted: application/json, application/merge-patch+json (PATCH), application/x-ndjson (Accept header for streaming)
  • Returned: application/json or application/x-ndjson
  • Required for: POST, PUT, PATCH requests (rejected with 400 if absent)

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.

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:

HeaderValue
X-RateLimit-LimitConfigured requests per window
X-RateLimit-RemainingRemaining requests in the current window
Retry-AfterSeconds until the window resets

Rate limiting is configured via [security.rate_limiting] in fraiseql.toml. See Rate Limiting.

The compiler generates an OpenAPI 3.0.3 specification from your schema at compile time. No manual YAML required.

Compile-time generation (canonical):

Terminal window
fraiseql-cli openapi schema.compiled.json -o openapi.json

Runtime serving (convenience):

GET /rest/v1/openapi.json (configurable via openapi_path) when openapi_enabled = true in [rest].

Terminal window
curl https://api.example.com/rest/v1/openapi.json

The 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.

Key configuration fields in [rest]:

FieldTypeDefaultDescription
enabledbooltrueEnable/disable REST transport
pathstring"/rest/v1"Base URL path for all REST routes
require_authboolfalseRequire authentication for all REST endpoints
includearray[]Whitelist operations (empty = all)
excludearray[]Blacklist operations
delete_responsestring"no_content"Default DELETE response style
max_page_sizeinteger100Maximum allowed limit value
default_page_sizeinteger20Default limit when not specified
etagbooltrueEnable ETag / If-None-Match
max_filter_bytesinteger4096Maximum ?filter= JSON size
max_embedding_depthinteger3Maximum nesting depth for ?select= embedding
openapi_enabledboolfalseServe OpenAPI spec
openapi_pathstring"/rest/v1/openapi.json"OpenAPI spec endpoint path
titlestring"FraiseQL REST API"OpenAPI info.title
api_versionstring"1.0.0"OpenAPI info.version

For full configuration options, see TOML Configuration Reference.