This version is in beta. Some features may change before release.

REST plugin

Auto-generated JSON CRUD from your models, plus authentication, permissions, filtering, pagination.

umbral-rest walks the same model registry the migration engine uses and mounts a JSON CRUD surface for every registered model. Add the plugin, get /api/<table>/ on List + Retrieve + Create + Update + Delete, plus filtering, free-text search, FK expansion (?include=), sparse fieldsets (?fields=), pagination, and an OpenAPI spec everything else (the umbral-playground UI, code generators, third-party clients) reads from.

Code
rust
use umbral::prelude::*;
 
App::builder()
.model::<Article>()
.plugin(umbral_rest::RestPlugin::default())
.build()?;
// → GET/POST/PATCH/PUT/DELETE under /api/article/

The two access controls

A request goes through two gates before the handler runs:

  1. Authentication answers who are you?. It reads the headers, maybe hits the database, returns an Identity (or None for anonymous).
  2. Permission answers are you allowed?. It takes the Identity and the action (List, Retrieve, Create, Update, Delete, or Custom) and either passes or returns a 401 / 403.

Authentication defaults to NoAuthentication (every request anonymous). Permission defaults to ReadOnly: anonymous reads pass, writes get 403. So a fresh RestPlugin::default() serves a public read-only API; POST / PATCH / DELETE return 403 until you opt in with a per-resource .permission(...) or the plugin-wide .default_permission(...).

PageWhat it covers
AuthenticationSessionAuthentication, BearerAuthentication, ChainAuthentication, writing your own
PermissionsAllowAny, IsAuthenticated, IsStaff, ReadOnly, Or / And combinators, writing your own

Other surfaces

  • Model exposure. Every registered model is auto-served at /api/<table>/ (opt-out, not opt-in); a set of framework-internal tables (auth_user, session, umbral_migrations, the permissions_* tables, task_row, admin_audit_log) is blocked by default, and password_hash is stripped from every response un-overridably. Tighten with include_only / exclude, or deliberately opt in with expose. See Model exposure.
  • Filtering, search, pagination. Every list endpoint exposes ?field__op=value (lookups: eq, ne, gt, gte, lt, lte, in, contains, icontains, startswith, isnull), ?search=term, and ?page=2&page_size=50 (page-number pagination) or ?limit=&offset= once you wire a Pagination class via RestPlugin::paginate(...). Default is no pagination. ?search= is a case-insensitive substring match (icontains) across the resource's text columns, ORing UPPER(col) LIKE UPPER('%term%') over each, and matching integers/bools by equality when the term parses. It is not full-text search: it doesn't stem or rank, and it skips tsvector columns. For word-aware, index-backed search use a TsVector column and .matches_websearch(...). Restrict the searched columns with ResourceConfig::search_fields(["title", "body"]), or turn it off with disable_search().
  • FK expansion and sparse fieldsets. ?include=author,category inlines related rows (select_related under the hood, up to 3 hops); ?fields=id,title trims the response to named columns. Sparse fieldsets project into included relations: ?include=created_by&fields=created_by__name (or the dotted created_by.name) prunes the nested object down to just name, and a multi-hop author__profile__bio prunes each level. __ and . are interchangeable separators, depth is capped at 3 hops, and a nested path against a relation you didn't ?include= silently leaves the raw FK integer in place.
  • List cap. A list response returns at most 1000 rows. An unbounded query is clamped to that ceiling; a paginator passes its own concrete limit.
  • OPTIONS / method discovery. Every resource answers OPTIONS with 204 No Content and an Allow header - OPTIONS, GET, POST on a collection (plus PATCH, DELETE when .bulk() is on), OPTIONS, GET, PUT, PATCH, DELETE on a detail URL - instead of a bare 405. CORS-preflight OPTIONS is handled separately by the cors layer.
  • Custom actions. ResourceConfig::action("publish", Method::POST, ActionScope::Detail, |ctx| async move { ... }) adds a non-CRUD endpoint that runs under the same auth + permission gate.
  • Field scoping. ResourceConfig::hide("password_hash") strips a column from responses, and a hidden field can't be written back either (the write body is scrubbed of it). hide accepts a single field or many: .hide(["password_hash", "ssn"]). Key a config off the model instead of a literal table with ResourceConfig::for_::<AuthUser>().hide(["password_hash", "email"]), or hide on the plugin builder with RestPlugin::hide_model::<AuthUser>(["password_hash"]) (uses AuthUser::TABLE, so a typo is a compile error). noform / noedit field attributes control write-side behaviour.

The plugin contract itself is documented in arch.md; this folder covers the everyday consumer surface.

restapijsonplugin