Skip to Content
"Hello World!"

“Hello World” Example

About this example: Despite the name, this example covers many features: validation, streaming, clients in several languages, and OpenAPI output. For the minimal setup, see the Quick Start guide first.

The “Hello World” app at hello-world.vovk.dev  is a Next.js app built with Vovk.ts. It shows the core features with:

  • Back-end:
    • UserController with an updateUser procedure (POST /api/users/{id}).
    • StreamController with a JSON Lines procedure, streamTokens (GET /api/streams/tokens).
    • OpenApiController with getSpec (GET /api/static/openapi.json), which serves the generated OpenAPI spec. The /openapi page shows it as documentation.
  • Front-end:
    • A form, with a JSON Lines streaming demo above it.
  • Configuration:
    • Client-side validation, the segmented and composed TypeScript clients, Rust and Python clients, an npm bundle, and OpenAPI metadata (info and servers).

The source is in the GitHub repository . The generated files are committed under dist , tmp_prebundle , dist_rust  and dist_python , so you can read them. In a real project, list them in .gitignore.

The code on this page comes from the repository on GitHub. The live pages are embedded as iframes.

Running the Example Locally

Copy the example. This also installs its dependencies:

npx create-next-app@latest --example https://github.com/finom/vovk/tree/main/examples/hello-world vovk-hello-world cd vovk-hello-world

Start the dev server:

npm run dev

Then open http://localhost:3000 .

Topics and Concepts Covered

Live Demo

The demo has a form without native validation attributes, and a “Disable client-side input validation” checkbox that toggles the disableClientValidation option. “Notification type” has an invalid value on purpose, so you can see both the client and the server validation fail.

UserController and UserService

UserController and UserService implement /api/users/{id} with updateUser.

The procedure maps POST with @post, and procedure() validates body, params, query and output with Zod. Each schema calls meta to add description and examples to the OpenAPI output. To show nesting, body holds email and a profile object (name, age).

The service method takes its parameter types from the procedure. The handler returns the service result directly, which avoids implicit any errors from self-reference.

src/modules/user/user-controller.ts
import { operation, post, prefix, procedure } from 'vovk'; import { z } from 'zod'; import UserService from './user-service'; @prefix('users') export default class UserController { @operation({ summary: 'Update user', description: 'Update user by ID', }) @post('{id}') static updateUser = procedure({ body: z .object({ email: z.email().meta({ description: 'User email', examples: ['john@example.com', 'jane@example.com'], }), profile: z .object({ name: z .string() .min(2) .meta({ description: 'User full name', examples: ['John Doe', 'Jane Smith'], }), age: z .int() .min(16) .max(120) .meta({ description: 'User age', examples: [25, 30] }), }) .meta({ description: 'User profile object' }), }) .meta({ description: 'User data object' }), params: z .object({ id: z.uuid().meta({ description: 'User ID', examples: ['123e4567-e89b-12d3-a456-426614174000'], }), }) .meta({ description: 'Path parameters', }), query: z .object({ notify: z .enum(['email', 'push', 'none']) .meta({ description: 'Notification type' }), }) .meta({ description: 'Query parameters', }), output: z .object({ success: z.boolean().meta({ description: 'Success status' }), id: z.uuid().meta({ description: 'User ID' }), notify: z.enum(['email', 'push', 'none']).meta({ description: 'Notification type', }), }) .meta({ description: 'Response object' }), }).handle(async (req, { id }) => { const body = await req.vovk.body(); const { notify } = req.vovk.query(); return UserService.updateUser(id, body, notify); }); }

The code above is fetched from GitHub repository. 

StreamController and StreamService

/api/streams/tokens streams tokens. Its procedure is a generator that delegates to the service with yield*. The iteration schema validates each streamed item. setTimeout adds a delay between tokens.

src/modules/stream/stream-controller.ts
import { get, operation, prefix, procedure } from 'vovk'; import { z } from 'zod'; import StreamService from './stream-service'; @prefix('streams') export default class StreamController { @operation({ summary: 'Stream tokens', description: 'Stream tokens to the client', }) @get('tokens') static streamTokens = procedure({ validateEachIteration: true, iteration: z .object({ message: z.string().meta({ description: 'Message from the token' }), }) .meta({ description: 'Streamed token object', }), }).handle(async function* () { yield* StreamService.streamTokens(); }); }

The code above is fetched from GitHub repository. 

React Components

The demo uses @tanstack/react-query  for both standard requests and streaming.

src/components/demo/index.tsx
'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import StreamDemo from './stream-demo'; import UserFormDemo from './user-form-demo'; const queryClient = new QueryClient(); const Demo = () => { return ( <QueryClientProvider client={queryClient}> <StreamDemo /> <h2 className="text-lg font-bold mb-1 text-center"> &quot;Update User&quot; Demo </h2> <p className="text-xs mb-4 text-center"> <strong>*</strong> form validation isn&apos;t enabled for demo purposes </p> <UserFormDemo /> </QueryClientProvider> ); }; export default Demo;

The code above is fetched from GitHub repository. 

Config

The app is configured to:

  • Validate on the client with Ajv, the main client-side validation library, described in the customization article. The bundle has no validation, to stay small.
  • Generate Rust and Python code when you run vovk dev or vovk generate.
  • Show the segmented client, which has RPC modules per segment.
  • Generate the clients and the bundle with a package field and an explicit origin:
    • Python and Rust clients: https://hello-world.vovk.dev.
    • TypeScript bundle: https://hello-world.vovk.dev.
    • Composed TypeScript client:
      • In development: http://localhost:PORT, so Node.js code can call the local server through the generated @/client.
      • In production: an empty origin, so requests go to the current origin.
    • Segmented client: an empty origin, so requests go to the current origin.
