navree

OpenAPI

An OpenAPI 3.1 document generated from your routes at boot, a docs UI, exports for typed clients, and API resources.

On this page

Featureapi Every Navree route already knows its method, path, access rules and, if you tell it, its request and response types. navree-api turns that into an OpenAPI 3.1 document when the app boots, serves it with an interactive docs page, and writes it to a file so front ends can generate typed clients from it. There is no separate spec file to keep in sync: the document comes from the code.

Setup

Add ApiModule to your app:

src/app.rs

You now have two public routes under the API prefix (http.api_prefix, /api by default):

RouteWhat it serves
GET /api/openapi.jsonThe document, pretty-printed JSON.
GET /api/docsThe Scalar docs UI, reading that document.

Neither appears in the document itself. The api section is optional:

config/app.toml
KeyDefaultWhat it does
api.titleNavree APIinfo.title, and the docs page's title.
api.version1.0.0info.version, your API's version (not Navree's).
api.servers[]Server URLs listed in the document. Without any, clients use the URL they loaded the document from. From the environment, comma-separated: APP_API__SERVERS=https://a.example,https://b.example.
api.docs_uitrueServe the docs UI at {api_prefix}/docs.

Or configure it in code:

Rust

ApiModule imports HttpModule, so the document covers every route of the app, framework routes included (authentication, notifications, health checks and so on).

Describing routes

Without any annotation, a route is documented with its path, path parameters, security and access-related error responses. To add the rest, put #[api(...)] on the route. This is the CRM's deals controller:

crates/crm-deals/src/controller.rs
KeyWhat it documents
summary = "..."The one-line summary.
description = "..."A longer description (Markdown).
tag = "...", tags("A", "B")The groups the docs UI shows the route under.
request = TThe JSON request body (required), from T's schema.
response = TThe success response's JSON body.
status = 201The success status (default 200).
responses(400, 404)Error statuses the route can answer, documented with the error schema.

Give a route a name with #[name("deals.index")] to set its operationId; routes without a name have none. Routes written by hand describe themselves with RouteDef::api(ApiMeta { .. }), where request and response are Some(schema_fn::<T>) (both from navree::http).

What is generated for you

  • Paths are the mounted paths, prefix and version included, in OpenAPI form ({*rest} becomes {rest}). Every {param} is a required path parameter of type string, whatever its Rust type.
  • Security: every route that isn't public lists the bearer (JWT access token) and cookie (session) security schemes and documents 401. Routes checked by the route authoriser (#[roles], #[permissions], #[can]) also document 403.
  • Errors: every documented error status uses the HttpError schema, the NestJS error format (statusCode, message, error, and errors for validation failures).
  • Tags are listed in the order routes first use them.

The cookie scheme names the session cookie from session.cookie (default navree_session). It is listed even in apps that only use bearer tokens, such as the CRM.

Schemas

Request and response types describe themselves with utoipa's ToSchema derive. Add utoipa to your app's dependencies (the generated app doesn't include it):

Cargo.toml

Then derive ToSchema next to serde. Doc comments become descriptions, and #[schema(as = Name)] sets the component name the document uses:

src/modules/notes/resource.rs

The routes then name these types in #[api(..)]:

src/modules/notes/controller.rs

The request and response schemas of every route, and every schema they refer to, become components.schemas. Two kinds of mistakes stop the boot, so a broken document never ships:

  • Two different schemas with the same name (ApiError::SchemaConflict): the error names both routes; rename one type with #[schema(as = OtherName)].
  • A reference to a schema nothing registered (ApiError::MissingSchemas): utoipa doesn't collect the schemas of generic type arguments, so a response such as your own generic Page<Note> refers to a Note component that nothing provides. Register it in its module with m.api_schema::<T>().

Two different body schemas with the same name are not an error: the first becomes the component and the others are inlined where they are used.

m.api_schema::<T>() (from the ModuleBuilderApiExt trait, in the prelude) adds any type to the components, with the schemas it refers to. The CRM uses it for its list filters, which no route body names:

crates/crm-companies/src/companies/mod.rs

Paginated responses

navree-db's Page<T> doesn't derive ToSchema, so #[api(response = ..)] can't name it. Describe each list with its own named type, as the CRM does, while the handler keeps returning Page<T> (pagination):

crates/crm-deals/src/resource.rs

The route then says #[api(response = DealPage)], as in the controller above.

Limitations

The document is honest about what it knows, and there are things it can't express yet:

  • Query parameters aren't documented. ApiMeta has no field for them, so list filters, sorting and paging parameters are missing. Describe them in the route's description and, as the CRM does, register the filter type with m.api_schema::<T>() so its fields appear among the schemas. Generated clients then need these parameters added by hand (see below).
  • Bodies are JSON only. A non-JSON response, such as the CRM's CSV export at GET /api/v1/contacts/export, has no response schema, and multipart request bodies (uploads) can't be described.
  • Path parameters are always strings, even when the handler parses a UUID or a number.
  • Some framework routes have no schemas: /api/me/abilities and /api/access/* (navree-access), /broadcasting/auth and /app/{key} (real time), /health/* and /metrics.
  • User resource extensions aren't reflected. GET /api/auth/me and the login response's user are documented as UserResource, without the fields a UserResourceExtension adds (the CRM's roles).
  • No shared page metadata component. Every page schema inlines its meta.

These are tracked in the framework's open issues and on the roadmap.

Exporting the document

The openapi:export command writes the document to a file, openapi.json by default, creating missing directories:

Shell
Text

It writes exactly what GET /api/openapi.json serves. Commit the file if your front end builds from it, and re-export after changing routes or types.

Typed clients from the document

The CRM's front end, apps/crm-web, generates its TypeScript types from the exported document with openapi-typescript and calls the API with openapi-fetch:

Shell

The app never imports schema.d.ts directly. src/lib/api/types.ts gives the schemas friendly names, so a renamed schema only needs that file to change, and adds the list endpoints' query parameters that the document can't describe yet:

apps/crm-web/src/lib/api/types.ts

API resources

Front ends that check permissions with CASL need to know what type each JSON object is. Navree's convention is a __type field holding the subject type ("Deal"), which CASL reads with detectSubjectType: (o) => o.__type. The CRM puts it in its resources as a renamed field, so the schema documents it (see NoteResource above).

For resources that don't carry the field, navree::api has helpers:

Rust
  • resource_json(&value) serializes a resource and adds "__type": T::TYPE.
  • mask_fields(value, Some(&fields)) keeps only the listed top-level fields, plus id and __type, in an object or in each object of an array; None keeps everything. Pass the fields an ability permits for field-level access control; see Authorization.

See Responses and API resources for how handlers return them.

Testing

The document is built at boot, so a schema conflict or a missing schema already fails any test that boots the app. To check what a route documents, fetch the document like a client would:

src/modules/notes/tests/mod.rs

Each CRM feature crate has a test like this (for example crates/crm-deals/tests/deals/openapi.rs), checking that its routes refer to the schema names apps/crm-web imports.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks