Prefer headers that change how responses come back.
For the framing, see PostgREST reference. For applying these patterns to the AI schema, see ai-schema recipes.
Embedded resources (joins in one request)
PostgREST infers joins from foreign keys. Useselect= with a relation name to embed:
author:users(...) syntax aliases the embedded users row as author. The columns inside parens are projected from the joined table.
Many-to-many: if users has many posts via an author_id FK, embed in the other direction:
posts array of joined rows.
Filtering on the embed:
posts array. The filter on the embed uses the dotted-path syntax.
Inner joins (exclude users with no posts):
!inner modifier makes the embed an INNER JOIN instead of LEFT JOIN.
Full-text search
PostgREST supports four FTS operators that map to Postgres’s text-search functions:| Operator | Maps to | Use | |
|---|---|---|---|
fts | @@ to_tsquery | Boolean queries with operators (&, ` | , !`) |
plfts | @@ plainto_tsquery | Treat input as plain text; no operators | |
phfts | @@ phraseto_tsquery | Phrase search; word order matters | |
wfts | @@ websearch_to_tsquery | Google-style (“foo bar” OR baz) |
tsvector column or expression index for performance. The bare body=plfts.foo form scans every row’s text — fine for prototypes, terrible at scale.
JSONB operators
PostgreSQL’s JSONB operators work in filters via PostgREST’s syntax:cs (contains) and cd (contained by) operators work on both JSONB and array columns.
For complex queries (multiple JSONB conditions, indexing strategies), check the Postgres JSON docs for the operator semantics and create matching GIN indexes (CREATE INDEX ... USING gin (meta jsonb_path_ops)).
Counting
By default, PostgREST returns the matching rows without a total count — counting can be expensive. Opt in via thePrefer header:
count=exact— runs a separateCOUNT(*)query. Accurate, slowest. The total comes back in theContent-Range: 0-9/247response header.count=planned— uses Postgres’s planner estimate. Fast, imprecise.count=estimated— runsCOUNT(*)only if planned > some threshold; planner estimate otherwise.
count=planned unless the user explicitly asks for an exact total.
Upserts
POST withPrefer: resolution=merge-duplicates does INSERT-or-UPDATE:
email is a unique column and a row already exists with that email, the existing row updates. Otherwise inserts.
For composite conflict columns, use on_conflict=col1,col2:
Bulk inserts
POST an array instead of a single object:Prefer: tx=rollback to test without committing, or Prefer: missing=default to allow rows with omitted columns to use their defaults instead of failing.
Response shape control
Prefer: return=... changes what comes back from a write:
| Value | Response body | When |
|---|---|---|
return=minimal (default for POST/PATCH) | empty | You don’t need the inserted/updated rows back |
return=representation | the rows | You need the rows (e.g., for the generated id) |
return=headers-only | empty + Location header | RPC-style “tell me where the new row is” |
Prefer: return=representation is what you want. It returns the inserted rows complete with database-generated columns.
Single-row responses
PostgREST returns arrays by default. To get a single object instead of[{...}], send Accept: application/vnd.pgrst.object+json:
{...} directly. Fails with 406 if the filter doesn’t match exactly one row. Use this when you want “fail loudly if I expected one row and got zero or many.”
Partial response columns
TheRange header lets you do offset-based pagination at the response level:
Prefer: count=exact and the Content-Range response header tells the client how many total items exist.
For cursor-style pagination (which is friendlier to large tables), use id comparisons instead — ?id=gt.<last_seen>&limit=20.
RPC for non-CRUD logic
For anything that needs SQL beyond filter/sort/paginate, write a function and call it via RPC:Next steps
PostgREST reference
The base CRUD surface these patterns extend.
ai-schema recipes
The same techniques applied to ai.* tables.
RLS Cookbook
Policies that gate which rows these queries see.
Direct Postgres
For SQL PostgREST doesn’t express, drop down to a direct connection.