Skip to Content
RPC for NestJS

RPC for NestJS

As NestJS provides built-in OpenAPI support with @nestjs/swagger  and Vovk.ts can generate a client from OpenAPI 3+ specification, it’s possible to generate a type-safe “RPC” client for a NestJS app with all the benefits of Vovk.ts, including client-side validation, function calling support, and more.

Install dependencies

npm i vovk-client
npm i vovk-cli@draft -D

Expose OpenAPI spec in your NestJS app

At this case we expose it at /api-ref endpoint that contain OAS and Swagger UI at /docs.

src/main.ts
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { const appOptions = { cors: true }; const app = await NestFactory.create(AppModule, appOptions); app.setGlobalPrefix('api'); const options = new DocumentBuilder() .setTitle('NestJS Example App') .setDescription('The API description') .setVersion('1.0') .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, options); app.getHttpAdapter().get('/api-ref', (req, res) => { res.json(document); }); SwaggerModule.setup('/docs', app, document); await app.listen(3000); } bootstrap() .catch((err) => { console.log(err); });

Use OpenAPI decorators from @nestjs/swagger 

Follow the official documentation  to decorate your controllers and DTOs with OpenAPI decorators.

import { Controller, Post, Body, Query, Param, HttpStatus } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiParam, ApiBody, ApiQuery, ApiExtraModels } from '@nestjs/swagger'; import { CreateUserDto, QueryParamsDto, UserResponseDto } from './dto/user.dto'; @Controller('users') @ApiExtraModels(UserResponseDto) // Ensures the DTO appears in schemas export class UserController { @Post(':organizationId') @ApiOperation({ summary: 'Create a new user' }) @ApiParam({ name: 'organizationId', type: 'string', description: 'Organization ID', }) @ApiBody({ type: CreateUserDto, description: 'User creation data' }) @ApiQuery({ type: QueryParamsDto, required: false }) @ApiResponse({ status: HttpStatus.CREATED, description: 'User successfully created', type: UserResponseDto, // This ensures it appears in components/schemas }) async createUser( @Param('organizationId') organizationId: string, @Body() createUserDto: CreateUserDto, @Query() queryParams: QueryParamsDto ): Promise<UserResponseDto> { // ... } }

Add generate script to package.json

Run the development server so NestJS should be available at http://localhost:3000 . Set --openapi-root-url to your server URL if it’s different.

package.json
{ "scripts": { // ... "start:dev": "nest start --watch", "generate": "vovk g --openapi=http://localhost:3000/api-ref --openapi-get-module-name=nestjs-operation-id --openapi-get-method-name=nestjs-operation-id --openapi-root-url=http://localhost:3000 --openapi-fallback .openapi-cache/openapi.json" } }

The generate command is quite long, let’s break it down:

  • --openapi=http://localhost:3000/api-ref - the URL to the OpenAPI spec endpoint.
  • --openapi-get-module-name=nestjs-operation-id - use nestjs-operation-id strategy to get module names from operationId field of the OpenAPI spec.
  • --openapi-get-method-name=nestjs-operation-id - use nestjs-operation-id strategy to get method names from operationId field of the OpenAPI spec.
  • --openapi-root-url=http://localhost:3000 - the root URL of your API.
  • --openapi-fallback .openapi-cache/openapi.json - save the OpenAPI spec to a local file and use it as a fallback if the URL is not available. Good for CI/CD.

The nestjs-operation-id strategy assumes that operationId in your OpenAPI spec is defined as XxxController_methodName, e.g. UserController_createUser. By this strategy XxxController is turned into an RPC module name XxxRPC and methodName is turned into a method name as is. If you want to change this behavior, you can create a config file and define getModuleName and getMethodName functions manually.

Run the generate script in a separate terminal:

npm run generate

You can optionally run start:dev and generate scripts in parallel using concurrently  package. The --watch option makes generate command to watch for changes in the OpenAPI spec and regenerate the client automatically.

package.json
{ "scripts": { // ... "generate": "...", "start:dev": "nest start --watch", "dev": "concurrently \"npm run start:dev\" \"sleep 5 && npm run generate -- --watch\" --kill-others" } }

The dev script is going to run both start:dev and generate scripts in parallel, waiting 5 seconds before starting generate to give NestJS some time to start.

npm run dev

For more flexibility it’s recommended to create a config file.

Import and use

Import the resulting client from any fetch-enabled environment:

import { UserRPC } from 'vovk-client'; const user = await UserRPC.createUser({ body: { name: 'John Doe', email: 'john.doe@example.com' }, query: { page: 1, limit: 10 }, params: { organizationId: 'org123' } });

Create config file

If the flags are too long, or if you want to use additioannal Vovk.ts features, create a config file in the project root:

vovk.config.mjs
/** @type {import('vovk-cli').VovkConfig} */ const config = { generatorConfig: { segments: { nest: { openAPIMixin: { source: { url: 'http://localhost:3000/api-ref', fallback: '.openapi-cache/openapi.json', }, apiRoot: 'http://localhost:3000', getModuleName: 'nestjs-operation-id', getMethodName: 'nestjs-operation-id', }, }, }, }, }; export default config;

At this case the generate script can be simplified:

package.json
{ "scripts": { // ... "generate": "vovk g" } }

Configure fetcher

In order to configure authorization headers, add custom options to the generated client, or transform the response, you can create a custom fetcher (requires config file).

import { UserRPC } from 'vovk-client'; await UserRPC.updateUser({ // ... successMessage: 'User updated successfully', useAuthorization: true, somethingCustom: 'customValue', });

Enable client-side validation

The client-side validation isn’t enabled by default. For a NestJS app there are 2 options:

vovk-ajv

npm i vovk-ajv@draft
vovk.config.mjs
/** @type {import('vovk').VovkConfig} */ const config = { generatorConfig: { imports: { validateOnClient: 'vovk-ajv', }, }, }; export default config;

For more information see the vovk-ajv section.

vovk-dto/validateOnClient

npm i vovk-dto@draft
vovk.config.mjs
/** @type {import('vovk').VovkConfig} */ const config = { generatorConfig: { imports: { validateOnClient: 'vovk-dto/validateOnClient', }, }, }; export default config;

In this case, the generated client methods will validate input data with DTOs using class-validator  library before sending the request to the server.

import { UserRPC } from 'vovk-client'; import { plainToInstance } from 'class-transformer'; import { UpdateUserBodyDto, UpdateUserResponseDto } from '@/modules/user/UserDto'; const respData = await UserRPC.updateUser({ body: plainToInstance(UpdateUserBodyDto, { name: 'John Doe', age: 42, } satisfies UpdateUserBodyDto), // ... same for query and params }); // optionally transform response data to DTO const dtoInstance = plainToInstance(UpdateUserResponseDto, respData);

For more information see the vovk-dto/validateOnClient section.

Function calling

As any other RPC module, the generated NestJS RPC modules can be turned into LLM tools, invoking the server methods thru HTTP protocol.

For more information see the function calling page.

Last updated on