Routing
access-router gives you three router classes:
RootRouterfor batch requestsModelRouterfor Mongoose modelsDataRouterfor in-memory collections
The main factory is acl.createRouter(...).
Router Factories
import mongoose from 'mongoose';
import acl from '@web-ts-toolkit/access-router';
const modelRouter = acl.createRouter('User', { basePath: '/users' });
const dataRouter = acl.createDataRouter('fruit', { basePath: '/fruit', data: [] });
const rootRouter = acl.createRouter({ basePath: '/batch', operationAccess: true });
const typedRouter = acl.createRouter(
mongoose.model('User', new mongoose.Schema({ name: String })),
{ basePath: '/users' },
);
createAccessRuntime() returns a fresh acl instance with isolated runtime state.
Mounting
Each router exposes:
routerfor the internal JsonRouter instanceroutesfor the underlying Express routerfullBasePathfor the normalized mounted path
Use combineRoutes(...) to mount several routers at once.
import express from 'express';
import acl, { combineRoutes } from '@web-ts-toolkit/access-router';
app.use(combineRoutes(fruitRouter, userRouter, rootRouter));
app.use(acl.combineRoutes(fruitRouter, userRouter, rootRouter));
combineRoutes(...) accepts ModelRouter, DataRouter, RootRouter, or a plain Express Router.
Model Router Routes
Default route segments are __query and __mutation.
| Base guard | Basic endpoint → override | Advanced endpoint → override |
|---|---|---|
list | GET /base → basicList | POST /base/__query → advancedList |
read | GET /base/:id → basicRead | POST /base/__query/:id, POST /base/__query/__filter → advancedRead |
create | POST /base → basicCreate | POST /base/__mutation → advancedCreate |
update | PATCH /base/:id → basicUpdate | PATCH /base/__mutation/:id → advancedUpdate |
upsert | PUT /base → basicUpsert | PUT /base/__mutation → advancedUpsert |
count | GET /base/count → basicCount | POST /base/count → advancedCount |
distinct | GET /base/distinct/:field → basicDistinct | POST /base/distinct/:field → advancedDistinct |
GET /base/new and DELETE /base/:id keep unpaired new and delete guards.
Filtered count/distinct POSTs are advanced even without an advanced path segment.
Custom idParam, queryRouteSegment, and mutationRouteSegment change the paths,
not the guard names.
An exact variant replaces its base guard; only undefined inherits. Denials return
HTTP 401 Unauthorized before validation/service dispatch. All endpoints remain
registered in router.router.getEndpoints() and OpenAPI. HEAD fallback uses its
corresponding GET's basic guard. See Configuration
for fallback, model-default, and closed-subdocument-field behavior.
import acl from '@web-ts-toolkit/access-router';
// User is already registered with the default runtime.
const router = acl.createRouter('User', {
basePath: '/users',
operationAccess: { list: true, read: true, basicRead: false },
permissionSchema: { name: true },
});
router.operationAccess('basicRead', false); // Live key update.
Among top-level model routes, this denies GET/HEAD /users/:id only. Both advanced reads and GET /users remain
allowed, subject to ordinary field/row policy; new/count/distinct/mutation guards
are unaffected. Subdocument reads use the documented field fallback. With
{ list: true, advancedList: false }, only POST
/users/__query is denied among the list pair.
Basic model list GETs accept select and sort query params, for example
GET /base?select=name&sort=name%20-createdAt. sort is a single
space-separated list of fields, prefixed with - for descending; order sets
multi-field priority. Sort authorization and sortableFields/
stripDisallowedSort apply exactly as on advanced lists. Omitted sort keeps
backend defaults; ?sort= disables them. Basic GET-by-id reads also accept
select.
Sub-document routes are generated for every referenced subpath discovered on the model schema.
For each subdocument path sub, these routes are added:
| Base guard | Basic endpoint → override | Advanced endpoint → override |
|---|---|---|
subs.<sub>.list | GET /base/:id/<sub> → subs.<sub>.basicList | POST /base/:id/<sub>/__query → subs.<sub>.advancedList |
subs.<sub>.read | GET /base/:id/<sub>/:subId → subs.<sub>.basicRead | POST /base/:id/<sub>/:subId/__query → subs.<sub>.advancedRead |
Unpaired subdocument mutations retain their base guards:
PATCH /base/:id/<sub>andPATCH /base/:id/<sub>/:subId→subs.<sub>.updatePOST /base/:id/<sub>→subs.<sub>.createDELETE /base/:id/<sub>/:subId→subs.<sub>.delete
That gives you generated list, advanced list, bulk update, read, advanced read, update, create, and delete operations for each discovered subdocument collection.
Data Router Routes
Default route segment is __query.
| Base guard | Basic endpoint → override | Advanced endpoint → override |
|---|---|---|
list | GET /base → basicList | POST /base/__query → advancedList |
read | GET /base/:id → basicRead | POST /base/__query/:id, POST /base/__query/__filter → advancedRead |
These four keys use data-router options only, with no model-default inheritance.
Set idParam: 'id' explicitly for the illustrated data identifier paths.
Both advanced reads share advancedRead; HEAD follows the basic GET guard.
Root Router
RootRouter handles batch payloads that dispatch to model or data operations.
Use it when you want a single request to run multiple operations in order.
Construct it with the single-options overload
api.createRouter({ basePath: '/batch', operationAccess: true }) and mount .routes.
Its runtime must also own the target registrations. Limits default to
maxBatchEntries: 100, maxOrderGroups: 25, maxConcurrentOperations: 10.
maxOrderGroups bounds both distinct group count and each numeric order:
at the default, orders are integers in 0..25 inclusive (omission is 0), but at most
25 different values can occur. A lone order 26 or all 26 values 0..25 reject before
entry dispatch. Invalid/non-positive/unsafe constructor limits use the defaults.
Groups execute in ascending order; operations within each group share the whole- operation concurrency ceiling. Results retain input order. Hook-chain and persistence pools are separate request/runtime budgets. Batches are non-transactional; unexpected rejection can leave already-committed writes.
Each batch item uses:
target:modelordataname: the model or data router nameop: the operation nameorder: optional execution bucket
Operation-specific fields are kept at the top level:
idfor single-document operationssubandsubIdfor sub-document operationsfieldfordistinctfilterfor query-based lookupsdatafor writesargsfor selection, populate, paging, and task inputsoptionsfor behavior switches such as permissions and counts
Direct routes and root entries are request-only. Trusted service authorization, opaque
document handles, private prepared/execution controls and the removed overrides container
are rejected in control slots, including nested include/populate/$$sq options and
validator-transformed envelopes. They are not client request features. Business data fields
with those names remain valid under ordinary permissions. Use internal services
from server code for per-call authority; generated route guards still apply independently.
Root entries retain existing base operation names and target base entry checks;
basicList/advancedRead (and the other variants) are not valid op values.
Allowing a direct variant does not override a denied root base entry. A denied
entry has status 401 inside the HTTP 200 batch envelope. The enclosing root
endpoint's own operationAccess remains a scalar guard.
Root entries accept the same include shapes as direct routes: legacy
localField/foreignField joins and correlated entries
(mode: 'correlated' with { "$parent": "<field>" } markers resolved per
parent against the immediate parent snapshot). Direct and root validators
share one schema, so verdicts agree; malformed correlated input is a
controlled BadRequest.
The response wraps each operation as:
{
index: number;
target: 'model' | 'data';
name: string;
op: string;
statusCode: number;
message: string;
result: unknown;
}
result keeps the public service response instead of flattening it into a second batch-only
format. Internal context(s), prepared state, document handles and query diagnostics are omitted.
OpenAPI Router
access-router can also mount a separate documentation router:
app.use(acl.createOpenApiRouter());
By default this exposes:
GET /openapi.jsonGET /docs
You can customize those paths with jsonPath and docsPath, or disable Swagger UI entirely with docsPath: false.
Mutability
Build-time route-shape options are immutable after router construction.
Model router build-time options:
basePathparentPathidParamqueryRouteSegmentmutationRouteSegment
Data router build-time options:
basePathparentPathidParamqueryRouteSegment
Behavior options such as filters, hooks, permissions, and defaults can be changed later with set(...), setOption(...), or setOptions(...).
Operation variants are live behavior options. router.operationAccess('basicRead', false) updates one key. Passing an object replaces the whole operationAccess
object shallowly, not a deep merge. router.options stays a frozen construction
snapshot; read current stored values through the owning runtime's getters.