“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 asPOST
to/api/users/{id}
endpoint. - A JSONLines handler
streamTokens
served asGET
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.
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}
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}
1import type { VovkBody, VovkOutput, VovkParams, VovkQuery } from "vovk";
2import type UserController from "./UserController";
3
4export default class UserService {
5 static updateUser = (
6 id: VovkParams<typeof UserController.updateUser>["id"],
7 body: VovkBody<typeof UserController.updateUser>,
8 notify: VovkQuery<typeof UserController.updateUser>["notify"],
9 ) => {
10 console.log(
11 id satisfies string,
12 body satisfies { email: string; profile: { name: string; age: number } },
13 notify satisfies "email" | "push" | "none",
14 );
15 return {
16 success: true,
17 } satisfies VovkOutput<typeof UserController.updateUser>;
18 };
19}
1import type { VovkBody, VovkOutput, VovkParams, VovkQuery } from "vovk";
2import type UserController from "./UserController";
3
4export default class UserService {
5 static updateUser = (
6 id: VovkParams<typeof UserController.updateUser>["id"],
7 body: VovkBody<typeof UserController.updateUser>,
8 notify: VovkQuery<typeof UserController.updateUser>["notify"],
9 ) => {
10 console.log(
11 id satisfies string,
12 body satisfies { email: string; profile: { name: string; age: number } },
13 notify satisfies "email" | "push" | "none",
14 );
15 return {
16 success: true,
17 } satisfies VovkOutput<typeof UserController.updateUser>;
18 };
19}
1import type { VovkBody, VovkOutput, VovkParams, VovkQuery } from "vovk";
2import type UserController from "./UserController";
3
4export default class UserService {
5 static updateUser = (
6 id: VovkParams<typeof UserController.updateUser>["id"],
7 body: VovkBody<typeof UserController.updateUser>,
8 notify: VovkQuery<typeof UserController.updateUser>["notify"],
9 ) => {
10 console.log(
11 id satisfies string,
12 body satisfies { email: string; profile: { name: string; age: number } },
13 notify satisfies "email" | "push" | "none",
14 );
15 return {
16 success: true,
17 } satisfies VovkOutput<typeof UserController.updateUser>;
18 };
19}
1import type { VovkBody, VovkOutput, VovkParams, VovkQuery } from "vovk";
2import type UserController from "./UserController";
3
4export default class UserService {
5 static updateUser = (
6 id: VovkParams<typeof UserController.updateUser>["id"],
7 body: VovkBody<typeof UserController.updateUser>,
8 notify: VovkQuery<typeof UserController.updateUser>["notify"],
9 ) => {
10 console.log(
11 id satisfies string,
12 body satisfies { email: string; profile: { name: string; age: number } },
13 notify satisfies "email" | "push" | "none",
14 );
15 return {
16 success: true,
17 } satisfies VovkOutput<typeof UserController.updateUser>;
18 };
19}
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.
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}
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}
1import { VovkIteration } from "vovk";
2import type StreamController from "./StreamController";
3
4export default class StreamService {
5 static async *streamTokens() {
6 const tokens: VovkIteration<typeof StreamController.streamTokens>[] =
7 "Vovk.ts is a RESTful back-end meta-framework with RPC built on top of Next.js App router, and this text is a JSONLines stream demo."
8 .match(/[^\s.-]+|\s+|\.|-/g)
9 ?.map((message) => ({ message })) || [];
10
11 for (const token of tokens) {
12 yield token;
13 await new Promise((resolve) => setTimeout(resolve, 100));
14 }
15 }
16}
1import { VovkIteration } from "vovk";
2import type StreamController from "./StreamController";
3
4export default class StreamService {
5 static async *streamTokens() {
6 const tokens: VovkIteration<typeof StreamController.streamTokens>[] =
7 "Vovk.ts is a RESTful back-end meta-framework with RPC built on top of Next.js App router, and this text is a JSONLines stream demo."
8 .match(/[^\s.-]+|\s+|\.|-/g)
9 ?.map((message) => ({ message })) || [];
10
11 for (const token of tokens) {
12 yield token;
13 await new Promise((resolve) => setTimeout(resolve, 100));
14 }
15 }
16}
1import { VovkIteration } from "vovk";
2import type StreamController from "./StreamController";
3
4export default class StreamService {
5 static async *streamTokens() {
6 const tokens: VovkIteration<typeof StreamController.streamTokens>[] =
7 "Vovk.ts is a RESTful back-end meta-framework with RPC built on top of Next.js App router, and this text is a JSONLines stream demo."
8 .match(/[^\s.-]+|\s+|\.|-/g)
9 ?.map((message) => ({ message })) || [];
10
11 for (const token of tokens) {
12 yield token;
13 await new Promise((resolve) => setTimeout(resolve, 100));
14 }
15 }
16}
1import { VovkIteration } from "vovk";
2import type StreamController from "./StreamController";
3
4export default class StreamService {
5 static async *streamTokens() {
6 const tokens: VovkIteration<typeof StreamController.streamTokens>[] =
7 "Vovk.ts is a RESTful back-end meta-framework with RPC built on top of Next.js App router, and this text is a JSONLines stream demo."
8 .match(/[^\s.-]+|\s+|\.|-/g)
9 ?.map((message) => ({ message })) || [];
10
11 for (const token of tokens) {
12 yield token;
13 await new Promise((resolve) => setTimeout(resolve, 100));
14 }
15 }
16}
React Components
The components in the demo use @tanstack/react-query for data fetching.
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 "Update user" demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn'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 "Update user" demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn'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 "Update user" demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn'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 "Update user" demo
14 </h2>
15 <p className="text-xs mb-4 text-center">
16 <strong>*</strong> form validation isn't enabled for demo purposes
17 </p>
18 <UserFormDemo />
19 </QueryClientProvider>
20 );
21};
22
23export default Demo;
1"use client";
2import React, { useState } from "react";
3import { type VovkQuery } from "vovk";
4import { UserRPC } from "../../client/root"; // segmented client
5import { useMutation } from "@tanstack/react-query";
6
7const UserFormDemo = () => {
8 const [disableClientValidation, setDisableClientValidation] = useState(false);
9 const [name, setName] = useState("John Doe");
10 const [age, setAge] = useState(35);
11 const [email, setEmail] = useState("john@example.com");
12 const [id, setId] = useState("a937629d-e8f6-4b1e-a819-7669358650a0");
13 const updateUserMutation = useMutation({
14 mutationFn: UserRPC.updateUser,
15 });
16 const [notify, setNotify] = useState<
17 VovkQuery<typeof UserRPC.updateUser>["notify"]
18 >(
19 "sms" as "email", // set error value by default
20 );
21
22 const handleSubmit = (e: React.FormEvent) => {
23 e.preventDefault();
24
25 updateUserMutation.mutate({
26 body: {
27 email,
28 profile: {
29 name,
30 age,
31 },
32 },
33 query: { notify },
34 params: { id },
35 disableClientValidation,
36 });
37 };
38 return (
39 <form onSubmit={handleSubmit}>
40 <h3>Body</h3>
41 <div>
42 <label>User email</label>
43 <input
44 name="email"
45 placeholder="john@example.com"
46 value={email}
47 onChange={(e) => setEmail(e.target.value)}
48 />
49 </div>
50 <div>
51 <label>User full name</label>
52 <input
53 name="name"
54 type="text"
55 placeholder="John Doe"
56 value={name}
57 onChange={(e) => setName(e.target.value)}
58 />
59 </div>
60 <div>
61 <label>User age</label>
62 <input
63 name="age"
64 type="number"
65 placeholder="35"
66 value={age}
67 onChange={(e) => setAge(Number(e.target.value))}
68 />
69 </div>
70
71 <h3>Params</h3>
72 <div>
73 <label>User ID</label>
74 <input
75 name="id"
76 type="text"
77 placeholder="123e4567-e89b-12d3-a456-426614174000"
78 value={id}
79 onChange={(e) => setId(e.target.value)}
80 />
81 </div>
82 <h3>Query</h3>
83 <div>
84 <label>Notification type</label>
85 <select
86 name="notify"
87 value={notify}
88 onChange={(e) =>
89 setNotify(e.target.value as "email" | "push" | "none")
90 }
91 >
92 <option value="none">None</option>
93 <option value="email">Email</option>
94 <option value="push">Push</option>
95 <option value="sms">SMS (error)</option>
96 </select>
97 </div>
98 <br />
99 <label>
100 <input
101 type="checkbox"
102 onChange={({ target }) => setDisableClientValidation(target.checked)}
103 checked={disableClientValidation}
104 />{" "}
105 Disable client-side input validation
106 </label>
107 <button type="submit">Submit</button>
108 {(updateUserMutation.data || updateUserMutation.error) && (
109 <output>
110 <strong>Response:</strong>{" "}
111 {updateUserMutation.error ? (
112 <div className="text-red-500">
113 {updateUserMutation.error.message}
114 </div>
115 ) : (
116 <div className="text-green-500">
117 {JSON.stringify(updateUserMutation.data)}
118 </div>
119 )}
120 </output>
121 )}
122 </form>
123 );
124};
125
126export default UserFormDemo;
1"use client";
2import React, { useState } from "react";
3import { type VovkQuery } from "vovk";
4import { UserRPC } from "../../client/root"; // segmented client
5import { useMutation } from "@tanstack/react-query";
6
7const UserFormDemo = () => {
8 const [disableClientValidation, setDisableClientValidation] = useState(false);
9 const [name, setName] = useState("John Doe");
10 const [age, setAge] = useState(35);
11 const [email, setEmail] = useState("john@example.com");
12 const [id, setId] = useState("a937629d-e8f6-4b1e-a819-7669358650a0");
13 const updateUserMutation = useMutation({
14 mutationFn: UserRPC.updateUser,
15 });
16 const [notify, setNotify] = useState<
17 VovkQuery<typeof UserRPC.updateUser>["notify"]
18 >(
19 "sms" as "email", // set error value by default
20 );
21
22 const handleSubmit = (e: React.FormEvent) => {
23 e.preventDefault();
24
25 updateUserMutation.mutate({
26 body: {
27 email,
28 profile: {
29 name,
30 age,
31 },
32 },
33 query: { notify },
34 params: { id },
35 disableClientValidation,
36 });
37 };
38 return (
39 <form onSubmit={handleSubmit}>
40 <h3>Body</h3>
41 <div>
42 <label>User email</label>
43 <input
44 name="email"
45 placeholder="john@example.com"
46 value={email}
47 onChange={(e) => setEmail(e.target.value)}
48 />
49 </div>
50 <div>
51 <label>User full name</label>
52 <input
53 name="name"
54 type="text"
55 placeholder="John Doe"
56 value={name}
57 onChange={(e) => setName(e.target.value)}
58 />
59 </div>
60 <div>
61 <label>User age</label>
62 <input
63 name="age"
64 type="number"
65 placeholder="35"
66 value={age}
67 onChange={(e) => setAge(Number(e.target.value))}
68 />
69 </div>
70
71 <h3>Params</h3>
72 <div>
73 <label>User ID</label>
74 <input
75 name="id"
76 type="text"
77 placeholder="123e4567-e89b-12d3-a456-426614174000"
78 value={id}
79 onChange={(e) => setId(e.target.value)}
80 />
81 </div>
82 <h3>Query</h3>
83 <div>
84 <label>Notification type</label>
85 <select
86 name="notify"
87 value={notify}
88 onChange={(e) =>
89 setNotify(e.target.value as "email" | "push" | "none")
90 }
91 >
92 <option value="none">None</option>
93 <option value="email">Email</option>
94 <option value="push">Push</option>
95 <option value="sms">SMS (error)</option>
96 </select>
97 </div>
98 <br />
99 <label>
100 <input
101 type="checkbox"
102 onChange={({ target }) => setDisableClientValidation(target.checked)}
103 checked={disableClientValidation}
104 />{" "}
105 Disable client-side input validation
106 </label>
107 <button type="submit">Submit</button>
108 {(updateUserMutation.data || updateUserMutation.error) && (
109 <output>
110 <strong>Response:</strong>{" "}
111 {updateUserMutation.error ? (
112 <div className="text-red-500">
113 {updateUserMutation.error.message}
114 </div>
115 ) : (
116 <div className="text-green-500">
117 {JSON.stringify(updateUserMutation.data)}
118 </div>
119 )}
120 </output>
121 )}
122 </form>
123 );
124};
125
126export default UserFormDemo;
1"use client";
2import React, { useState } from "react";
3import { type VovkQuery } from "vovk";
4import { UserRPC } from "../../client/root"; // segmented client
5import { useMutation } from "@tanstack/react-query";
6
7const UserFormDemo = () => {
8 const [disableClientValidation, setDisableClientValidation] = useState(false);
9 const [name, setName] = useState("John Doe");
10 const [age, setAge] = useState(35);
11 const [email, setEmail] = useState("john@example.com");
12 const [id, setId] = useState("a937629d-e8f6-4b1e-a819-7669358650a0");
13 const updateUserMutation = useMutation({
14 mutationFn: UserRPC.updateUser,
15 });
16 const [notify, setNotify] = useState<
17 VovkQuery<typeof UserRPC.updateUser>["notify"]
18 >(
19 "sms" as "email", // set error value by default
20 );
21
22 const handleSubmit = (e: React.FormEvent) => {
23 e.preventDefault();
24
25 updateUserMutation.mutate({
26 body: {
27 email,
28 profile: {
29 name,
30 age,
31 },
32 },
33 query: { notify },
34 params: { id },
35 disableClientValidation,
36 });
37 };
38 return (
39 <form onSubmit={handleSubmit}>
40 <h3>Body</h3>
41 <div>
42 <label>User email</label>
43 <input
44 name="email"
45 placeholder="john@example.com"
46 value={email}
47 onChange={(e) => setEmail(e.target.value)}
48 />
49 </div>
50 <div>
51 <label>User full name</label>
52 <input
53 name="name"
54 type="text"
55 placeholder="John Doe"
56 value={name}
57 onChange={(e) => setName(e.target.value)}
58 />
59 </div>
60 <div>
61 <label>User age</label>
62 <input
63 name="age"
64 type="number"
65 placeholder="35"
66 value={age}
67 onChange={(e) => setAge(Number(e.target.value))}
68 />
69 </div>
70
71 <h3>Params</h3>
72 <div>
73 <label>User ID</label>
74 <input
75 name="id"
76 type="text"
77 placeholder="123e4567-e89b-12d3-a456-426614174000"
78 value={id}
79 onChange={(e) => setId(e.target.value)}
80 />
81 </div>
82 <h3>Query</h3>
83 <div>
84 <label>Notification type</label>
85 <select
86 name="notify"
87 value={notify}
88 onChange={(e) =>
89 setNotify(e.target.value as "email" | "push" | "none")
90 }
91 >
92 <option value="none">None</option>
93 <option value="email">Email</option>
94 <option value="push">Push</option>
95 <option value="sms">SMS (error)</option>
96 </select>
97 </div>
98 <br />
99 <label>
100 <input
101 type="checkbox"
102 onChange={({ target }) => setDisableClientValidation(target.checked)}
103 checked={disableClientValidation}
104 />{" "}
105 Disable client-side input validation
106 </label>
107 <button type="submit">Submit</button>
108 {(updateUserMutation.data || updateUserMutation.error) && (
109 <output>
110 <strong>Response:</strong>{" "}
111 {updateUserMutation.error ? (
112 <div className="text-red-500">
113 {updateUserMutation.error.message}
114 </div>
115 ) : (
116 <div className="text-green-500">
117 {JSON.stringify(updateUserMutation.data)}
118 </div>
119 )}
120 </output>
121 )}
122 </form>
123 );
124};
125
126export default UserFormDemo;
1"use client";
2import React, { useState } from "react";
3import { type VovkQuery } from "vovk";
4import { UserRPC } from "../../client/root"; // segmented client
5import { useMutation } from "@tanstack/react-query";
6
7const UserFormDemo = () => {
8 const [disableClientValidation, setDisableClientValidation] = useState(false);
9 const [name, setName] = useState("John Doe");
10 const [age, setAge] = useState(35);
11 const [email, setEmail] = useState("john@example.com");
12 const [id, setId] = useState("a937629d-e8f6-4b1e-a819-7669358650a0");
13 const updateUserMutation = useMutation({
14 mutationFn: UserRPC.updateUser,
15 });
16 const [notify, setNotify] = useState<
17 VovkQuery<typeof UserRPC.updateUser>["notify"]
18 >(
19 "sms" as "email", // set error value by default
20 );
21
22 const handleSubmit = (e: React.FormEvent) => {
23 e.preventDefault();
24
25 updateUserMutation.mutate({
26 body: {
27 email,
28 profile: {
29 name,
30 age,
31 },
32 },
33 query: { notify },
34 params: { id },
35 disableClientValidation,
36 });
37 };
38 return (
39 <form onSubmit={handleSubmit}>
40 <h3>Body</h3>
41 <div>
42 <label>User email</label>
43 <input
44 name="email"
45 placeholder="john@example.com"
46 value={email}
47 onChange={(e) => setEmail(e.target.value)}
48 />
49 </div>
50 <div>
51 <label>User full name</label>
52 <input
53 name="name"
54 type="text"
55 placeholder="John Doe"
56 value={name}
57 onChange={(e) => setName(e.target.value)}
58 />
59 </div>
60 <div>
61 <label>User age</label>
62 <input
63 name="age"
64 type="number"
65 placeholder="35"
66 value={age}
67 onChange={(e) => setAge(Number(e.target.value))}
68 />
69 </div>
70
71 <h3>Params</h3>
72 <div>
73 <label>User ID</label>
74 <input
75 name="id"
76 type="text"
77 placeholder="123e4567-e89b-12d3-a456-426614174000"
78 value={id}
79 onChange={(e) => setId(e.target.value)}
80 />
81 </div>
82 <h3>Query</h3>
83 <div>
84 <label>Notification type</label>
85 <select
86 name="notify"
87 value={notify}
88 onChange={(e) =>
89 setNotify(e.target.value as "email" | "push" | "none")
90 }
91 >
92 <option value="none">None</option>
93 <option value="email">Email</option>
94 <option value="push">Push</option>
95 <option value="sms">SMS (error)</option>
96 </select>
97 </div>
98 <br />
99 <label>
100 <input
101 type="checkbox"
102 onChange={({ target }) => setDisableClientValidation(target.checked)}
103 checked={disableClientValidation}
104 />{" "}
105 Disable client-side input validation
106 </label>
107 <button type="submit">Submit</button>
108 {(updateUserMutation.data || updateUserMutation.error) && (
109 <output>
110 <strong>Response:</strong>{" "}
111 {updateUserMutation.error ? (
112 <div className="text-red-500">
113 {updateUserMutation.error.message}
114 </div>
115 ) : (
116 <div className="text-green-500">
117 {JSON.stringify(updateUserMutation.data)}
118 </div>
119 )}
120 </output>
121 )}
122 </form>
123 );
124};
125
126export default UserFormDemo;
1"use client";
2import {
3 useQuery,
4 experimental_streamedQuery as streamedQuery,
5} from "@tanstack/react-query";
6import { StreamRPC } from "../../client/root"; // segmented client
7
8const StreamDemo = () => {
9 const { data } = useQuery({
10 queryKey: StreamRPC.streamTokens.queryKey(),
11 queryFn: streamedQuery({
12 queryFn: () => StreamRPC.streamTokens(),
13 }),
14 });
15
16 return (
17 <div className="h-20">
18 {data?.map((token, index) => (
19 <span key={index}>{token.message}</span>
20 ))}
21 </div>
22 );
23};
24export default StreamDemo;
1"use client";
2import {
3 useQuery,
4 experimental_streamedQuery as streamedQuery,
5} from "@tanstack/react-query";
6import { StreamRPC } from "../../client/root"; // segmented client
7
8const StreamDemo = () => {
9 const { data } = useQuery({
10 queryKey: StreamRPC.streamTokens.queryKey(),
11 queryFn: streamedQuery({
12 queryFn: () => StreamRPC.streamTokens(),
13 }),
14 });
15
16 return (
17 <div className="h-20">
18 {data?.map((token, index) => (
19 <span key={index}>{token.message}</span>
20 ))}
21 </div>
22 );
23};
24export default StreamDemo;
1"use client";
2import {
3 useQuery,
4 experimental_streamedQuery as streamedQuery,
5} from "@tanstack/react-query";
6import { StreamRPC } from "../../client/root"; // segmented client
7
8const StreamDemo = () => {
9 const { data } = useQuery({
10 queryKey: StreamRPC.streamTokens.queryKey(),
11 queryFn: streamedQuery({
12 queryFn: () => StreamRPC.streamTokens(),
13 }),
14 });
15
16 return (
17 <div className="h-20">
18 {data?.map((token, index) => (
19 <span key={index}>{token.message}</span>
20 ))}
21 </div>
22 );
23};
24export default StreamDemo;
1"use client";
2import {
3 useQuery,
4 experimental_streamedQuery as streamedQuery,
5} from "@tanstack/react-query";
6import { StreamRPC } from "../../client/root"; // segmented client
7
8const StreamDemo = () => {
9 const { data } = useQuery({
10 queryKey: StreamRPC.streamTokens.queryKey(),
11 queryFn: streamedQuery({
12 queryFn: () => StreamRPC.streamTokens(),
13 }),
14 });
15
16 return (
17 <div className="h-20">
18 {data?.map((token, index) => (
19 <span key={index}>{token.message}</span>
20 ))}
21 </div>
22 );
23};
24export default StreamDemo;
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
pnpm dlx vovk generate --from rs --from py --origin https://vovk-hello-world.vercel.app
yarn dlx vovk generate --from rs --from py --origin https://vovk-hello-world.vercel.app
bun x 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
pnpm dlx vovk bundle --origin https://vovk-hello-world.vercel.app
yarn dlx vovk bundle --origin https://vovk-hello-world.vercel.app
bun x vovk bundle --origin https://vovk-hello-world.vercel.app
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;
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",
}
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);
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);
1from dist_python.src.vovk_hello_world import UserRPC, OpenApiRPC, StreamRPC # local module
2import vovk_hello_world
3
4def main() -> None:
5 print("\n--- Python Demo ---")
6
7 body: UserRPC.UpdateUserBody = {
8 "email": "john@example.com",
9 "profile": {
10 "name": "John Doe",
11 "age": 25
12 }
13 }
14 query: UserRPC.UpdateUserQuery = {"notify": "email"}
15 params: UserRPC.UpdateUserParams = {"id": "123e4567-e89b-12d3-a456-426614174000"}
16 # Update user using local module
17 update_user_response = UserRPC.update_user(
18 params=params,
19 body=body,
20 query=query
21 )
22 print('UserRPC.update_user:', update_user_response)
23
24 # Get OpenAPI spec from local module
25 openapi_response = OpenApiRPC.get_spec()
26 print(f"OpenApiRPC.get_spec from the local module: {openapi_response['info']['title']} {openapi_response['info']['version']}")
27
28 # Stream tokens from local module
29 stream_response = StreamRPC.stream_tokens()
30 print("streamTokens:")
31 for item in stream_response:
32 print(item['message'], end='', flush=True)
33 print() # Add newline after streaming
34
35 # Get OpenAPI spec from installed package
36 openapi_response_from_bundle = vovk_hello_world.OpenApiRPC.get_spec()
37 print(f"OpenApiRPC.get_spec from \"vovk_hello_world\" package: {openapi_response_from_bundle['info']['title']} {openapi_response_from_bundle['info']['version']}")
38
39if __name__ == "__main__":
40 try:
41 main()
42 except Exception as e:
43 print(f"Error: {e}")
1from dist_python.src.vovk_hello_world import UserRPC, OpenApiRPC, StreamRPC # local module
2import vovk_hello_world
3
4def main() -> None:
5 print("\n--- Python Demo ---")
6
7 body: UserRPC.UpdateUserBody = {
8 "email": "john@example.com",
9 "profile": {
10 "name": "John Doe",
11 "age": 25
12 }
13 }
14 query: UserRPC.UpdateUserQuery = {"notify": "email"}
15 params: UserRPC.UpdateUserParams = {"id": "123e4567-e89b-12d3-a456-426614174000"}
16 # Update user using local module
17 update_user_response = UserRPC.update_user(
18 params=params,
19 body=body,
20 query=query
21 )
22 print('UserRPC.update_user:', update_user_response)
23
24 # Get OpenAPI spec from local module
25 openapi_response = OpenApiRPC.get_spec()
26 print(f"OpenApiRPC.get_spec from the local module: {openapi_response['info']['title']} {openapi_response['info']['version']}")
27
28 # Stream tokens from local module
29 stream_response = StreamRPC.stream_tokens()
30 print("streamTokens:")
31 for item in stream_response:
32 print(item['message'], end='', flush=True)
33 print() # Add newline after streaming
34
35 # Get OpenAPI spec from installed package
36 openapi_response_from_bundle = vovk_hello_world.OpenApiRPC.get_spec()
37 print(f"OpenApiRPC.get_spec from \"vovk_hello_world\" package: {openapi_response_from_bundle['info']['title']} {openapi_response_from_bundle['info']['version']}")
38
39if __name__ == "__main__":
40 try:
41 main()
42 except Exception as e:
43 print(f"Error: {e}")
1from dist_python.src.vovk_hello_world import UserRPC, OpenApiRPC, StreamRPC # local module
2import vovk_hello_world
3
4def main() -> None:
5 print("\n--- Python Demo ---")
6
7 body: UserRPC.UpdateUserBody = {
8 "email": "john@example.com",
9 "profile": {
10 "name": "John Doe",
11 "age": 25
12 }
13 }
14 query: UserRPC.UpdateUserQuery = {"notify": "email"}
15 params: UserRPC.UpdateUserParams = {"id": "123e4567-e89b-12d3-a456-426614174000"}
16 # Update user using local module
17 update_user_response = UserRPC.update_user(
18 params=params,
19 body=body,
20 query=query
21 )
22 print('UserRPC.update_user:', update_user_response)
23
24 # Get OpenAPI spec from local module
25 openapi_response = OpenApiRPC.get_spec()
26 print(f"OpenApiRPC.get_spec from the local module: {openapi_response['info']['title']} {openapi_response['info']['version']}")
27
28 # Stream tokens from local module
29 stream_response = StreamRPC.stream_tokens()
30 print("streamTokens:")
31 for item in stream_response:
32 print(item['message'], end='', flush=True)
33 print() # Add newline after streaming
34
35 # Get OpenAPI spec from installed package
36 openapi_response_from_bundle = vovk_hello_world.OpenApiRPC.get_spec()
37 print(f"OpenApiRPC.get_spec from \"vovk_hello_world\" package: {openapi_response_from_bundle['info']['title']} {openapi_response_from_bundle['info']['version']}")
38
39if __name__ == "__main__":
40 try:
41 main()
42 except Exception as e:
43 print(f"Error: {e}")
1from dist_python.src.vovk_hello_world import UserRPC, OpenApiRPC, StreamRPC # local module
2import vovk_hello_world
3
4def main() -> None:
5 print("\n--- Python Demo ---")
6
7 body: UserRPC.UpdateUserBody = {
8 "email": "john@example.com",
9 "profile": {
10 "name": "John Doe",
11 "age": 25
12 }
13 }
14 query: UserRPC.UpdateUserQuery = {"notify": "email"}
15 params: UserRPC.UpdateUserParams = {"id": "123e4567-e89b-12d3-a456-426614174000"}
16 # Update user using local module
17 update_user_response = UserRPC.update_user(
18 params=params,
19 body=body,
20 query=query
21 )
22 print('UserRPC.update_user:', update_user_response)
23
24 # Get OpenAPI spec from local module
25 openapi_response = OpenApiRPC.get_spec()
26 print(f"OpenApiRPC.get_spec from the local module: {openapi_response['info']['title']} {openapi_response['info']['version']}")
27
28 # Stream tokens from local module
29 stream_response = StreamRPC.stream_tokens()
30 print("streamTokens:")
31 for item in stream_response:
32 print(item['message'], end='', flush=True)
33 print() # Add newline after streaming
34
35 # Get OpenAPI spec from installed package
36 openapi_response_from_bundle = vovk_hello_world.OpenApiRPC.get_spec()
37 print(f"OpenApiRPC.get_spec from \"vovk_hello_world\" package: {openapi_response_from_bundle['info']['title']} {openapi_response_from_bundle['info']['version']}")
38
39if __name__ == "__main__":
40 try:
41 main()
42 except Exception as e:
43 print(f"Error: {e}")
1use std::io::Write;
2use vovk_hello_world_local::{
3 user_rpc,
4 open_api_rpc,
5 stream_rpc
6};
7use vovk_hello_world::open_api_rpc as open_api_rpc_from_crate;
8pub fn main() {
9 println!("\n--- Rust Demo ---");
10 use user_rpc::update_user_::{
11 body as Body,
12 body_::profile as Profile,
13 query as Query,
14 query_::notify as Notify,
15 params as Params,
16 };
17
18 let update_user_response = user_rpc::update_user(
19 Body {
20 email: String::from("john@example.com"),
21 profile: Profile {
22 name: String::from("John Doe"),
23 age: 25
24 }
25 },
26 Query {
27 notify: Notify::email
28 },
29 Params {
30 id: String::from("123e4567-e89b-12d3-a456-426614174000")
31 },
32 None,
33 None,
34 false,
35 );
36
37 match update_user_response {
38 Ok(response) => println!("user_rpc.update_user: {:?}", response),
39 Err(e) => println!("update_user error: {:?}", e),
40 }
41
42 let openapi_response = open_api_rpc::get_spec(
43 (),
44 (),
45 (),
46 None,
47 None,
48 false,
49 );
50
51 match openapi_response {
52 Ok(spec) => {
53 if let (Some(title), Some(version)) = (
54 spec["info"]["title"].as_str(),
55 spec["info"]["version"].as_str()
56 ) {
57 println!("open_api_rpc.get_spec from the local module: {} {}", title, version);
58 } else {
59 println!("Could not extract title or version from OpenAPI spec");
60 }
61 },
62 Err(e) => println!("Error fetching OpenAPI spec: {:?}", e),
63 }
64
65 let stream_response = stream_rpc::stream_tokens(
66 (),
67 (),
68 (),
69 None,
70 None,
71 false,
72 );
73
74 match stream_response {
75 Ok(stream) => {
76 print!("streamTokens:\n");
77 for (_i, item) in stream.enumerate() {
78 print!("{}", item.message.as_str());
79 std::io::stdout().flush().unwrap();
80 }
81 println!();
82 },
83 Err(e) => println!("Error initiating stream: {:?}", e),
84 }
85
86 let openapi_response_from_crate = open_api_rpc_from_crate::get_spec(
87 (),
88 (),
89 (),
90 None,
91 None,
92 false,
93 );
94
95 match openapi_response_from_crate {
96 Ok(spec) => {
97 if let (Some(title), Some(version)) = (
98 spec["info"]["title"].as_str(),
99 spec["info"]["version"].as_str()
100 ) {
101 println!("open_api_rpc.get_spec from \"vovk_hello_world\" crate: {} {}", title, version);
102 } else {
103 println!("Could not extract title or version from OpenAPI spec");
104 }
105 },
106 Err(e) => println!("Error fetching OpenAPI spec from crate: {:?}", e),
107 }
108}
1use std::io::Write;
2use vovk_hello_world_local::{
3 user_rpc,
4 open_api_rpc,
5 stream_rpc
6};
7use vovk_hello_world::open_api_rpc as open_api_rpc_from_crate;
8pub fn main() {
9 println!("\n--- Rust Demo ---");
10 use user_rpc::update_user_::{
11 body as Body,
12 body_::profile as Profile,
13 query as Query,
14 query_::notify as Notify,
15 params as Params,
16 };
17
18 let update_user_response = user_rpc::update_user(
19 Body {
20 email: String::from("john@example.com"),
21 profile: Profile {
22 name: String::from("John Doe"),
23 age: 25
24 }
25 },
26 Query {
27 notify: Notify::email
28 },
29 Params {
30 id: String::from("123e4567-e89b-12d3-a456-426614174000")
31 },
32 None,
33 None,
34 false,
35 );
36
37 match update_user_response {
38 Ok(response) => println!("user_rpc.update_user: {:?}", response),
39 Err(e) => println!("update_user error: {:?}", e),
40 }
41
42 let openapi_response = open_api_rpc::get_spec(
43 (),
44 (),
45 (),
46 None,
47 None,
48 false,
49 );
50
51 match openapi_response {
52 Ok(spec) => {
53 if let (Some(title), Some(version)) = (
54 spec["info"]["title"].as_str(),
55 spec["info"]["version"].as_str()
56 ) {
57 println!("open_api_rpc.get_spec from the local module: {} {}", title, version);
58 } else {
59 println!("Could not extract title or version from OpenAPI spec");
60 }
61 },
62 Err(e) => println!("Error fetching OpenAPI spec: {:?}", e),
63 }
64
65 let stream_response = stream_rpc::stream_tokens(
66 (),
67 (),
68 (),
69 None,
70 None,
71 false,
72 );
73
74 match stream_response {
75 Ok(stream) => {
76 print!("streamTokens:\n");
77 for (_i, item) in stream.enumerate() {
78 print!("{}", item.message.as_str());
79 std::io::stdout().flush().unwrap();
80 }
81 println!();
82 },
83 Err(e) => println!("Error initiating stream: {:?}", e),
84 }
85
86 let openapi_response_from_crate = open_api_rpc_from_crate::get_spec(
87 (),
88 (),
89 (),
90 None,
91 None,
92 false,
93 );
94
95 match openapi_response_from_crate {
96 Ok(spec) => {
97 if let (Some(title), Some(version)) = (
98 spec["info"]["title"].as_str(),
99 spec["info"]["version"].as_str()
100 ) {
101 println!("open_api_rpc.get_spec from \"vovk_hello_world\" crate: {} {}", title, version);
102 } else {
103 println!("Could not extract title or version from OpenAPI spec");
104 }
105 },
106 Err(e) => println!("Error fetching OpenAPI spec from crate: {:?}", e),
107 }
108}
1use std::io::Write;
2use vovk_hello_world_local::{
3 user_rpc,
4 open_api_rpc,
5 stream_rpc
6};
7use vovk_hello_world::open_api_rpc as open_api_rpc_from_crate;
8pub fn main() {
9 println!("\n--- Rust Demo ---");
10 use user_rpc::update_user_::{
11 body as Body,
12 body_::profile as Profile,
13 query as Query,
14 query_::notify as Notify,
15 params as Params,
16 };
17
18 let update_user_response = user_rpc::update_user(
19 Body {
20 email: String::from("john@example.com"),
21 profile: Profile {
22 name: String::from("John Doe"),
23 age: 25
24 }
25 },
26 Query {
27 notify: Notify::email
28 },
29 Params {
30 id: String::from("123e4567-e89b-12d3-a456-426614174000")
31 },
32 None,
33 None,
34 false,
35 );
36
37 match update_user_response {
38 Ok(response) => println!("user_rpc.update_user: {:?}", response),
39 Err(e) => println!("update_user error: {:?}", e),
40 }
41
42 let openapi_response = open_api_rpc::get_spec(
43 (),
44 (),
45 (),
46 None,
47 None,
48 false,
49 );
50
51 match openapi_response {
52 Ok(spec) => {
53 if let (Some(title), Some(version)) = (
54 spec["info"]["title"].as_str(),
55 spec["info"]["version"].as_str()
56 ) {
57 println!("open_api_rpc.get_spec from the local module: {} {}", title, version);
58 } else {
59 println!("Could not extract title or version from OpenAPI spec");
60 }
61 },
62 Err(e) => println!("Error fetching OpenAPI spec: {:?}", e),
63 }
64
65 let stream_response = stream_rpc::stream_tokens(
66 (),
67 (),
68 (),
69 None,
70 None,
71 false,
72 );
73
74 match stream_response {
75 Ok(stream) => {
76 print!("streamTokens:\n");
77 for (_i, item) in stream.enumerate() {
78 print!("{}", item.message.as_str());
79 std::io::stdout().flush().unwrap();
80 }
81 println!();
82 },
83 Err(e) => println!("Error initiating stream: {:?}", e),
84 }
85
86 let openapi_response_from_crate = open_api_rpc_from_crate::get_spec(
87 (),
88 (),
89 (),
90 None,
91 None,
92 false,
93 );
94
95 match openapi_response_from_crate {
96 Ok(spec) => {
97 if let (Some(title), Some(version)) = (
98 spec["info"]["title"].as_str(),
99 spec["info"]["version"].as_str()
100 ) {
101 println!("open_api_rpc.get_spec from \"vovk_hello_world\" crate: {} {}", title, version);
102 } else {
103 println!("Could not extract title or version from OpenAPI spec");
104 }
105 },
106 Err(e) => println!("Error fetching OpenAPI spec from crate: {:?}", e),
107 }
108}
1use std::io::Write;
2use vovk_hello_world_local::{
3 user_rpc,
4 open_api_rpc,
5 stream_rpc
6};
7use vovk_hello_world::open_api_rpc as open_api_rpc_from_crate;
8pub fn main() {
9 println!("\n--- Rust Demo ---");
10 use user_rpc::update_user_::{
11 body as Body,
12 body_::profile as Profile,
13 query as Query,
14 query_::notify as Notify,
15 params as Params,
16 };
17
18 let update_user_response = user_rpc::update_user(
19 Body {
20 email: String::from("john@example.com"),
21 profile: Profile {
22 name: String::from("John Doe"),
23 age: 25
24 }
25 },
26 Query {
27 notify: Notify::email
28 },
29 Params {
30 id: String::from("123e4567-e89b-12d3-a456-426614174000")
31 },
32 None,
33 None,
34 false,
35 );
36
37 match update_user_response {
38 Ok(response) => println!("user_rpc.update_user: {:?}", response),
39 Err(e) => println!("update_user error: {:?}", e),
40 }
41
42 let openapi_response = open_api_rpc::get_spec(
43 (),
44 (),
45 (),
46 None,
47 None,
48 false,
49 );
50
51 match openapi_response {
52 Ok(spec) => {
53 if let (Some(title), Some(version)) = (
54 spec["info"]["title"].as_str(),
55 spec["info"]["version"].as_str()
56 ) {
57 println!("open_api_rpc.get_spec from the local module: {} {}", title, version);
58 } else {
59 println!("Could not extract title or version from OpenAPI spec");
60 }
61 },
62 Err(e) => println!("Error fetching OpenAPI spec: {:?}", e),
63 }
64
65 let stream_response = stream_rpc::stream_tokens(
66 (),
67 (),
68 (),
69 None,
70 None,
71 false,
72 );
73
74 match stream_response {
75 Ok(stream) => {
76 print!("streamTokens:\n");
77 for (_i, item) in stream.enumerate() {
78 print!("{}", item.message.as_str());
79 std::io::stdout().flush().unwrap();
80 }
81 println!();
82 },
83 Err(e) => println!("Error initiating stream: {:?}", e),
84 }
85
86 let openapi_response_from_crate = open_api_rpc_from_crate::get_spec(
87 (),
88 (),
89 (),
90 None,
91 None,
92 false,
93 );
94
95 match openapi_response_from_crate {
96 Ok(spec) => {
97 if let (Some(title), Some(version)) = (
98 spec["info"]["title"].as_str(),
99 spec["info"]["version"].as_str()
100 ) {
101 println!("open_api_rpc.get_spec from \"vovk_hello_world\" crate: {} {}", title, version);
102 } else {
103 println!("Could not extract title or version from OpenAPI spec");
104 }
105 },
106 Err(e) => println!("Error fetching OpenAPI spec from crate: {:?}", e),
107 }
108}
OpenAPI specification
OpenAPI specification requires creating another GET
endpoint that uses vovkSchemaToOpenAPI
function to generate the OpenAPI spec from the Vovk.ts schema.
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}
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.