Skip to main content

Request Lifecycle

Every Rhino API request follows a predictable pipeline from route matching to JSON response. Understanding this flow helps you debug issues and know where to hook in custom behavior.

Request Flow Overview​

Step-by-Step Breakdown​

1. Route Matching​

When you register a model in src/rhino.config.ts, Rhino generates a set of routes automatically. Each route is bound to a specific action on the GlobalController:

ActionRouteController Method
IndexGET /api/:modelindex()
StorePOST /api/:modelstore()
ShowGET /api/:model/:idshow()
UpdatePUT /api/:model/:idupdate()
DestroyDELETE /api/:model/:iddestroy()
TrashedGET /api/:model/trashedtrashed()
RestorePOST /api/:model/:id/restorerestore()
Force DeleteDELETE /api/:model/:id/force-deleteforceDelete()

The model slug and route group key are stored in the route metadata, so the controller knows which model registration to resolve. Routes are named rhino.{group}.{slug}.{action} (e.g., rhino.tenant.posts.index). See Route Groups for details.

2. Auth Guard​

For route groups other than public, the JwtAuthGuard runs first. It verifies the JWT from the Authorization: Bearer <token> header and attaches the authenticated user to req.user.

Routes in groups with skipAuth: true (such as the reserved public group) skip this step.

3. Route Group Middleware & Organization Resolution​

Middleware classes defined on the route group run next. For the tenant route group, this is typically ResolveOrganizationMiddleware, which:

  • Extracts the :organization route parameter (or resolves it from the host for subdomain mode) and looks up the Organization model by the configured identifier column (id, slug, or uuid).
  • Verifies the authenticated user belongs to the organization.
  • Sets req.organization for downstream consumers.

4. Model Middleware​

If the registration defines middleware or actionMiddleware, those NestJS middleware classes are applied to the appropriate routes during registration:

src/rhino.config.ts
posts: {
model: 'post',
// Applied to all routes for this model
middleware: [ThrottleMiddleware],
// Applied only to specific actions
actionMiddleware: {
store: [VerifiedMiddleware],
destroy: [AdminMiddleware],
},
},

5. Model Resolution​

The GlobalController extracts the model slug from the route metadata, looks up the ModelRegistration in the models map, and accesses the corresponding Prisma delegate via PrismaService.model(registration.model).

For member actions (show, update, destroy, restore, force-delete), the record is loaded by matching the :id route parameter against the model's route key: routeKey on the registration if set, otherwise the global routeKey config, otherwise the primary key. When a custom route key is configured, the parameter is always compared as a string. Unless configured, nothing changes — the lookup uses the primary key as always. See Models — Route Key.

6. Authorization​

The controller resolves the policy from the registration's policy field (if defined) and calls the appropriate policy method (each also receives the resolved organization for tenant routes):

ActionPolicy MethodArguments
IndexviewAny(user, org?)—
Showview(user, record, org?)Loaded record
Storecreate(user, org?)—
Updateupdate(user, record, org?)Loaded record
Destroydelete(user, record, org?)Loaded record
TrashedviewTrashed(user, org?)—
Restorerestore(user, record, org?)Loaded record
Force DeleteforceDelete(user, record, org?)Loaded record

If no policy is defined, all actions are allowed.

For ?include= parameters, the controller also checks viewAny permission on each related model's policy. A 403 is returned if the user lacks permission for any requested include.

7. Organization Scoping​

When an organization is present in the request context (set by middleware in the tenant route group) and the registration has belongsToOrganization: true, the controller applies organization filtering to the query. Non-tenant route groups skip this step. The scoping strategy follows this order of precedence:

  1. Resource IS the Organization model -- restrict to the current org's primary key
  2. Model has an organizationId column -- simple WHERE organizationId = ?
  3. owner chain is configured / auto-detected -- Rhino walks the foreign-key relation(s) named by owner to find a model with organizationId and filters via a nested relation condition
  4. No relationship found -- model is global (no scope applied)
Two kinds of "scope"

This step (plus any always-on scopes: [...] classes) is the enforced scope layer — it runs on every request and no query parameter can bypass it. It is distinct from a client-selected named scope (?scope=name), applied during Query Execution below. A named scope is ANDed after organization scoping and can only narrow the authorized set, never widen it.

8. Validation​

For store and update actions, the controller resolves permitted fields from the policy (permittedAttributesForCreate or permittedAttributesForUpdate) and checks the raw input for forbidden fields (returns 403). It then runs the request class registered for that action — prepare() → authorize() → rules() → safeParse — and the parse output becomes the write payload. A model with no request class for that action falls back to the deprecated registration schemas.

An authorize() that returns false is a 403 identical to a policy denial. A failed parse is a 422:

Response
{
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"details": {
"errors": {
"title": ["Required"],
"content": ["Expected string, received number"]
}
}
}

The cross-tenant foreign-key check (fkConstraints) runs on the resulting payload and answers 422 CROSS_TENANT.

9. Query Execution​

The QueryBuilderService translates URL query parameters into Prisma operations:

  1. Scope selection -- ?scope=availableForDrivers
  2. Filters -- ?filter[status]=published
  3. Sorts -- ?sort=-createdAt,title
  4. Search -- ?search=nestjs
  5. Includes -- ?include=author,comments
  6. Field selection -- ?fields[posts]=id,title
  7. Pagination -- ?page=1&per_page=20

Scope selection resolves the ?scope= value against the registration's namedScopes (falling back to defaultScope when absent) and ANDs the scope's where-fragment into the query. A non-whitelisted name returns a 403 — unlike filters/sorts, it is not silently ignored. See Querying — Named Scopes.

10. Response​

The controller returns a JSON response. For paginated results, metadata is sent in response headers:

X-Current-Page: 1
X-Last-Page: 10
X-Per-Page: 20
X-Total: 195

The response body contains the data array (for index endpoints) or a single object (for show/store/update). Delete operations return a 204 No Content response.

Action Exclusion​

Models can opt out of specific routes using exceptActions:

src/rhino.config.ts
settings: {
model: 'setting',
// Only allow index and show -- no create, update, or delete
exceptActions: ['store', 'update', 'destroy', 'trashed', 'restore', 'forceDelete'],
},

Valid action names: index, show, store, update, destroy, trashed, restore, forceDelete.

Error Responses​

Rhino uses a consistent JSON error format across all actions:

StatusMeaningExample
403Authorization denied{ "message": "This action is unauthorized." }
404Resource not found{ "message": "Organization not found" }
422Validation failed{ "errors": { "title": ["..."] } }
204Success (no content)Empty body (delete operations)