Skip to Content
"Hello World!" Example

“Hello World” Example

📘 About this example: Although it is called “Hello World,” this sample is intentionally comprehensive. It brings together validation, streaming, multi‑language client generation, and OpenAPI output in a single, focused project. If you need the absolute minimal starting point, see the Quick Start guide first.

The “Hello World” app at hello-world.vovk.dev  is a Next.js / Vovk.ts example showcasing core features by implementing:

  • Back end:
    • UserController with an updateUser method (POST /api/users/{id}).
    • StreamController with a JSONLines streamTokens handler (GET /api/streams/tokens).
    • OpenApiController with getSpec (GET /api/static/openapi/spec.json), serving the generated OpenAPI spec. Documentation is viewable at the /openapi page.
  • Front end:
    • A form plus a JSONLines streaming demo above it.
  • Configuration:
    • Client‑side validation; segmented & composed TypeScript clients; generation of Rust/Python clients; npm bundle output; and OpenAPI metadata (info + servers).

Explore the source in the GitHub repository . Generated artifacts are committed under dist , tmp_prebundle , dist_rust , and dist_python  for inspection (in real projects they should go in .gitignore).

All snippets on this page are pulled directly from GitHub; live example pages are embedded via iframes.

Running the Example Locally

Clone and install:

git clone https://github.com/finom/vovk-hello-world.git cd vovk-hello-world npm i

Start the dev server:

npm run dev

Then open http://localhost:3000 .

Topics and Concepts Covered

Live Demo

The demo provides a simple form (no native validation attributes) and a “Disable client-side input validation” checkbox toggling the disableClientValidation option. “Notification type” is intentionally mis-set to show both client and server validation behavior.

UserController and UserService

/api/users/{id} is implemented by UserController / UserService through updateUser.

The controller method uses @post to map POST and withZod for Zod validation of body, params, query and output. Each schema applies meta for richer OpenAPI (description, examples). For illustration, body nests email and a profile object (name, age).

The service method infers parameter types from the controller method, returning the service method result directly and avoiding implicit any self-reference issues.

src/modules/user/UserController.ts
import { z } from "zod"; import { prefix, post, operation } from "vovk"; import { withZod } from "vovk-zod"; import UserService from "./UserService"; @prefix("users") export default class UserController { @operation({ summary: "Update user", description: "Update user by ID", }) @post("{id}") static updateUser = withZod({ 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" }), async handle(req, { id }) { const body = await req.json(); const notify = req.nextUrl.searchParams.get("notify"); return UserService.updateUser(id, body, notify); }, }); }

The code above is fetched from GitHub repository. 

StreamController and StreamService

/api/streams/tokens streams tokens using a controller generator method that delegates with yield* to the service. Each streamed item is validated via the iteration schema. Delays are simulated with setTimeout.

src/modules/stream/StreamController.ts
import { prefix, get, operation } from "vovk"; import { withZod } from "vovk-zod"; import { z } from "zod"; import StreamService from "./StreamService"; @prefix("streams") export default class StreamController { @operation({ summary: "Stream tokens", description: "Stream tokens to the client", }) @get("tokens") static streamTokens = withZod({ iteration: z .object({ message: z.string().meta({ description: "Message from the token" }), }) .meta({ description: "Streamed token object", }), async *handle() { 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 "./StreamDemo"; import UserFormDemo from "./UserFormDemo"; 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:

  • Client-side validation via Ajv.
  • Generate Rust and Python code when running vovk dev or vovk generate.
  • Demonstrate the segmented client, generating RPC modules per segment.
  • Generate clients and bundle with an explicit origin:
    • Python and Rust clients: use https://hello-world.vovk.dev.
    • TypeScript Bundle: use https://hello-world.vovk.dev.
    • Composed TypeScript client:
      • In development: use http://localhost:PORT (so Node.js via vovk-client can call locally).
      • In production: use an empty origin (requests are relative to the current origin).
    • Segmented client: use an empty origin (requests are relative 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-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: ["mjs", "cjs", "py", "rs"], // enabled: true, // outDir: "./node_modules/.vovk-client", outputConfig: { origin: process.env.NODE_ENV === "production" ? null : `http://localhost:${process.env.PORT ?? 3000}`, }, // prettifyClient: false, }, segmentedClient: { // fromTemplates: ["ts"], enabled: true, // outDir: "./src/client", // outputConfig: { origin: '' }, // prettifyClient: true, }, bundle: { outputConfig: { origin: PROD_ORIGIN }, keepPrebundleDir: true, build: async ({ entry, outDir }) => { const { build } = await import("tsdown"); await build({ entry, dts: true, format: ["cjs", "esm"], hash: false, fixedExtension: true, clean: true, outDir, tsconfig: "./tsconfig.bundle.json", }); }, }, 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

The OpenAPI specification  is served by a GET endpoint returning the generated spec (openapi from vovk-client/openapi).

src/modules/static/openapi/OpenApiController.ts
import { get, operation } from "vovk"; import { openapi } from "vovk-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 Scalar‑compatible samples you can reuse immediately.

Building and Packaging

This example also demonstrates how to quickly produce distributable packages published on npm , PyPI , and crates.io . The provided templates compile ready-to-use packages with language-specific files such as package.json , Cargo.toml , and pyproject.toml , as well as README files that use code samples served as API/client documentation.

npm run patch:

  1. Verifies a clean working tree.
  2. Bumps the patch version.
  3. Triggers postversion to regenerate clients, bundle TypeScript, publish all packages, and create a commit + tag.
"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 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" }

The README.md files are also updated with the latest examples and descriptions. IFrames below are rendered from the READMEs by Github Pages.

Node.js, Rust, and Python CLI Demo

The repository includes CLI demos for invoking RPC methods from Node.js, Rust, and Python. Each language has two variants:

  • local: uses locally generated clients.
    • Rust / Python still target https://hello-world.vovk.dev for simplicity.
    • Node.js hits http://localhost:PORT (run npm run dev separately).
  • packaged: uses published registries; all call https://hello-world.vovk.dev.

Each demo invokes updateUser, streamTokens, and getSpec.

demo/node/packaged.mts
import { UserRPC, OpenApiRPC, StreamRPC } from "vovk-hello-world"; async function main() { console.log("\n--- Node.js Demo (Packaged) ---"); const updateUserResponse = await UserRPC.updateUser({ params: { id: "123e4567-e89b-12d3-a456-426614174000", }, body: { email: "john@example.com", profile: { name: "John Doe", age: 25, }, }, query: { notify: "email", }, }); console.log("UserRPC.updateUser response:", updateUserResponse); const openapiResponse = await OpenApiRPC.getSpec(); console.log( `OpenApiRPC.getSpec response: ${openapiResponse.info.title} ${openapiResponse.info.version}`, ); const streamResponse = await StreamRPC.streamTokens(); console.log(`streamTokens:`); for await (const item of streamResponse) { process.stdout.write(item.message); } process.stdout.write("\n"); } main().catch(console.error);

The code above is fetched from GitHub repository. 

Local demos differ only by import paths (Node.js: vovk-client; Python: minor import workarounds; Rust: dependency path in Cargo.toml).

Conclusion

Even with only three endpoints (updateUser, streamTokens, getSpec), Vovk.ts delivers:

  • Turnkey multi-language client generation (TypeScript bundle + Rust + Python) with publish-ready metadata and READMEs.
  • Rich, documented OpenAPI output rendered via Scalar.
  • Text streaming and unified client-side validation flows.

Additional topics not shown here include Function Calling (LLM tool generation), source‑only templates, custom decorators, advanced imports configuration, and more—explore the docs for deeper features.

Last updated on