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:
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
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 typeVovkHandlerSchema
;controllerSchema
- the schema object of the methodβs controller of typeVovkControllerSchema
;segmentSchema
- the schema object of the segment of typeVovkSegmentSchema
;fullSchema
- the full schema object of typeVovkSchema
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 andpackage.json
that can be used to publish the client library as a package.
For more info, check the client templates documentation.