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
.
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.
{
"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
- usenestjs-operation-id
strategy to get module names fromoperationId
field of the OpenAPI spec.--openapi-get-method-name=nestjs-operation-id
- usenestjs-operation-id
strategy to get method names fromoperationId
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.
{
"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:
/** @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:
{
"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:
- Validate with the emitted JSON schema with vovk-ajv library.
- Validate with DTOs using vovk-dto/validateOnClient.
vovk-ajv
npm i vovk-ajv@draft
/** @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
/** @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.