“Hello World” showcase
The “Hello World” app is a Next.js/Vovk.ts project example that showcases some of the core features of Vovk.ts by implementing an HTML form with some extra features. It implements UserController with an updateUser method served as POST to /api/users/{id} endpoint, StreamController with a JSONLines handler streamTokens served as GET to /api/streams/tokens endpoint, but also OpenApiController with getSpec method served as GET to /api/static/openapi/spec.json endpoint that provides an OpenAPI specification for the API from a static segment. The OpenAPI documentation can be viewed at /openapi page.
The project can be explored at the GitHub repository and viewed live at hello-world.vovk.dev . 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 pulled from the source code on Github and HTML pages are served within an iframe.
Topics/concepts covered
- Zod validation, with a
body,queryandparamsinputs and schema descriptions using Zod’s meta method. - Client-side validation for RPC method requests.
- Composed & segmented TypeScript client.
- JSONLines streaming.
- Type inference in a service, while controller method returns service’s method result.
useQueryanduseMutationusage with queryKey method.- Generation of Rust and Python clients with README, published on Crates.io and PyPI .
- TypeScript client bundle with README, published on NPM .
- OpenAPI spec with code samples served from static segment using Scalar .
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’s 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 source of truth for the service’s input arguments, and the service’s return 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. 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";
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";
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";
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";
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 type { 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 type { 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 type { 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 type { 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 streamFn: () => 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 streamFn: () => 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 streamFn: () => 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 streamFn: () => 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 a 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://hello-world.vovk.devpnpm dlx vovk generate --from rs --from py --origin https://hello-world.vovk.devyarn dlx vovk generate --from rs --from py --origin https://hello-world.vovk.devbun x vovk generate --from rs --from py --origin https://hello-world.vovk.devAnd the bundle with:
npx vovk bundle --origin https://hello-world.vovk.devpnpm dlx vovk bundle --origin https://hello-world.vovk.devyarn dlx vovk bundle --origin https://hello-world.vovk.devbun x vovk bundle --origin https://hello-world.vovk.dev1// @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: '',
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 segmentedClient: {
32 // fromTemplates: ["mjs", "cjs"],
33 enabled: true,
34 // outDir: "./src/client",
35 },
36 bundle: {
37 generatorConfig: { origin: PROD_URL },
38 keepPrebundleDir: true,
39 // tsdownBuildOptions: { outDir: "./dist" },
40 },
41 clientTemplateDefs: {
42 py: {
43 extends: "py",
44 generatorConfig: { origin: PROD_URL },
45 // composedClient: { outDir: "./dist_python" },
46 },
47 rs: {
48 extends: "rs",
49 generatorConfig: { origin: PROD_URL },
50 // composedClient: { outDir: "./dist_rust" },
51 },
52 },
53 moduleTemplates: {
54 controller: "vovk-zod/module-templates/controller.ts.ejs",
55 service: "vovk-cli/module-templates/service.ts.ejs",
56 },
57};
58
59module.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: '',
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 segmentedClient: {
32 // fromTemplates: ["mjs", "cjs"],
33 enabled: true,
34 // outDir: "./src/client",
35 },
36 bundle: {
37 generatorConfig: { origin: PROD_URL },
38 keepPrebundleDir: true,
39 // tsdownBuildOptions: { outDir: "./dist" },
40 },
41 clientTemplateDefs: {
42 py: {
43 extends: "py",
44 generatorConfig: { origin: PROD_URL },
45 // composedClient: { outDir: "./dist_python" },
46 },
47 rs: {
48 extends: "rs",
49 generatorConfig: { origin: PROD_URL },
50 // composedClient: { outDir: "./dist_rust" },
51 },
52 },
53 moduleTemplates: {
54 controller: "vovk-zod/module-templates/controller.ts.ejs",
55 service: "vovk-cli/module-templates/service.ts.ejs",
56 },
57};
58
59module.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: '',
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 segmentedClient: {
32 // fromTemplates: ["mjs", "cjs"],
33 enabled: true,
34 // outDir: "./src/client",
35 },
36 bundle: {
37 generatorConfig: { origin: PROD_URL },
38 keepPrebundleDir: true,
39 // tsdownBuildOptions: { outDir: "./dist" },
40 },
41 clientTemplateDefs: {
42 py: {
43 extends: "py",
44 generatorConfig: { origin: PROD_URL },
45 // composedClient: { outDir: "./dist_python" },
46 },
47 rs: {
48 extends: "rs",
49 generatorConfig: { origin: PROD_URL },
50 // composedClient: { outDir: "./dist_rust" },
51 },
52 },
53 moduleTemplates: {
54 controller: "vovk-zod/module-templates/controller.ts.ejs",
55 service: "vovk-cli/module-templates/service.ts.ejs",
56 },
57};
58
59module.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: '',
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 segmentedClient: {
32 // fromTemplates: ["mjs", "cjs"],
33 enabled: true,
34 // outDir: "./src/client",
35 },
36 bundle: {
37 generatorConfig: { origin: PROD_URL },
38 keepPrebundleDir: true,
39 // tsdownBuildOptions: { outDir: "./dist" },
40 },
41 clientTemplateDefs: {
42 py: {
43 extends: "py",
44 generatorConfig: { origin: PROD_URL },
45 // composedClient: { outDir: "./dist_python" },
46 },
47 rs: {
48 extends: "rs",
49 generatorConfig: { origin: PROD_URL },
50 // composedClient: { outDir: "./dist_rust" },
51 },
52 },
53 moduleTemplates: {
54 controller: "vovk-zod/module-templates/controller.ts.ejs",
55 service: "vovk-cli/module-templates/service.ts.ejs",
56 },
57};
58
59module.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 { get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4export default class OpenApiController {
5 @operation({
6 summary: "OpenAPI spec",
7 description: 'Get the OpenAPI spec for the "Hello World" app API',
8 })
9 @get("openapi.json", { cors: true })
10 static getSpec = () => openapi;
11}
1import { get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4export default class OpenApiController {
5 @operation({
6 summary: "OpenAPI spec",
7 description: 'Get the OpenAPI spec for the "Hello World" app API',
8 })
9 @get("openapi.json", { cors: true })
10 static getSpec = () => openapi;
11}
1import { get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4export default class OpenApiController {
5 @operation({
6 summary: "OpenAPI spec",
7 description: 'Get the OpenAPI spec for the "Hello World" app API',
8 })
9 @get("openapi.json", { cors: true })
10 static getSpec = () => openapi;
11}
1import { get, operation } from "vovk";
2import { openapi } from "vovk-client/openapi";
3
4export default class OpenApiController {
5 @operation({
6 summary: "OpenAPI spec",
7 description: 'Get the OpenAPI spec for the "Hello World" app API',
8 })
9 @get("openapi.json", { cors: true })
10 static getSpec = () => openapi;
11}
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.