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