Skip to Content
"Hello World!" Example

“Hello World” showcase

The “Hello World” app is a simple example that showcases some of the core features of Vovk.ts. More specifically, it demonstrates:

  • An updateUser method served as POST to /api/users/{id} endpoint.
  • A JSONLines handler streamTokens served as GET to /api/streams/tokens.
  • An OpenAPI specification that describes the API endpoints powered by Scalar .
  • Building and packaging the app with Vovk CLI and external tools.

The project can be explored at the GitHub repository  and viewed live at vovk-hello-world.vercel.app . Bundled and generated files are available in the Git index as dist , dist_rust  and dist_python , so you can explore (usually they should be .gitignore’d). All code snippets on this page directly built from the source code of the app and demos are served within an iframe.

Live Demo

The demo below implements a simple form without validation attributes with “Disable client-side input validation” checkbox that represents disableClientValidation option.

UserController and UserService

The /api/users/{id} endpoint is implemented by the UserController and UserService classes that provide updateUser method.

The controller updateUser method is implemented with @post decorator that maps the method to the POST HTTP verb and withZod function that applies Zod validation to the request input (body, params, and query). The input Zod models use meta method to provide additional metadata for the OpenAPI specification. For deminstrational purposes, the body input model implements nested validation of email and profile fields, where profile is an object with name and age properties.

The service updateUser method infers the input type from the controller method that in its turn returns the result of the service’s updateUser method call, demonstrating lack of self-reference errors (implicit “any”). In other words, the controller’s input type is being a single source of truth for the service’s input type, and the service’s output type is inferred from the controller’s output type.

UserController.ts
UserService.ts
1import { z } from "zod";
2import { prefix, post, operation } from "vovk";
3import { withZod } from "vovk-zod";
4import UserService from "./UserService";
5
6@prefix("users")
7export default class UserController {
8 @operation({
9 summary: "Update user",
10 description: "Update user by ID",
11 })
12 @post("{id}")
13 static updateUser = withZod({
14 body: z
15 .object({
16 email: z.email().meta({
17 description: "User email",
18 examples: ["john@example.com", "jane@example.com"],
19 }),
20 profile: z
21 .object({
22 name: z.string().meta({
23 description: "User full name",
24 examples: ["John Doe", "Jane Smith"],
25 }),
26 age: z
27 .int()
28 .min(16)
29 .max(120)
30 .meta({ description: "User age", examples: [25, 30] }),
31 })
32 .meta({ description: "User profile object" }),
33 })
34 .meta({ description: "User data object" }),
35 params: z
36 .object({
37 id: z.uuid().meta({
38 description: "User ID",
39 examples: ["123e4567-e89b-12d3-a456-426614174000"],
40 }),
41 })
42 .meta({
43 description: "Path parameters",
44 }),
45 query: z
46 .object({
47 notify: z
48 .enum(["email", "push", "none"])
49 .meta({ description: "Notification type" }),
50 })
51 .meta({
52 description: "Query parameters",
53 }),
54 output: z
55 .object({
56 success: z.boolean().meta({ description: "Success status" }),
57 })
58 .meta({ description: "Response object" }),
59 async handle(req, { id }) {
60 const body = await req.json();
61 const notify = req.nextUrl.searchParams.get("notify");
62
63 return UserService.updateUser(id, body, notify);
64 },
65 });
66}
1import { z } from "zod";
2import { prefix, post, operation } from "vovk";
3import { withZod } from "vovk-zod";
4import UserService from "./UserService";
5
6@prefix("users")
7export default class UserController {
8 @operation({
9 summary: "Update user",
10 description: "Update user by ID",
11 })
12 @post("{id}")
13 static updateUser = withZod({
14 body: z
15 .object({
16 email: z.email().meta({
17 description: "User email",
18 examples: ["john@example.com", "jane@example.com"],
19 }),
20 profile: z
21 .object({
22 name: z.string().meta({
23 description: "User full name",
24 examples: ["John Doe", "Jane Smith"],
25 }),
26 age: z
27 .int()
28 .min(16)
29 .max(120)
30 .meta({ description: "User age", examples: [25, 30] }),
31 })
32 .meta({ description: "User profile object" }),
33 })
34 .meta({ description: "User data object" }),
35 params: z
36 .object({
37 id: z.uuid().meta({
38 description: "User ID",
39 examples: ["123e4567-e89b-12d3-a456-426614174000"],
40 }),
41 })
42 .meta({
43 description: "Path parameters",
44 }),
45 query: z
46 .object({
47 notify: z
48 .enum(["email", "push", "none"])
49 .meta({ description: "Notification type" }),
50 })
51 .meta({
52 description: "Query parameters",
53 }),
54 output: z
55 .object({
56 success: z.boolean().meta({ description: "Success status" }),
57 })
58 .meta({ description: "Response object" }),
59 async handle(req, { id }) {
60 const body = await req.json();
61 const notify = req.nextUrl.searchParams.get("notify");
62
63 return UserService.updateUser(id, body, notify);
64 },
65 });
66}

StreamController and StreamService

The /api/streams/tokens endpoint is implemented by the StreamController and StreamService classes that provide streamTokens method.

The controller streamTokens method is implemented with @get decorator that maps the method to the GET HTTP verb and withZod function that validates iterations of JSONLines responses. It delegates iterable execution with yield* syntax to the service’s streamTokens method, which is a generator function that yields tokens one by one.

The service streamTokens method is a generator function that yields tokens one by one, simulating a stream of data. It uses setTimeout to simulate a delay between token emissions, demonstrating how to handle streaming responses in Vovk.ts.

StreamController.ts
StreamService.ts
1import { prefix, get, operation } from "vovk";
2import { withZod } from "vovk-zod";
3import { z } from "zod/v4";
4import StreamService from "./StreamService";
5
6@prefix("streams")
7export default class StreamController {
8 @operation({
9 summary: "Stream tokens",
10 description: "Stream tokens to the client",
11 })
12 @get("tokens")
13 static streamTokens = withZod({
14 iteration: z
15 .object({
16 message: z.string().meta({ description: "Message from the token" }),
17 })
18 .meta({
19 description: "Streamed token object",
20 }),
21 async *handle() {
22 yield* StreamService.streamTokens();
23 },
24 });
25}
1import { prefix, get, operation } from "vovk";
2import { withZod } from "vovk-zod";
3import { z } from "zod/v4";
4import StreamService from "./StreamService";
5
6@prefix("streams")
7export default class StreamController {
8 @operation({
9 summary: "Stream tokens",
10 description: "Stream tokens to the client",
11 })
12 @get("tokens")
13 static streamTokens = withZod({
14 iteration: z
15 .object({
16 message: z.string().meta({ description: "Message from the token" }),
17 })
18 .meta({
19 description: "Streamed token object",
20 }),
21 async *handle() {
22 yield* StreamService.streamTokens();
23 },
24 });
25}

React Components

The components in the demo use @tanstack/react-query  for data fetching.

index.tsx
UserFormDemo.tsx
StreamDemo.tsx
1"use client";
2import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3import StreamDemo from "./StreamDemo";
4import UserFormDemo from "./UserFormDemo";
5
6const queryClient = new QueryClient();
7
8const Demo = () => {
9 return (
10 <QueryClientProvider client={queryClient}>
11 <StreamDemo />
12 <h2 className="text-lg font-bold mb-1 text-center">
13 &quot;Update user&quot; demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn&apos;t enabled for demo purposes
17 </p>
18 <UserFormDemo />
19 </QueryClientProvider>
20 );
21};
22
23export default Demo;
1"use client";
2import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3import StreamDemo from "./StreamDemo";
4import UserFormDemo from "./UserFormDemo";
5
6const queryClient = new QueryClient();
7
8const Demo = () => {
9 return (
10 <QueryClientProvider client={queryClient}>
11 <StreamDemo />
12 <h2 className="text-lg font-bold mb-1 text-center">
13 &quot;Update user&quot; demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn&apos;t enabled for demo purposes
17 </p>
18 <UserFormDemo />
19 </QueryClientProvider>
20 );
21};
22
23export default Demo;

Config

The app is configured to:

  • Handle client-side validation with Ajv.
  • Generate Rust and Python code automatically when vovk dev command is run.
  • Demonstrate segmented client, so the RPC modules are generated in separate files for each segment.
  • Bundle with proper origin.

If you use default config settings, you can generate the client for other languages with:

npx vovk generate --from rs --from py --origin https://vovk-hello-world.vercel.app

And the bundle with:

