Skip to content

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

yaml
dependencies:
  vyuh_server_plugin_openapi:
    hosted: https://pub.vyuh.tech
    version: ^0.1.0

Basic Wiring

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

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

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

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

dart
// Imperative (inside extend(scope)):
scope.apiGet('/items', listItems, doc: Operation(...));

// Declarative (inside routes: [...]):
ApiRoute.get('/items', listItems, doc: Operation(...));

Customizing Paths

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

dart
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

dart
OpenApiPlugin(
  title: 'My API',
  docsRenderer: const SwaggerUiRenderer(),
);

The familiar Swagger UI layout.

Custom Renderer

DocsRenderer is a one-method interface:

dart
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

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

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