Details
There is no dedicated single-record route — a resource is read the same way it is listed, through Search, filtered down to one id. A record that does not exist, is soft-deleted, belongs to another tenant, or that the view preset says you may not see — someone else’s in a view: own resource — comes back the same way: an empty data array. There is no 404 or 403 to tell those cases apart, on purpose: the scope clause that excludes rows you cannot see and the where clause that excludes rows that do not exist are the same kind of filter, applied before the query ever runs, not a lookup that then checks a permission.
Every generated resource also exposes its own shape at GET /blog-posts/describe: the fields it has, everything a search request may name — sorts, filters, selects, includes, aggregates, whether it is paginated and, when it is, the accepted limits — and the validation rules create and update enforce, as JSON Schema reflected straight off the same Zod schemas the server validates against, not a hand-maintained copy of them.
The search half of that is not a copy either: the controller declares one BLOG_POST_CONTRACT object, parseSearchRequest validates against it and describe publishes it. So the endpoint cannot advertise a filter it rejects, or accept one it never mentioned — which it did, for id.
POST /blog-posts/search
Authorization: Bearer <token>
Content-Type: application/json
{
"filters": [
{
"field": "id",
"value": "cxmpl00000000000000000029"
}
]
}Using the contract
Section titled “Using the contract”Use describe’s output to build a form that only shows the fields the resource actually has, marks the ones create.required lists as mandatory, and validates lengths client-side before ever hitting the server — the server still re-validates on write, this is for UX, not for security.
includes and aggregates are reported per relation, each with the filters, sorts and selects that relation accepts — the same nested contract a search request is validated against. There is no relations key for the mutation side: attaching and detaching is described by Update, not here.
Same guard as every other route on the resource — a caller without permission to view the resource gets the usual 403, not the contract.
Reading one record is the search handler — see Search for its real code. describe is its own handler, from examples/blog-post/blog-post.controller.ts:
@Get('describe')
@Capability(canViewAnyBlogPost)
@CacheableRead()
describe() {
return ok(BLOG_POST_DESCRIBE);
}