Skip to Content
OpenAPI Specification

OpenAPI Specification and @operation Decorator

Vovk.ts generates an OpenAPI specification from the procedures that have an operation object. It fills parameters, requestBody and responses from the validation models. The @operation decorator gives a procedure its operation object, with metadata such as summary, description and tags. @operation.tool and @operation.error give it one too. A procedure without any of them is left out of the specification, and deriveTools makes no tool of it either. The decorator takes the OperationObject type from openapi3-ts/oas31 , plus the Vovk.ts x-tool property for the deriveTools function.

src/modules/user/user-controller.ts
import { procedure, put, prefix, operation } from 'vovk'; import { z } from 'zod'; @prefix('users') export default class UserController { @operation({ summary: 'Update User', description: 'Update user information', }) @put('{id}') static updateUser = procedure({ // ... }); }

The validation models of a procedure map to the operation object like this:

  • params → parameters with in: "path", each one required. A {name} in the path that no params model describes is a string parameter.
  • query → parameters with in: "query". An object parameter gets style: "deepObject", since the server reads filter[status]=sold. An array parameter keeps the default style, form (tags=a&tags=b), and the server reads a single tags=a as a one-item array. OpenAPI has no style for an array of objects; the server reads items[0][a]=x, the form that Vovk.ts clients send.
  • body → requestBody with the application/json (or custom contentType) content type.
  • output → responses with status 200 and the application/json content type.
  • iteration → responses with status 200 and the application/jsonl content type, with an example of three lines. The server sends text/plain unless the Accept header includes application/jsonl.

A schema with an id, such as a Zod schema with .meta({ id }), goes to components.schemas. In a name, each character OpenAPI doesn’t allow becomes _: User Profile becomes User_Profile. When another schema already has the name, including one in openAPIObject.components, the RPC module, procedure and slot names go in front of it: UserRPCCreateUserBodyUser.

Configuring the OpenAPI Specification

Configure the specification in the vovk.config file, with the outputConfig.openAPIObject option. The object is merged into the generated specification, so you can set global properties such as info and servers.

vovk.config.js
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { openAPIObject: { info: { title: 'My app API', description: 'API for My App hosted at https://myapp.example.com/.', license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT', }, version: '1.0.0', }, servers: [ { url: 'https://myapp.example.com', description: 'Production', }, { url: 'http://localhost:3000', description: 'Localhost', }, ], }, }, }; module.exports = config;

You can also set openAPIObject for each segment, with outputConfig.segments.[segmentName].openAPIObject.

vovk.config.js
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { outputConfig: { segments: { admin: { openAPIObject: { info: { title: 'Admin API', description: 'API for Admin segment.', version: '1.0.0', }, }, }, }, }, };

Using the OpenAPI Specification

The generated RPC client exports an openapi object from its openapi module. For the composed client, it holds the specification of the whole back end. With the segmented client, each segment also exports its own specification.

import { openapi } from '@/client/openapi'; // composed client
import { openapi } from '@/client/admin/openapi'; // segmented client

Use the specification as a variable, or serve it as JSON from a controller in a static segment:

src/modules/static/openapi/openapi-controller.ts
import { get } from 'vovk'; import { openapi } from '@/client/openapi'; export default class OpenApiController { @get('openapi.json') static getSpec = () => openapi; }

A plain Next.js route handler can serve the spec too. Then you register no controller, and the route stays out of the generated schema:

src/app/openapi.json/route.ts
import { openapi } from '@/client/openapi'; export const GET = () => Response.json(openapi);

The openapiJson template writes openapi.json as a file of its own, without a client. Use it to serve the spec as a static file or to pass it to another tool:

npx vovk generate --from openapiJson --out ./public

On the client side, any OpenAPI documentation generator works. Scalar  is recommended, because Vovk.ts adds code samples for the generated RPC modules to the spec.

import { ApiReferenceReact } from "@scalar/api-reference-react"; import "@scalar/api-reference-react/style.css"; async function App() { return ( <ApiReferenceReact configuration={{ url: "/api/static/openapi.json" }} /> ); } export default App;
Scalar API reference generated from the OpenAPI specification

For a live demo, see the “Hello World” application spec . The “Hello World” page has the details.


@operation.tool sets attributes for the deriveTools function. They go under the x-tool key of the operation object.

src/modules/user/user-controller.ts
import { procedure, put, operation } from 'vovk'; export default class UserController { @operation.tool({ name: 'update_user', description: 'Update user information in the system', }) @operation({ summary: 'Update User', description: 'Update user information', }) @put('{id}') static updateUser = procedure({ // ... }); }

For details, see the deriveTools documentation.

Last updated on