“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.
For a larger reference app that AI agents operate (Realtime UI, MCP, voice and chat), see the Realtime UI overview.
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:
UserControllerwith anupdateUserprocedure (POST/api/users/{id}).StreamControllerwith a JSON Lines procedure,streamTokens(GET/api/streams/tokens).OpenApiControllerwithgetSpec(GET/api/static/openapi.json), which serves the generated OpenAPI spec. The/openapipage 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 (
infoandservers).
- Client-side validation, the segmented and composed TypeScript clients, Rust and Python clients, an npm bundle, and OpenAPI metadata (
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-worldStart the dev server:
npm run devThen open http://localhost:3000 .
Topics and Concepts Covered
- Zod validation with the procedure function:
body,queryandparamsfor input,outputanditerationfor output, withdescriptionandexamplesfrom Zod meta . - Client-side validation of RPC input, described in the customization article.
- Composed and segmented TypeScript clients.
- JSON Lines streaming.
- Type inference between the service and the controller.
useQueryanduseMutationwithqueryKey.- A TypeScript client bundle, published on npm (see bundlephobia ).
- Rust and Python clients, published on crates.io and PyPI .
- An OpenAPI spec, served from a static segment and rendered with Scalar .
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.
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);
});
}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.
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();
});
}React Components
The demo uses @tanstack/react-query for both standard requests and streaming.
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">
"Update User" Demo
</h2>
<p className="text-xs mb-4 text-center">
<strong>*</strong> form validation isn't enabled for demo purposes
</p>
<UserFormDemo />
</QueryClientProvider>
);
};
export default Demo;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
packagefield and an explicitorigin:- 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.
- In development:
- Segmented client: an empty origin, so requests go to the current origin.
- Python and Rust clients:
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;OpenAPI Specification
A GET endpoint serves the OpenAPI specification . It returns the generated spec, openapi from @/client/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 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:
- Verifies a clean working tree.
- Bumps the patch version.
- Triggers
postversionto 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:
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"
}
}
}Tests
Tests in test/node , test/rust and test/python cover both the local and the published clients.
npm run testThe command builds the Next.js app, starts the server and runs the tests.