npx vovk bundle --origin https://vovk-hello-world.vercel.app
vovk.config.js
1// @ts-check
2
3const PROD_URL = "https://vovk-hello-world.vercel.app";
4// Commented lines indicate default values
5/** @type {import('vovk').VovkConfig} */
6const config = {
7 generatorConfig: {
8 origin: 'http://localhost:3000',
9 imports: {
10 validateOnClient: "vovk-ajv",
11 },
12 openAPIObject: {
13 info: {
14 title: '"Hello World" app API',
15 description:
16 'API for "Hello World" app hosted at https://vovk-hello-world.vercel.app/. Source code is available on Github https://github.com/finom/vovk-hello-world.',
17 license: {
18 name: "MIT",
19 url: "https://opensource.org/licenses/MIT",
20 },
21 version: "1.0.0",
22 },
23 }
24 },
25
26 composedClient: {
27 fromTemplates: ["mjs", "cjs", "py", "rs"],
28 // enabled: true,
29 // outDir: "./node_modules/.vovk-client",
30 },
31 /* bundle: {
32 outDir: "./dist",
33 }, */
34 segmentedClient: {
35 // fromTemplates: ["mjs", "cjs"],
36 enabled: true,
37 // outDir: "./src/client",
38 },
39 bundle: {
40 generatorConfig: { origin: PROD_URL },
41 },
42 clientTemplateDefs: {
43 py: {
44 extends: "py",
45 generatorConfig: { origin: PROD_URL },
46 composedClient: {
47 // outDir: "./dist_python",
48 },
49 },
50 rs: {
51 extends: "rs",
52 generatorConfig: { origin: PROD_URL },
53 composedClient: {
54 // outDir: "./dist_rust",
55 },
56 },
57 },
58 moduleTemplates: {
59 controller: "vovk-zod/module-templates/controller.ts.ejs",
60 service: "vovk-cli/module-templates/service.ts.ejs",
61 },
62};
63module.exports = config;
1// @ts-check
2
3const PROD_URL = "https://vovk-hello-world.vercel.app";
4// Commented lines indicate default values
5/** @type {import('vovk').VovkConfig} */
6const config = {
7 generatorConfig: {
8 origin: 'http://localhost:3000',
9 imports: {
10 validateOnClient: "vovk-ajv",
11 },
12 openAPIObject: {
13 info: {
14 title: '"Hello World" app API',
15 description:
16 'API for "Hello World" app hosted at https://vovk-hello-world.vercel.app/. Source code is available on Github https://github.com/finom/vovk-hello-world.',
17 license: {
18 name: "MIT",
19 url: "https://opensource.org/licenses/MIT",
20 },
21 version: "1.0.0",
22 },
23 }
24 },
25
26 composedClient: {
27 fromTemplates: ["mjs", "cjs", "py", "rs"],
28 // enabled: true,
29 // outDir: "./node_modules/.vovk-client",
30 },
31 /* bundle: {
32 outDir: "./dist",
33 }, */
34 segmentedClient: {
35 // fromTemplates: ["mjs", "cjs"],
36 enabled: true,
37 // outDir: "./src/client",
38 },
39 bundle: {
40 generatorConfig: { origin: PROD_URL },
41 },
42 clientTemplateDefs: {
43 py: {
44 extends: "py",
45 generatorConfig: { origin: PROD_URL },
46 composedClient: {
47 // outDir: "./dist_python",
48 },
49 },
50 rs: {
51 extends: "rs",
52 generatorConfig: { origin: PROD_URL },
53 composedClient: {
54 // outDir: "./dist_rust",
55 },
56 },
57 },
58 moduleTemplates: {
59 controller: "vovk-zod/module-templates/controller.ts.ejs",
60 service: "vovk-cli/module-templates/service.ts.ejs",
61 },
62};
63module.exports = config;

Building and Packaging

The example also demonstrates the ability to quickly create a distributable package, published on npm , PyPI  and crates.io . The templates offered in the framework compile a ready-to-use packages with corresponding files for each language, such as package.json , Cargo.toml , and pyproject.toml , but also the README.md files that follow the idea of documenting the API as RPC method calls with comments as method descriptions and examples at the same time.

On the root of the “hello world” project in the package.json file, you can find the scripts section with the following self-explanatory commands:

