π Automatically generate OpenAPI/Swagger response types from your NestJS service return types - Zero duplication, maximum type safety!
- π Automatic Response Generation: Analyzes your service methods and generates corresponding Swagger response classes
- π― Zero Duplication: No need to write separate response DTOs - they're inferred from your service return types
- π‘οΈ Type Safety: Full TypeScript support with generated types
- ποΈ Build-time Generation: Integrates seamlessly with NestJS build process via CLI plugin
- π Rich Swagger Documentation: Automatically generates detailed OpenAPI specifications
- π Easy Integration: Simple plugin configuration in
nest-cli.json - π¨ Smart Decorators:
@InferredAPIResponsedecorator automatically detects response types and HTTP status codes - β‘ File Watcher Integration: Automatically regenerates types when service files change during development
- π Intelligent Type Inference: Handles complex return types, shorthand properties, and conditional expressions
npm install nest-responses-generator-plugin
# or
pnpm add nest-responses-generator-plugin
# or
yarn add nest-responses-generator-pluginAfter installation, you need to build the plugin and ensure it's properly linked in your workspace:
# 1. Build the plugin package
cd packages/plugin
pnpm run build
# 2. Install dependencies in your example app
cd examples/basic-example
pnpm install
# 3. Start the development server
pnpm run start:devNote: If you're using this plugin in a monorepo setup, make sure to run pnpm install from the workspace root to properly link all packages.
Add the plugin to your nest-cli.json:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"plugins": [
{
"name": "@nestjs/swagger",
"options": {
"classValidatorShim": true,
"introspectComments": true,
"controllerFilenameSuffix": [".controller.ts"],
"dtoFilenameSuffix": [".dto.ts", ".entity.ts"]
}
},
{
"name": "nest-responses-generator-plugin",
"options": {
"outputDir": "src/generated",
"servicePattern": "**/*.service.ts",
"controllerPattern": "**/*.controller.ts",
"clean": false
}
}
]
}
}| Option | Type | Default | Description |
|---|---|---|---|
outputDir |
string |
'src/generated' |
Output directory for generated files |
servicePattern |
string |
'**/*.service.ts' |
Glob pattern to match service files |
controllerPattern |
string |
'**/*.controller.ts' |
Glob pattern to match controller files |
clean |
boolean |
false |
Whether to clean output directory before generation |
// users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from '../common/dto/user.dto';
export interface User {
id: number;
firstname: string;
lastname: string;
email: string;
role: string;
}
@Injectable()
export class UsersService {
create(createUserDto: CreateUserDto): User {
return {
id: 1,
firstname: createUserDto.firstname,
lastname: createUserDto.lastname,
email: createUserDto.email,
role: createUserDto.role,
};
}
findAll(): User[] {
return [
{ id: 1, firstname: 'John', lastname: 'Doe', email: '[email protected]', role: 'user' },
{ id: 2, firstname: 'Jane', lastname: 'Smith', email: '[email protected]', role: 'admin' },
];
}
findOne(id: number): User {
return {
id,
firstname: 'John',
lastname: 'Doe',
email: '[email protected]',
role: 'admin',
};
}
update(id: number, updateUserDto: UpdateUserDto): User {
return {
id,
firstname: updateUserDto.firstname || 'John',
lastname: updateUserDto.lastname || 'Doe',
email: updateUserDto.email || '[email protected]',
role: updateUserDto.role || 'admin',
};
}
remove(id: number): { deleted: boolean } {
return { deleted: true };
}
findAllPaginated(page: number = 1, limit: number = 10): {
data: User[];
meta: {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
} {
const users = this.findAll();
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedUsers = users.slice(startIndex, endIndex);
const total = users.length;
const totalPages = Math.ceil(total / limit);
return {
data: paginatedUsers,
meta: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
};
}
}When you build your NestJS application, the plugin automatically generates response classes:
npm run build
# or during development (with automatic regeneration)
npm run start:devThis generates two files:
src/generated/responses/usersservice.response.ts:
// Auto-generated - do not edit manually
import { ApiProperty } from '@nestjs/swagger';
export class UsersServiceCreateResponse {
@ApiProperty({ example: 1, type: 'number' })
id: number;
@ApiProperty({ example: 'example name', type: 'string' })
firstname: string;
@ApiProperty({ example: 'example name', type: 'string' })
lastname: string;
@ApiProperty({ example: '[email protected]', type: 'string' })
email: string;
@ApiProperty({ example: 'user', type: 'string' })
role: string;
}
export class UsersServiceFindAllResponseItem {
@ApiProperty({ example: 1, type: 'number' })
id: number;
@ApiProperty({ example: 'example name', type: 'string' })
firstname: string;
@ApiProperty({ example: 'example name', type: 'string' })
lastname: string;
@ApiProperty({ example: '[email protected]', type: 'string' })
email: string;
@ApiProperty({ example: 'user', type: 'string' })
role: string;
}
export class UsersServiceFindAllPaginatedResponseDataItem {
@ApiProperty({ example: 1, type: 'number' })
id: number;
@ApiProperty({ example: 'example name', type: 'string' })
firstname: string;
@ApiProperty({ example: 'example name', type: 'string' })
lastname: string;
@ApiProperty({ example: '[email protected]', type: 'string' })
email: string;
@ApiProperty({ example: 'user', type: 'string' })
role: string;
}
export class UsersServiceFindAllPaginatedResponseMeta {
@ApiProperty({ example: 1, type: 'number' })
page: number;
@ApiProperty({ example: 10, type: 'number' })
limit: number;
@ApiProperty({ example: 100, type: 'number' })
total: number;
@ApiProperty({ example: 10, type: 'number' })
totalPages: number;
@ApiProperty({ example: true, type: 'boolean' })
hasNext: boolean;
@ApiProperty({ example: false, type: 'boolean' })
hasPrev: boolean;
}
export class UsersServiceFindAllPaginatedResponse {
@ApiProperty({ isArray: true, type: UsersServiceFindAllPaginatedResponseDataItem })
data: UsersServiceFindAllPaginatedResponseDataItem[];
@ApiProperty({ type: UsersServiceFindAllPaginatedResponseMeta })
meta: UsersServiceFindAllPaginatedResponseMeta;
}
export class UsersServiceRemoveResponse {
@ApiProperty({ example: true, type: 'boolean' })
deleted: boolean;
}
// Object to access response types by method name
export const UsersServiceResponse = {
create: UsersServiceCreateResponse,
findAll: UsersServiceFindAllResponseItem,
findOne: UsersServiceFindOneResponse,
update: UsersServiceUpdateResponse,
remove: UsersServiceRemoveResponse,
findAllPaginated: UsersServiceFindAllPaginatedResponse,
} as const;src/generated/index.ts: (Generated decorator and configuration)
// Auto-generated - do not edit manually
import { ApiOkResponse, ApiCreatedResponse } from '@nestjs/swagger';
import * as UsersServiceResponseImport from './responses/usersservice.response';
export const RESPONSE_CONFIG = {
UsersController: {
create: {
responseClass: UsersServiceResponseImport.UsersServiceCreateResponse,
isArray: false,
status: 201
},
findAll: {
responseClass: UsersServiceResponseImport.UsersServiceFindAllResponseItem,
isArray: true,
status: 200
},
findOne: {
responseClass: UsersServiceResponseImport.UsersServiceFindOneResponse,
isArray: false,
status: 200
},
update: {
responseClass: UsersServiceResponseImport.UsersServiceUpdateResponse,
isArray: false,
status: 200
},
remove: {
responseClass: UsersServiceResponseImport.UsersServiceRemoveResponse,
isArray: false,
status: 200
},
findAllPaginated: {
responseClass: UsersServiceResponseImport.UsersServiceFindAllPaginatedResponse,
isArray: false,
status: 200
},
},
};
export function InferredAPIResponse(options: { description?: string } = {}) {
return function (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
// Runtime decorator that applies correct Swagger decorators based on RESPONSE_CONFIG
// Implementation details handled automatically
};
}// users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
ParseIntPipe,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from '../common/dto/user.dto';
import { InferredAPIResponse } from '../generated'; // Import from generated index
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiOperation({
summary: 'Create a new user',
description: 'Creates a new user with the provided information',
})
@InferredAPIResponse({ description: 'User created successfully' })
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
@ApiOperation({ summary: 'Get all users', description: 'Retrieves a list of all users' })
@InferredAPIResponse({ description: 'List of users retrieved successfully' })
findAll() {
return this.usersService.findAll();
}
@Get('paginated')
@ApiOperation({
summary: 'Get paginated users',
description: 'Retrieves a paginated list of users',
})
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page' })
@InferredAPIResponse({ description: 'Paginated users retrieved successfully' })
findAllPaginated(
@Query('page', ParseIntPipe) page?: number,
@Query('limit', ParseIntPipe) limit?: number
) {
return this.usersService.findAllPaginated(page, limit);
}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID', description: 'Retrieves a specific user by their ID' })
@ApiParam({ name: 'id', type: Number, description: "User's ID" })
@InferredAPIResponse({ description: 'User retrieved successfully' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Patch(':id')
@ApiOperation({ summary: 'Update user', description: 'Updates a user by their ID' })
@ApiParam({ name: 'id', type: Number, description: "User's ID" })
@InferredAPIResponse({ description: 'User updated successfully' })
update(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete user', description: 'Deletes a user by their ID' })
@ApiParam({ name: 'id', type: Number, description: "User's ID" })
@InferredAPIResponse({ description: 'User deleted successfully' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}π The magic decorator! Automatically detects the correct response type and HTTP status code based on your service method and HTTP verb.
Parameters:
options: Configuration objectdescription: Custom description for the response
Features:
- β Automatic Type Detection: Infers response class from controller method name and service
- β
HTTP Status Detection: Uses
@ApiCreatedResponsefor@Postmethods,@ApiOkResponsefor others - β
Array Detection: Automatically detects array responses (e.g.,
findAllmethods) - β Zero Configuration: No need to specify response types manually
Examples:
@Post()
@InferredAPIResponse({ description: 'User created successfully' })
// β Automatically uses @ApiCreatedResponse with UsersServiceCreateResponse
@Get()
@InferredAPIResponse({ description: 'Users retrieved successfully' })
// β Automatically uses @ApiOkResponse with UsersServiceFindAllResponseItem[] (array)
@Get(':id')
@InferredAPIResponse({ description: 'User found' })
// β Automatically uses @ApiOkResponse with UsersServiceFindOneResponseYou can also apply @InferredAPIResponse to the entire controller:
@Controller('users')
@InferredAPIResponse() // Applies to all methods in the controller
export class UsersController {
// All methods automatically get appropriate response types
}- Service Scanning: The plugin scans all
*.service.tsfiles during compilation - Type Extraction: Uses TypeScript's compiler API to analyze method return types
- Smart Inference: Handles complex patterns like:
- Shorthand properties (
{ id }instead of{ id: id }) - Conditional expressions (
updateUserDto.name || 'Default') - Nested objects and arrays
- Paginated responses with metadata
- Shorthand properties (
- Class Generation: Creates
@ApiProperty-decorated classes with proper examples - Decorator Generation: Creates intelligent
@InferredAPIResponsedecorator - Runtime Integration: Applies correct Swagger decorators based on HTTP methods
- Automatic Array Detection: Methods like
findAll()automatically useisArray: true - HTTP Status Inference:
@Postmethods get@ApiCreatedResponse, others get@ApiOkResponse - Nested Object Support: Complex return types are properly decomposed into nested classes
- Development-friendly: Automatic regeneration when files change in watch mode
src/
βββ generated/
β βββ index.ts # Exported decorator and configuration
β βββ responses/
β βββ usersservice.response.ts
β βββ productsservice.response.ts
β βββ ... # One file per service
npm run start:dev- β Plugin automatically detects service changes
- β Regenerates response types in real-time
- β No manual intervention required
- β TypeScript compilation continues seamlessly
npm run build- β Response types generated during build
- β Optimized for production
- β No runtime overhead
Check out the examples directory for complete working examples:
- Basic Example - Simple CRUD API demonstrating core features
// β Lots of duplication and manual work
export class CreateUserResponseDto {
@ApiProperty({ example: 1 })
id: number;
@ApiProperty({ example: 'John' })
firstname: string;
// ... more properties
}
@Controller('users')
export class UsersController {
@Post()
@ApiCreatedResponse({ type: CreateUserResponseDto })
create(@Body() dto: CreateUserDto) {
return this.service.create(dto); // Returns different shape!
}
}// β
Zero duplication, automatic synchronization
@Controller('users')
export class UsersController {
@Post()
@InferredAPIResponse({ description: 'User created successfully' })
create(@Body() dto: CreateUserDto) {
return this.service.create(dto); // Plugin infers response type automatically!
}
}- π― 90% less boilerplate code
- π Always in sync - response schemas match actual service return types
- β‘ Development speed - focus on business logic, not documentation
- π‘οΈ Type safety - TypeScript ensures consistency
- π Rich documentation - automatic examples and proper OpenAPI specs
If you encounter the error "nest-responses-generator-plugin" plugin is not installed, follow these steps:
-
Build the plugin package:
cd packages/plugin pnpm run build -
Reinstall dependencies:
cd examples/basic-example pnpm install -
Verify plugin is properly linked:
ls node_modules/nest-responses-generator-plugin/
You should see the
plugin.jsfile anddist/directory. -
Check nest-cli.json configuration: Ensure the plugin is properly configured in your
nest-cli.json:{ "compilerOptions": { "plugins": [ { "name": "nest-responses-generator-plugin", "options": { ... } } ] } }
- TypeScript compilation errors: Make sure your service methods have explicit return types
- Generated files not updating: Check that the plugin has write permissions to the output directory
- Import errors: Ensure the generated files are not manually edited (they should be auto-generated only)
This monorepo uses automated publishing with GitHub Actions and manual publishing scripts.
The repository is configured with smart auto-publishing that detects package changes and publishes them automatically.
- Automatic Detection: Pushes to
mainbranch automatically detect which packages changed - Smart Versioning: Auto-bumps versions if current version exists on npm
- Build & Test: Runs build and tests before publishing
- Git Integration: Commits version bumps back and creates tags
1. Add NPM Token to GitHub Secrets:
- Generate automation token at https://www.npmjs.com/settings/tokens
- Go to repository Settings β Secrets and variables β Actions
- Add new secret:
NPM_TOKENwith your token value
2. Push Changes:
# Make changes to any package
git add packages/plugin/src/new-feature.ts
git commit -m "feat: add new feature"
git push origin main
# Workflow automatically:
# β Detects changes in plugin package
# β Builds and tests the package
# β Bumps version if needed
# β Publishes to npm
# β Commits version bump back
# β Creates git tag3. Manual Trigger:
- Go to Actions tab β Auto Publish Packages β Run workflow
- Choose package name, version type, and options
- β Smart package change detection
- β Automatic version bumping (patch/minor/major)
- β Build validation before publishing
- β Git tags for published versions
- β Manual trigger with custom options
- β Parallel publishing for multiple packages
For local development and testing:
# Publish plugin with current version
pnpm run publish:plugin
# Test publishing (dry run)
pnpm run publish:plugin:dry
# Publish with version bump
pnpm run publish:plugin:patch # 0.0.1 β 0.0.2
pnpm run publish:plugin:minor # 0.0.1 β 0.1.0
pnpm run publish:plugin:major # 0.0.1 β 1.0.0# Publish any package by name
node scripts/publish-package.js <package-name> [version-type]
# Examples:
node scripts/publish-package.js nest-responses-generator-plugin
node scripts/publish-package.js nest-responses-generator-plugin patch
node scripts/publish-package.js nest-responses-generator-plugin --dry-runThe automated script:
- β Validates package exists in workspace
- β Updates version (if specified)
- β Builds the package
- β
Runs
prepublishOnlyscript - β Publishes to npm with public access
- β Handles authentication and git checks
To add a new publishable package to the monorepo:
-
Create package structure:
packages/your-package/ βββ package.json βββ src/ βββ ... -
Configure package.json:
{ "name": "your-package-name", "version": "0.0.1", "main": "dist/index.js", "types": "dist/index.d.ts", "files": ["dist/**/*"], "scripts": { "build": "tsc", "prepublishOnly": "npm run build" } } -
Add convenience scripts to root package.json:
{ "scripts": { "publish:your-package": "node scripts/publish-package.js your-package-name", "publish:your-package:dry": "node scripts/publish-package.js your-package-name --dry-run" } } -
The GitHub workflow will automatically detect and publish changes!
# Verify latest version on npm
npm view nest-responses-generator-plugin version
# Install published package
npm install nest-responses-generator-plugin@latest- Go to Actions tab in repository
- View workflow runs and their status
- Check detailed logs for troubleshooting
- β Use "Automation" token type for CI/CD
- β Store token in GitHub repository secrets only
- β Use descriptive token names
- β Never commit tokens to code
- β Never share tokens in logs or files
- π Patch (0.0.X): Bug fixes and small changes
- π Minor (0.X.0): New features, backward compatible
- π Major (X.0.0): Breaking changes
- π All changes go through main branch
- π·οΈ Automatic git tags for releases
- πΎ Version bumps committed back to repository
- π Clean git history maintained
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is MIT licensed.
- Built on top of the excellent NestJS framework
- Integrates seamlessly with @nestjs/swagger
- Inspired by the need to reduce boilerplate in API development
- Thanks to the TypeScript team for the powerful compiler API
π Transform your NestJS API development today! No more response type duplication, just pure productivity.
Happy coding! π