Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 139 additions & 3 deletions docs/guide/nestjs-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ Tspec parses the following NestJS decorators:
### Swagger Decorators
- `@ApiTags(...tags)` - Adds tags to all operations in the controller
- `@ApiResponse({ status, description?, type? })` - Defines response status codes and types
- `@ApiBearerAuth(name?)` - Adds Bearer token authentication to the operation
- `@ApiBasicAuth(name?)` - Adds Basic authentication to the operation
- `@ApiOAuth2(scopes[], name?)` - Adds OAuth2 authentication with scopes
- `@ApiSecurity(name, scopes?)` - Adds custom security scheme

## JSDoc Support

Expand All @@ -316,9 +320,8 @@ findAll(): Promise<Book[]> {
The current NestJS integration has some limitations:

1. **Type inference**: Complex generic types may not be fully resolved
2. **Custom decorators**: Only standard NestJS decorators are supported
3. **Validation decorators**: `class-validator` decorators are not parsed
4. **Interceptors/Guards**: These are not reflected in the generated spec
2. **Validation decorators**: `class-validator` decorators are not parsed
3. **Interceptors/Guards**: These are not reflected in the generated spec

::: tip
For more advanced use cases, consider using the standard Tspec approach with `Tspec.DefineApiSpec` alongside your NestJS controllers.
Expand Down Expand Up @@ -359,6 +362,7 @@ console.log(JSON.stringify(spec, null, 2));
| `openapi.description` | `string` | - | API description |
| `openapi.securityDefinitions` | `object` | - | Security schemes |
| `openapi.servers` | `array` | - | Server URLs |
| `openapi.authDecorators` | `object` | - | Map custom decorator names to security scheme names |

## Using @ApiResponse

Expand Down Expand Up @@ -415,6 +419,138 @@ responses:
When `@ApiResponse` decorators are present, they override the default response generation based on return type.
:::

## Security Decorators

Tspec supports security decorators from `@nestjs/swagger` to add authentication requirements to your API operations.

### Using @ApiBearerAuth

Add Bearer token authentication to endpoints:

```ts
import { Controller, Get } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';

@Controller('users')
export class UsersController {
@Get('me')
@ApiBearerAuth('bearerAuth') // Uses 'bearerAuth' security scheme
getCurrentUser(): Promise<User> {
// This endpoint requires Bearer token authentication
}

@Get('profile')
@ApiBearerAuth() // Uses default 'bearer' security scheme
getProfile(): Promise<UserProfile> {
// ...
}
}
```

Make sure to define the security scheme in your config:

```json
{
"nestjs": true,
"openapi": {
"securityDefinitions": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
```

This generates:

```yaml
paths:
/users/me:
get:
security:
- bearerAuth: []
```

### Using @ApiOAuth2

Add OAuth2 authentication with scopes:

```ts
@Get('admin')
@ApiOAuth2(['read', 'write'], 'oauth2Auth')
getAdminData(): Promise<AdminData> {
// Requires OAuth2 with read and write scopes
}
```

### Custom Auth Decorators

If you use composite decorators that wrap security decorators (e.g., using `applyDecorators`), you can configure Tspec to recognize them:

```ts
// auth.decorator.ts
import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from './auth.guard';

export function Auth(): MethodDecorator & ClassDecorator {
return applyDecorators(
UseGuards(AuthGuard),
ApiBearerAuth('access-token'),
);
}

export function AdminAuth(): MethodDecorator & ClassDecorator {
return applyDecorators(
UseGuards(AuthGuard, AdminGuard),
ApiBearerAuth('access-token'),
);
}
```

Configure `authDecorators` in your tspec config to map custom decorators to security schemes:

```json
{
"nestjs": true,
"openapi": {
"securityDefinitions": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
},
"authDecorators": {
"Auth": "bearerAuth",
"AdminAuth": "bearerAuth"
}
}
}
```

Now your custom decorators will be recognized:

```ts
@Controller('users')
export class UsersController {
@Get('me')
@Auth() // ✅ Recognized as bearerAuth
getCurrentUser(): Promise<User> {
// ...
}

@Get('admin')
@AdminAuth() // ✅ Recognized as bearerAuth
getAdminData(): Promise<AdminData> {
// ...
}
}
```

## Using @ApiTags

Tspec supports the `@ApiTags` decorator from `@nestjs/swagger` to organize your API operations:
Expand Down
1 change: 1 addition & 0 deletions packages/tspec/src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const generateTspec = async (
const app = parseNestControllers({
tsconfigPath: params.tsconfigPath || 'tsconfig.json',
controllerGlobs: params.specPathGlobs || ['src/**/*.controller.ts'],
authDecorators: params.openapi?.authDecorators,
});

logger.log(`Found ${app.controllers.length} controller(s)`);
Expand Down
1 change: 1 addition & 0 deletions packages/tspec/src/nestjs/openapiGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,5 +275,6 @@ const buildOperation = (
parameters: parameters.length > 0 ? parameters : undefined,
requestBody,
responses,
security: method.security,
};
};
92 changes: 89 additions & 3 deletions packages/tspec/src/nestjs/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const PARAM_DECORATORS = ['Param', 'Query', 'Body', 'Headers'];
const FILE_DECORATORS = ['UploadedFile', 'UploadedFiles'];

export const parseNestControllers = (options: NestParserOptions): ParsedNestApp => {
const { tsconfigPath, controllerGlobs } = options;
const { tsconfigPath, controllerGlobs, authDecorators } = options;

const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (configFile.error) {
Expand Down Expand Up @@ -49,7 +49,7 @@ export const parseNestControllers = (options: NestParserOptions): ParsedNestApp

ts.forEachChild(sourceFile, (node) => {
if (ts.isClassDeclaration(node)) {
const controller = parseController(node, checker, sourceFile);
const controller = parseController(node, checker, sourceFile, authDecorators);
if (controller) {
controllers.push(controller);
collectImports(sourceFile, imports);
Expand Down Expand Up @@ -279,6 +279,7 @@ const parseController = (
node: ts.ClassDeclaration,
checker: ts.TypeChecker,
sourceFile: ts.SourceFile,
authDecorators?: Record<string, string>,
): NestControllerMetadata | null => {
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
if (!decorators) return null;
Expand All @@ -301,7 +302,7 @@ const parseController = (

node.members.forEach((member) => {
if (ts.isMethodDeclaration(member)) {
const method = parseMethod(member, checker);
const method = parseMethod(member, checker, authDecorators);
if (method) {
methods.push(method);
}
Expand Down Expand Up @@ -380,6 +381,86 @@ const parseApiResponses = (
return responses;
};

// Security decorator names and their default security scheme names
const SECURITY_DECORATORS: Record<string, string> = {
ApiBearerAuth: 'bearer',
ApiBasicAuth: 'basic',
ApiOAuth2: 'oauth2',
ApiSecurity: '', // Uses first argument as security name
};

// Parse security decorators (@ApiBearerAuth, @ApiBasicAuth, @ApiOAuth2, @ApiSecurity, and custom decorators)
const parseApiSecurity = (
decorators: readonly ts.Decorator[],
authDecorators?: Record<string, string>,
): Array<Record<string, string[]>> => {
const security: Array<Record<string, string[]>> = [];

// Merge built-in security decorators with custom authDecorators
const allSecurityDecorators = { ...SECURITY_DECORATORS, ...authDecorators };

for (const decorator of decorators) {
if (!ts.isCallExpression(decorator.expression)) continue;
if (!ts.isIdentifier(decorator.expression.expression)) continue;

const decoratorName = decorator.expression.expression.text;

// Check if it's a custom auth decorator (from authDecorators config)
if (authDecorators && decoratorName in authDecorators) {
const securityName = authDecorators[decoratorName];
security.push({ [securityName]: [] });
continue;
}

// Check if it's a built-in security decorator
if (!(decoratorName in SECURITY_DECORATORS)) continue;

const args = decorator.expression.arguments;
let securityName: string;
let scopes: string[] = [];

if (decoratorName === 'ApiSecurity') {
// @ApiSecurity('securityName', ['scope1', 'scope2'])
if (args.length === 0) continue;
const firstArg = args[0];
if (!ts.isStringLiteral(firstArg)) continue;
securityName = firstArg.text;

// Parse scopes if provided
if (args.length > 1 && ts.isArrayLiteralExpression(args[1])) {
scopes = args[1].elements
.filter((el): el is ts.StringLiteral => ts.isStringLiteral(el))
.map((el) => el.text);
}
} else if (decoratorName === 'ApiOAuth2') {
// @ApiOAuth2(['scope1', 'scope2'], 'securityName')
// First arg is scopes array, second arg is optional security name
if (args.length > 0 && ts.isArrayLiteralExpression(args[0])) {
scopes = args[0].elements
.filter((el): el is ts.StringLiteral => ts.isStringLiteral(el))
.map((el) => el.text);
}
if (args.length > 1 && ts.isStringLiteral(args[1])) {
securityName = args[1].text;
} else {
securityName = SECURITY_DECORATORS[decoratorName];
}
} else {
// @ApiBearerAuth('securityName') or @ApiBasicAuth('securityName')
// First argument is optional security scheme name
if (args.length > 0 && ts.isStringLiteral(args[0])) {
securityName = args[0].text;
} else {
securityName = SECURITY_DECORATORS[decoratorName];
}
}

security.push({ [securityName]: scopes });
}

return security;
};

// Parse @ApiTags decorator to extract tag names
const parseApiTags = (
decorators: readonly ts.Decorator[],
Expand Down Expand Up @@ -427,6 +508,7 @@ const parseApiTags = (
const parseMethod = (
node: ts.MethodDeclaration,
checker: ts.TypeChecker,
authDecorators?: Record<string, string>,
): NestMethodMetadata | null => {
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
if (!decorators) return null;
Expand Down Expand Up @@ -463,6 +545,9 @@ const parseMethod = (
// Parse @ApiResponse decorators
const responses = parseApiResponses(decorators, checker);

// Parse security decorators (@ApiBearerAuth, @ApiBasicAuth, @ApiOAuth2, @ApiSecurity, and custom decorators)
const security = parseApiSecurity(decorators, authDecorators);

return {
name: methodName,
httpMethod,
Expand All @@ -473,6 +558,7 @@ const parseMethod = (
summary,
tags: tags.length > 0 ? tags : undefined,
responses: responses.length > 0 ? responses : undefined,
security: security.length > 0 ? security : undefined,
};
};

Expand Down
8 changes: 8 additions & 0 deletions packages/tspec/src/nestjs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface NestMethodMetadata {
summary?: string;
tags?: string[];
responses?: NestApiResponse[];
/** Security requirements from @ApiBearerAuth, @ApiBasicAuth, @ApiOAuth2, @ApiSecurity decorators */
security?: Array<Record<string, string[]>>;
}

export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options' | 'head';
Expand All @@ -42,6 +44,12 @@ export interface NestParameterMetadata {
export interface NestParserOptions {
tsconfigPath: string;
controllerGlobs: string[];
/**
* Map custom decorator names to security scheme names.
* Useful for composite decorators that wrap @ApiBearerAuth, etc.
* @example { "Auth": "bearerAuth", "AdminAuth": "bearerAuth" }
*/
authDecorators?: Record<string, string>;
}

export interface ParsedNestApp {
Expand Down
Loading