Skip to Content
TypeScript RPC 🚧

TypeScript RPC Client

Controllers implemented with Vovk.ts are compiled to RPC modules with similar structure but different argument signatures. Having the following controller defined:

src/modules/user/UserController.ts
import { z } from 'zod'; import { withZod } from 'vovk-zod'; import { prefix, post, openapi } from 'vovk'; @prefix('users') export default class UserController { @openapi({ summary: 'Update user (Zod)', description: 'Update user by ID with Zod validation', }) @post('{id}') static updateUser = withZod({ body: z .object({ name: z.string().meta({ description: 'User full name' }), age: z.number().min(0).max(120).meta({ description: 'User age' }), email: z.email().meta({ description: 'User email' }), }) .meta({ description: 'User object' }), params: z.object({ id: z.uuid().meta({ description: 'User ID' }), }), query: z.object({ notify: z.enum(['email', 'push', 'none']).meta({ description: 'Notification type' }), }), output: z .object({ success: z.boolean().meta({ description: 'Success status' }), }) .meta({ description: 'Response object' }), async handle(req, { id }) { const { name, age } = await req.json(); const notify = req.nextUrl.searchParams.get('notify'); // do something with the data console.log(`Updating user ${id}:`, { name, age, notify }); return { success: true, }; }, }); }

It will be compiled to the following RPC module that has body, params, query as triple input.

import { UserRPC } from 'vovk-client'; const updatedUser = await UserRPC.updateUser({ body: { name: 'John Doe', age: 30, email: 'john@example.com' }, params: { id: '69' }, query: { notify: 'push' }, });

Behind the scenes, the updateUser method serializes query and params to the URL and sends a regular fetch request to the server, which is then handled by the UserController.updateUser method on the server-side. The response is returned as a promise that resolves to the output type defined in the controller.

const resp = await fetch(`/api/users/${id}?notify=push`, { method: 'POST', body: JSON.stringify({ /* ... */ }), });

RPC method options

Every RPC method has a set of options that can be passed to it, besides body, params and query. The list of options can be customized with a custom fetcher.

apiRoot

An option that allows to override the default API root. By default, it’s /api and can be configured by setting rootEntry and/or origin.

init

Allows to set RequestInit options (the fetch function options) for a method. Can be used to set headers, credentials, and other options, supported by the native fetch but also custom Next.js options  like next: { revalidate: number }.

const user = await UserRPC.updateUser({ body: { /* ... */ }, params: { /* ... */ }, query: { /* ... */ }, init: { headers: { 'X-Custom-Header': 'value', }, credentials: 'include', next: { revalidate: 60 }, }, });

transform

Transforms the return value of the method. It can be a function that takes the response and returns a transformed value.

const user = await UserRPC.updateUser({ body: { /* ... */ }, params: { /* ... */ }, query: { /* ... */ }, transform: (data, response) => { // Modify the response here return value; }, });

It can also be used to receive the Response as part of return value:

const [user, response] = await UserRPC.updateUser({ body: { /* ... */ }, params: { /* ... */ }, query: { /* ... */ }, transform: (data, response) => [data, response], }); response satisfies Response;

disableClientValidation

Disables client-side validation for this invocation. Might be useful while you debug validation errors, and want to receive server-side validation error instead. See validation page for more details.

await UserRPC.updateUser({ // ... disableClientValidation: true, });

interpretAs

Overrides the response content-type interpretation. Originally created for the case when the server returns JSONLines but the content-type isn’t set to application/jsonl.

const user = await UserRPC.updateUser({ body: { /* ... */ }, params: { /* ... */ }, query: { /* ... */ }, interpretAs: 'application/jsonl', });

Customization

The client library’s fetching function, as well as its types can be customised in order to follow logic required by the application. See fetcher customization docs for more details.

await UserRPC.updateUser({ // ... successMessage: 'Successfully updated the user', someOtherCustomFlag: true, });

Type override

In case if your code makes it impossible to recognise the return type, you can override it manually with no need to convert it to unknown first.

import { UserRPC } from 'vovk-client'; import type { SomeType } from '../types'; // ... // Override the return type const updatedUser = await UserRPC.updateUser<SomeType>(/* ... * /);

Async iterable

src/modules/user/UserController.ts
import { get } from 'vovk'; export default class UserController { @get() static async *doSomething(/* ... */) { yield* iterable; } }

If iterable is returned, the client library is going to cast the method as a disposable async generator to implement response streaming. It’s explained in more details on the JSON streaming documentation.

Access to schema

Every RPC method has access to the emitted JSON schema. They are available in the following properties:

  • schema - the schema for this method of type VovkHandlerSchema;
  • controllerSchema - the schema object of the method’s controller of type VovkControllerSchema;
  • segmentSchema - the schema object of the segment of type VovkSegmentSchema;
  • fullSchema - the full schema object of type VovkSchema that includes all available segments but also emitted config (by default "libs" option only, see config documentation).
console.log(UserRPC.updateUser.schema.validation.body); // get body validation JSON schema console.log(UserRPC.updateUser.schema.openapi); // get openapi spec for this method console.log(UserRPC.updateUser.fullSchema.config.libs.ajv); // get config option

React Query

Every RPC method has queryKey utility that returns key for the method that is guaranteed to be unique, that can be used with @tanstack/react-query . It returns a touple of strings that can be used as a key for the React Query cache. The touple contains the following values: [segmentName, controllerPrefix, rpcModuleName, decoratorPath, httpMethod, ...key], where ...key is an optional array of additional keys that can be used to differentiate the queries that can be passed to the queryKey method.

import { useQuery } from '@tanstack/react-query'; import { UserRPC } from 'vovk-client'; const MyComponent = () => { const query = useQuery({ queryKey: UserRPC.getUser.queryKey(['123']), queryFn: () => UserRPC.getUser({ params: { id: '123' }, }), }); return <div>{query.isLoading ? 'Loading...' : JSON.stringify(query.data)}</div>; };

Streamed responses can utilize streamedQuery that allows to consume JSONLines response as an array.

import { useQuery, experimental_streamedQuery as streamedQuery } from '@tanstack/react-query'; import { JSONLinesRPC } from 'vovk-client'; const JSONLinesComponent = () => { const query = useQuery({ queryKey: JSONLinesRPC.streamTokens.queryKey(), queryFn: streamedQuery({ queryFn: () => JSONLinesRPC.streamTokens(), }), }); return ( <div> Stream result: {query.data?.map(({ message }, i) => <span key={i}>{message}</span>) ?? <em>Loading...</em>} </div> ); };

Mutations would also work with RPC module methods as expected.

import { useMutation } from '@tanstack/react-query'; import { UserRPC } from 'vovk-client'; const MyComponent = () => { const mutation = useMutation({ mutationFn: UserRPC.updateUser, }); return ( <div> <button onClick={() => mutation.mutate({ body: { name: 'John Doe', age: 30 }, params: { id: '123' }, }) } > Update User </button> {mutation.isLoading ? 'Loading...' : JSON.stringify(mutation.data)} </div> ); };

Used Templates

The TypeScript RPC client is generated from the following templates:

  • cjs, mjs (requires schemaCjs) - compiled CJS and ESM modules with typescript definitions; used by default at Composed Client;
  • ts (requires schemaTs) - uncompiled TypeScript module with typescript definitions; used by default at Segmented Client;
  • mixins - .d.ts types and .json file that are generated when OpenAPI Mixins are used;
  • readme, packageJson - README.md that contains RPC documentation and package.json that can be used to publish the client library as a package.

For more info, check the client templates documentation.

Last updated on