navree
Documentation

Route model binding

Bound<M> loads the record a route's path names, or answers 404: route keys, custom bindings, soft-deleted records and nested routes with BoundChild.

On this page

Most routes that take an id start by loading that record and answering 404 when it doesn't exist. Route model binding does it for you: a handler argument of type Bound<M> loads the record the path names, in the request's unit of work, before your handler runs.

src/modules/companies/controller.rs

GET /api/v1/companies/0199a7a0-... answers the company, and an id that names no company answers 404 in Navree's error shape, from the binding itself. Bound<M> dereferences to the Record<M>; into_inner() takes it out, or destructure it: Bound(company, _).

Which parameter, which column

A binding reads the path parameter named after the model in snake case ({company} for Company, {user_account} for UserAccount), else {id}, else the route's only parameter.

It looks the record up by the model's route key: its primary key, unless the model names another column:

Rust

The text of the parameter is read as the column's type first, so a malformed UUID, ULID or number names no record: that is a 404 without a query.

The model's query runs with its global scopes, in the request's unit of work. So a record another workspace owns is a 404 for a workspace-scoped model, and the same rules as everywhere else apply.

Soft-deleted records

A trashed record of a soft-deleting model is a 404 like any missing record. A route that works on trashed records, such as one that restores them, binds WithTrashed:

Rust

Custom bindings

A RouteBinding decides the parameter and the query for a route: implement it on a marker type and name it as Bound's second parameter.

Rust

query returns None when the text can't name a record, which answers 404 without a query. const WITH_TRASHED: bool = true binds trashed records too, and WithTrashed<ByTitle> wraps a binding of yours the same way.

Nested routes

In /companies/{company}/deals/{deal}, the deal must belong to the company: a deal id of another company should be a 404, not someone else's deal. BoundChild<P, C> binds the parent, then looks the child up among the parent's children only:

Rust

The parent comes from the parameter named after P (else the first one), the child from the one named after C (else the last). parent() and child() borrow them.

Bindings and access checks

A route that checks abilities on a record uses #[can(Update, Ticket, from = "ticket")] and an Authorized<Ticket, Update> argument instead: that loads the record the same way (by route key, with global scopes, in the unit of work), then checks the ability. See workspaces and access.