"scripts": { // ... "postversion:ts": "npm run bundle && npm publish ./dist", "postversion:rs": "cargo publish --manifest-path dist_rust/Cargo.toml --allow-dirty", "postversion:py": "python3 -m build ./dist_python --wheel --sdist && python3 -m twine upload ./dist_python/dist/*", "postversion": "vovk generate && npm run postversion:ts && npm run postversion:rs && npm run postversion:py && git push && git push --tags", "patch": "npm version patch" }

And once you run npm version patch, it will automatically generate packages for TypeScript, Rust, and Python, and publish the packages to npm, crates.io, and PyPI. The README.md files are also updated with the latest examples and descriptions.

The examples below are rendered from the READMEs on Github Pages.

Node.js, Rust and Python Demo

The app also includes a demo of how the RPC methods can be run in Node.js, Rust, and Python. Each demo showcases how to use the generated client to call the updateUser, streamTokens, getSpec (for static OpenAPI RPC) but also call the getSpec method of the published API.

The demo can be seen by running npm run dev in one terminal instance and npm run demo in another terminal instance.

The demo scripts are self-explanatory and can be found in the package.json file:

"scripts": { // ... "demo": "npm run demo:ts && npm run demo:rs && npm run demo:py", "demo:ts": "node --experimental-strip-types demo_node/index.mts", "demo:rs": "cargo run --manifest-path ./demo_rust/Cargo.toml --quiet", "demo:py": "python3 -m pip install --upgrade vovk_hello_world --quiet && python3 demo_python.py", }
index.mts
demo_python.py
main.rs
1import { UserRPC, OpenApiRPC, StreamRPC } from "vovk-client"; // composed client
2import { OpenApiRPC as OpenApiRPCFromBundle } from "vovk-hello-world"; // bundle
3
4async function main() {
5 console.log("\n--- Node.js Demo ---");
6 const updateUserResponse = await UserRPC.updateUser({
7 params: {
8 id: "123e4567-e89b-12d3-a456-426614174000",
9 },
10 body: {
11 email: "john@example.com",
12 profile: {
13 name: "John Doe",
14 age: 25,
15 },
16 },
17 query: {
18 notify: "email",
19 },
20 });
21
22 console.log("UserRPC.updateUser:", updateUserResponse);
23
24 const openapiResponse = await OpenApiRPC.getSpec();
25 console.log(
26 `OpenApiRPC.getSpec from the local module: ${openapiResponse.info.title} ${openapiResponse.info.version}`,
27 );
28
29 const streamResponse = await StreamRPC.streamTokens();
30 console.log(`streamTokens:`);
31 for await (const item of streamResponse) {
32 process.stdout.write(item.message);
33 }
34 process.stdout.write("\n");
35
36 const openapiResponseFromBundle = await OpenApiRPCFromBundle.getSpec();
37 console.log(
38 `OpenApiRPC.getSpec from "vovk-hello-world" package: ${openapiResponseFromBundle.info.title} ${openapiResponseFromBundle.info.version}`,
39 );
40}
41main().catch(console.error);
1import { UserRPC, OpenApiRPC, StreamRPC } from "vovk-client"; // composed client
2import { OpenApiRPC as OpenApiRPCFromBundle } from "vovk-hello-world"; // bundle
3
4async function main() {
5 console.log("\n--- Node.js Demo ---");
6 const updateUserResponse = await UserRPC.updateUser({
7 params: {
8 id: "123e4567-e89b-12d3-a456-426614174000",
9 },
10 body: {
11 email: "john@example.com",
12 profile: {
13 name: "John Doe",
14 age: 25,
15 },
16 },
17 query: {
18 notify: "email",
19 },
20 });
21
22 console.log("UserRPC.updateUser:", updateUserResponse);
23
24 const openapiResponse = await OpenApiRPC.getSpec();
25 console.log(
26 `OpenApiRPC.getSpec from the local module: ${openapiResponse.info.title} ${openapiResponse.info.version}`,
27 );
28
29 const streamResponse = await StreamRPC.streamTokens();
30 console.log(`streamTokens:`);
31 for await (const item of streamResponse) {
32 process.stdout.write(item.message);
33 }
34 process.stdout.write("\n");
35
36 const openapiResponseFromBundle = await OpenApiRPCFromBundle.getSpec();
37 console.log(
38 `OpenApiRPC.getSpec from "vovk-hello-world" package: ${openapiResponseFromBundle.info.title} ${openapiResponseFromBundle.info.version}`,
39 );
40}
41main().catch(console.error);

OpenAPI specification

OpenAPI specification  requires creating another GET endpoint that uses vovkSchemaToOpenAPI function to generate the OpenAPI spec from the Vovk.ts schema.

OpenApiController.ts
1import { prefix, get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4@prefix("openapi")
5export default class OpenApiController {
6 @operation({
7 summary: "OpenAPI spec",
8 description: 'Get the OpenAPI spec for the "Hello World" app API',
9 })
10 @get("spec.json", { cors: true })
11 static getSpec = () => openapi;
12}
1import { prefix, get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4@prefix("openapi")
5export default class OpenApiController {
6 @operation({
7 summary: "OpenAPI spec",
8 description: 'Get the OpenAPI spec for the "Hello World" app API',
9 })
10 @get("spec.json", { cors: true })
11 static getSpec = () => openapi;
12}

The generated specification also includes the Scalar  compatible examples that can be copied and pasted to your project stright away.

Conclusion

Besides being a back-end framework for Next.js, Vovk.ts offers extra features that ususally require extra skills and time to implement, such as:

  • Beautifully documented OpenAPI specification powered by Scalar .
  • RPC library that has the same shape as controllers, inferring types from the controller methods.
  • Client-side validation with Ajv for immediate feedback.
  • JSON streaming implementation with JSONLines.
  • Code generation and bundling to quickly publish the documented RESTful API library.
Last updated on