Skip to content

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.

ToolWhen
RouteSpecA single declarative route — most contributions
RouteModuleA named group of routes sharing a basePath and auth
RouterScopeThe imperative API the runtime hands to extend(scope)
DescriptorsWhen 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.

dart
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.

dart
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:

dart
FeatureDescriptor(
  name: 'analytics.reports',
  extend: (scope) => reportsModule.applyTo(scope),
);

The module:

  1. Calls scope.mount(basePath, setup, moduleName: name). Every route registered inside is tagged with the module name — picked up by the RouteListenerRegistry so OpenAPI can group operations by module.
  2. 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:

dart
scope.get(path, handler);
scope.post(path, handler);
scope.put(path, handler);
scope.patch(path, handler);
scope.delete(path, handler);

Plus:

dart
scope.mount(basePath, (childScope) {
  // childScope.basePath == parentBasePath + basePath
});

scope.protect(['/admin', '/internal']);
// auth middleware will apply to these paths after mount completes

Use 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.

dart
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:

  1. Descriptor MOUNT outputs — plugins emit routes from their registries.
  2. Plugin declarative routes — each plugin's routes: list.
  3. Plugin imperative extend — each plugin's extend(scope).
  4. Feature declarative routes — each feature's routes: list.
  5. 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:

dart
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:

dart
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 ​