Routes
Routes are how a server exposes its surface. Vyuh Server gives you four ways to mount them, each meant for a different shape of contribution.
| Tool | When |
|---|---|
RouteSpec | A single declarative route — most contributions |
RouteModule | A named group of routes sharing a basePath and auth |
RouterScope | The imperative API the runtime hands to extend(scope) |
| Descriptors | When the route table is a function of data (e.g. CRUD) |
RouteSpec — Declarative Single Routes
The smallest unit. One method, one path, one handler, optionally wrapped in route-local middleware.
FeatureDescriptor(
name: 'health',
routes: [
RouteSpec(
method: 'GET',
path: '/health',
handler: (req) async => Response.ok(body: Body.fromString('ok')),
),
RouteSpec(
method: 'POST',
path: '/internal/flush',
handler: _flushHandler,
middlewares: [_requireInternalToken()],
),
],
);Path parameters use Relic's :name syntax. The framework mounts each spec, wrapping the handler with middlewares in innermost-first order — middlewares.first is closest to the handler.
RouteModule — Named Groups with basePath + Auth
When several routes share a basePath or auth profile, group them. A RouteModule carries one name, one basePath, and a list of protectedPaths.
final reportsModule = RouteModule(
name: 'analytics.reports',
basePath: '/reports',
protectedPaths: ['/'], // every path under basePath
setup: (scope) {
scope.get('/active', _activeReportHandler);
scope.get('/audit', _auditReportHandler);
scope.get('/:id', _singleReportHandler);
},
);Mount it from a feature:
FeatureDescriptor(
name: 'analytics.reports',
extend: (scope) => reportsModule.applyTo(scope),
);The module:
- Calls
scope.mount(basePath, setup, moduleName: name). Every route registered inside is tagged with the module name — picked up by theRouteListenerRegistryso OpenAPI can group operations by module. - Calls
scope.protect(protectedPaths)inside the sub-scope so the auth middleware applies before any handler runs.
This is the canonical shape when a single feature owns several related endpoints with shared auth.
RouterScope — Imperative API
Inside a RouteModule.setup or a feature's extend(scope), you talk to the live RouterScope. Five method shortcuts cover most HTTP verbs:
scope.get(path, handler);
scope.post(path, handler);
scope.put(path, handler);
scope.patch(path, handler);
scope.delete(path, handler);Plus:
scope.mount(basePath, (childScope) {
// childScope.basePath == parentBasePath + basePath
});
scope.protect(['/admin', '/internal']);
// auth middleware will apply to these paths after mount completesUse extend only when the declarative routes: slot doesn't fit — computed paths from a config file, conditional registration based on env vars, legacy URL fallbacks.
Descriptor-Driven Routes
When the route table is a function of data — every entity gets the same six endpoints with different schemas — a descriptor is the right shape.
descriptors: [
EntityCrudDescriptor<Product>(
basePath: '/products',
config: productConfig,
),
EntityCrudDescriptor<Order>(
basePath: '/orders',
config: orderConfig,
),
];The feature ships zero handler code. The EntityCrudPlugin reads each descriptor at boot and emits the standard route table from the typed config. See Entity CRUD Plugin.
Mount Order
When runtime.start() builds the router, routes mount in this order:
- Descriptor MOUNT outputs — plugins emit routes from their registries.
- Plugin declarative routes — each plugin's
routes:list. - Plugin imperative extend — each plugin's
extend(scope). - Feature declarative routes — each feature's
routes:list. - Feature imperative extend — each feature's
extend?.call(scope).
Within each phase, items iterate in topo order. The first match wins when two routes claim the same method:path pair — so features can override plugin routes if needed.
OpenAPI-Aware Routing
When OpenApiPlugin is registered, every route is auto-discovered via the RouteListenerRegistry. Bare routes appear in /openapi.json with just method + path. To enrich them with parameters, schemas, and security, use the apiGet / apiPost extension methods:
scope.apiGet(
'/items',
listItems,
doc: Operation(
summary: 'List items',
tags: ['catalog'],
parameters: [
Parameter.query(
name: 'page',
schema: Schema.integer(defaultValue: 1),
),
],
responses: {
'200': Response(
description: 'Page of items',
content: {
'application/json': MediaType(schema: Schema.array(...)),
},
),
},
),
);For the declarative routes: slot, use the verb-named factories on ApiRoute — ApiRoute.get(...), ApiRoute.post(...), ApiRoute.put(...), ApiRoute.patch(...), ApiRoute.delete(...). Each returns a normal RouteSpec and attaches the Operation to the registry in one call. See OpenAPI Plugin.
Built-in Response Helpers
The core ships a small set of helpers to keep handlers terse:
import 'package:vyuh_server/vyuh_server.dart';
Future<Response> getProduct(Request req) async {
final id = req.pathParameters['id']!;
final product = await loadProduct(id);
// JSON responses with consistent shape:
return jsonResponse(body: {'success': true, 'data': product.toJson()});
// or TypedResponse.created(...).toResponse(...)
// Typed response with pagination:
// return typedPaginated(items: products, total: 42, page: 1);
}See the API Reference for the full list.
Where to Go Next
- Middleware & Context — how onion ordering works
- The
vyuhAccessor — typed verbs available from any handler - Entity CRUD Plugin — the canonical descriptor-driven route mounter