OpenAPI — vyuh_server_plugin_openapi
Plugin that auto-generates an OpenAPI 3.0 spec from your running router and mounts a docs explorer. Self-contained — drop it into plugins: and you get /openapi.json + /docs for free.
Install
dependencies:
vyuh_server_plugin_openapi:
hosted: https://pub.vyuh.tech
version: ^0.1.0Basic Wiring
import 'package:vyuh_server/vyuh_server.dart';
import 'package:vyuh_server_plugin_openapi/vyuh_server_plugin_openapi.dart';
final runtime = await VyuhServer.bootstrap(
plugins: [
OpenApiPlugin(
title: 'SaaS API',
version: '2.4.0',
description: 'Multi-tenant SaaS backend',
),
],
);The startup banner picks up the plugin's announcement:
saas-api listening on http://localhost:8080
Docs: http://localhost:8080/docs
Spec: http://localhost:8080/openapi.jsonAuto-Discovery
Every route the framework mounts — declarative RouteSpecs, imperative scope.get(...) calls, EntityCrudPlugin-emitted CRUD endpoints — is picked up by the RouteListenerRegistry. Bare routes appear in the spec with just method + path + a synthesized operation summary.
No annotations, no manual registration. The plugin reads the live router state.
Documented Operations
To enrich a route with the full Operation shape (parameters, schemas, security, callbacks), use the apiGet / apiPost extensions on RouterScope:
scope.apiGet(
'/products',
listProducts,
doc: Operation(
summary: 'List products',
tags: ['catalog'],
parameters: [
Parameter.query(
name: 'page',
schema: Schema.integer(defaultValue: 1),
),
Parameter.query(
name: 'page_size',
schema: Schema.integer(defaultValue: 50, maximum: 200),
),
Parameter.query(
name: 'filter',
description: 'cdx_query JSON',
schema: Schema.string(),
),
],
responses: {
'200': Response(
description: 'Page of products',
content: {
'application/json': MediaType(
schema: Schema.object(properties: {
'items': Schema.array(items: productSchema),
'total': Schema.integer(),
'page': Schema.integer(),
}),
),
},
),
},
),
);The plugin merges this Operation with the auto-discovered path entry. Routes without a doc still appear; routes with a doc get the full treatment.
Declarative Routes with ApiRoute
For the declarative routes: slot, use the verb-named factories on ApiRoute:
FeatureDescriptor(
name: 'catalog',
routes: [
ApiRoute.get(
'/products/featured',
_featuredHandler,
doc: Operation(
summary: 'List featured products',
tags: ['catalog'],
responses: { ... },
),
),
ApiRoute.post(
'/products',
_createProductHandler,
doc: Operation(
summary: 'Create a product',
tags: ['catalog'],
),
),
],
);ApiRoute.get / .post / .put / .patch / .delete each return a normal RouteSpec and attach the Operation to the registry — one call site, both effects.
The factory shape mirrors the imperative scope.apiGet / scope.apiPost extension methods one-for-one, so switching between the declarative routes: slot and the imperative extend(scope) callback never asks you to learn a second API:
// Imperative (inside extend(scope)):
scope.apiGet('/items', listItems, doc: Operation(...));
// Declarative (inside routes: [...]):
ApiRoute.get('/items', listItems, doc: Operation(...));Customizing Paths
OpenApiPlugin(
title: 'My API',
specPath: '/_internal/openapi.json', // hide from external listings
docsPath: '/_internal/docs',
);Choosing a Docs Renderer
The plugin ships two:
Scalar (default)
OpenApiPlugin(
title: 'My API',
docsRenderer: const ScalarRenderer(),
);A modern, dark-themed OpenAPI explorer with built-in request-builder and code samples (curl, Dart, JS, Python).
Swagger UI
OpenApiPlugin(
title: 'My API',
docsRenderer: const SwaggerUiRenderer(),
);The familiar Swagger UI layout.
Custom Renderer
DocsRenderer is a one-method interface:
abstract class DocsRenderer {
String renderHtml({
required String title,
required String specPath,
});
}Drop in Redoc, RapiDoc, or any other HTML — the plugin serves the returned string at docsPath.
Servers + Auth
OpenApiPlugin(
title: 'SaaS API',
serverUrl: 'https://api.saas-app.com',
// Authentication is declared per Operation's `security`
);When serverUrl is set, the docs explorer uses it as the request base URL; otherwise the UI infers from the page origin (useful in dev where you load the spec from the same host).
Generating the Spec at Build Time
For a CI-published spec (consumer-facing SDKs, contract tests), reach for the generator directly without booting the server:
// tool/dump_spec.dart
import 'dart:io';
import 'package:vyuh_server/vyuh_server.dart';
import 'package:vyuh_server_plugin_openapi/vyuh_server_plugin_openapi.dart';
Future<void> main() async {
final runtime = await VyuhServer.bootstrap(
name: 'spec-dump',
plugins: [
EntityCrudPlugin(),
OpenApiPlugin(title: 'SaaS API', version: '2.4.0'),
],
features: allFeatures,
);
// Mount the routes without actually listening:
await runtime.start(port: 0, banner: false);
final spec = OpenApiGenerator(title: 'SaaS API').generate();
await File('openapi.json').writeAsString(jsonEncode(spec.toJson()));
await runtime.shutdown();
}Wire into melos run dump-spec or a GitHub Action that publishes the spec on every merge to main.
Where to Go Next
- Routes —
RouteSpec,ApiRoute, scope imperatives - Entity CRUD Plugin — every CRUD route documented automatically