vovk.config.js
// @ts-check const PROD_ORIGIN = 'https://hello-world.vovk.dev'; // Commented lines indicate default values /** @type {import('vovk').VovkConfig} */ const config = { logLevel: 'debug', outputConfig: { imports: { validateOnClient: 'vovk-ajv', }, openAPIObject: { info: { title: '"Hello World" app API', description: 'API for "Hello World" app hosted at https://hello-world.vovk.dev/. Source code is available on Github https://github.com/finom/vovk/tree/main/examples/hello-world. For more information about this app, visit the documentation page https://vovk.dev/hello-world.', license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT', }, version: '1.0.0', }, servers: [ { url: 'https://hello-world.vovk.dev', description: 'Production', }, { url: 'http://localhost:3000', description: 'Localhost', }, ], }, }, composedClient: { fromTemplates: ['ts', 'py', 'rs'], // enabled: true, // outDir: "./src/client", outputConfig: { origin: process.env.NODE_ENV === 'production' ? null : `http://localhost:${process.env.PORT ?? 3000}`, }, // prettifyClient: true, }, segmentedClient: { // fromTemplates: ["ts"], enabled: true, // outDir: "./src/client", // outputConfig: { origin: '' }, // prettifyClient: true, }, bundle: { outputConfig: { origin: PROD_ORIGIN, imports: { validateOnClient: null }, package: { type: 'module', main: './index.js', types: './index.d.ts', exports: { '.': { default: './index.js', types: './index.d.ts', }, }, }, }, keepPrebundleDir: true, build: async ({ entry, outDir }) => { const { build } = await import('tsdown'); // the code and the types build in two passes: one pass writes an extra chunk.js for (const dts of [false, { emitDtsOnly: true }]) { await build({ entry, dts, format: 'esm', hash: false, fixedExtension: true, clean: !dts, outDir, platform: 'neutral', outExtensions: () => ({ js: '.js', dts: '.d.ts' }), outputOptions: { codeSplitting: false, }, inputOptions: { resolve: { mainFields: ['module', 'main'], }, }, deps: { alwaysBundle: ['!next/**'] }, }); } }, }, clientTemplateDefs: { py: { extends: 'py', outputConfig: { origin: PROD_ORIGIN }, // composedClient: { outDir: "./dist_python" }, }, rs: { extends: 'rs', outputConfig: { origin: PROD_ORIGIN }, // composedClient: { outDir: "./dist_rust" }, }, }, }; module.exports = config;

The code above is fetched from GitHub repository. 

OpenAPI Specification

A GET endpoint serves the OpenAPI specification . It returns the generated spec, openapi from @/client/openapi.

src/modules/static/openapi/openapi-controller.ts
import { get, operation } from 'vovk'; import { openapi } from '@/client/openapi'; export default class OpenApiController { @operation({ summary: 'OpenAPI spec', description: 'Get the OpenAPI spec for the "Hello World" app API', }) @get('openapi.json', { cors: true }) static getSpec = () => openapi; }

The code above is fetched from GitHub repository. 

The spec includes code samples that Scalar shows, ready to copy.

Building and Packaging

The example also builds the packages it publishes on npm , PyPI  and crates.io . The templates write each package with the files its language needs, such as package.json , Cargo.toml  and pyproject.toml , and a README whose code samples document the API and the client.

npm run patch:

  1. Verifies a clean working tree.
  2. Bumps the patch version.
  3. Triggers postversion to regenerate clients, bundle TypeScript, create package files and README files, publish all packages, and create a commit + tag.

vovk generate writes the Python and Rust packages to dist_python and dist_rust, because composedClient.fromTemplates lists py and rs. vovk bundle builds the npm package into dist. Each package has its own publish script:

"scripts": { // ... "publish:node": "npm publish ./dist", "publish:rust": "cargo publish --manifest-path dist_rust/Cargo.toml --allow-dirty", "publish:python": "python3 -m build ./dist_python --wheel --sdist && python3 -m twine upload ./dist_python/dist/*", "git-tag": "git add . && git commit -m \"chore: release v$(node -p \"require('./package.json').version\")\" && git tag vovk-hello-world-v$(node -p \"require('./package.json').version\")", "check-uncommitted": "git diff --quiet && git diff --cached --quiet || (echo '❌ Uncommitted changes!' && exit 1)", "postversion": "vovk generate && vovk bundle && npm run publish:node && npm run publish:rust && npm run publish:python && npm run git-tag", "patch": "npm run check-uncommitted && npm version patch --no-git-tag-version --no-workspaces-update" }

vovk generate and vovk bundle rewrite the READMEs on each run: TypeScript , Rust  and Python .

The generated package files:

dist/package.json
{ "name": "vovk-hello-world", "version": "0.0.89", "description": "A \"Hello World!\" app built with Next.js, Vovk.ts and Zod. For details, visit https://vovk.dev/hello-world", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/finom/vovk.git", "directory": "examples/hello-world" }, "homepage": "https://vovk.dev/hello-world", "bugs": { "url": "https://github.com/finom/vovk/issues" }, "author": "Andrey Gubanov", "keywords": [ "vovk", "openapi", "zod", "api" ], "type": "module", "main": "./index.js", "types": "./index.d.ts", "exports": { ".": { "default": "./index.js", "types": "./index.d.ts" } } }

The code above is fetched from GitHub repository. 

Tests

Tests in test/node , test/rust  and test/python  cover both the local and the published clients.

npm run test

The command builds the Next.js app, starts the server and runs the tests.

Last updated on