Documenting Authentication
Most real APIs are protected, so your OpenAPI document needs to describe how a client authenticates and which routes require it. NestJS handles this in two halves: you register one or more security schemes on the DocumentBuilder when bootstrapping Swagger, then you mark protected routes with decorators like @ApiBearerAuth. Done correctly, Swagger UI grows an Authorize button that lets a developer paste a token once and have it attached to every subsequent “Try it out” request.
Registering security schemes
A security scheme is declared on the DocumentBuilder. Each add* method takes an optional name (the first argument) that links the scheme to the route decorators later. The most common schemes are JWT bearer tokens, OAuth2 flows, and API keys.
// main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Orders API')
.setVersion('1.0')
.addBearerAuth(
{
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Paste your JWT access token',
},
'access-token', // <- this name is referenced by @ApiBearerAuth('access-token')
)
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
If you call addBearerAuth() with no name, NestJS uses the default name bearer, and @ApiBearerAuth() with no argument matches it. Naming the scheme explicitly is clearer once more than one scheme exists.
Choosing a scheme type
The three DocumentBuilder methods below cover almost every API. They differ in where the credential lives and how Swagger UI prompts for it.
| Method | OpenAPI type | Where the credential goes | Use it for |
|---|---|---|---|
addBearerAuth() | http / bearer | Authorization: Bearer <token> header | JWT or opaque access tokens |
addOAuth2() | oauth2 | Authorization header after a flow | Delegated auth via an identity provider |
addApiKey() | apiKey | A named header, query param, or cookie | Static service-to-service keys |
addBasicAuth() | http / basic | Authorization: Basic <base64> header | Legacy username/password APIs |
API key scheme
An API key scheme tells Swagger which header (or query parameter) carries the key. The name field is the header name; the second argument is the scheme reference name used by the decorator.
.addApiKey(
{ type: 'apiKey', name: 'X-API-Key', in: 'header' },
'api-key',
)
OAuth2 scheme
For OAuth2 you describe the flow and its scopes. Swagger UI then renders the provider’s login form inside the Authorize dialog.
.addOAuth2({
type: 'oauth2',
flows: {
authorizationCode: {
authorizationUrl: 'https://auth.example.com/authorize',
tokenUrl: 'https://auth.example.com/token',
scopes: {
'orders:read': 'Read orders',
'orders:write': 'Create and update orders',
},
},
},
})
Marking protected routes
Registering a scheme only adds it to the document’s component list — it does not attach the requirement to any operation. Apply the matching decorator at the controller or handler level so Swagger knows the route needs that credential. The argument MUST equal the scheme name you registered.
// orders.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';
@ApiTags('orders')
@ApiBearerAuth('access-token') // applies to every route in this controller
@UseGuards(AuthGuard('jwt'))
@Controller('orders')
export class OrdersController {
constructor(private readonly orders: OrdersService) {}
@Get()
@ApiUnauthorizedResponse({ description: 'Missing or invalid bearer token' })
findAll() {
return this.orders.findAll();
}
@Post()
create(@Body() dto: CreateOrderDto) {
return this.orders.create(dto);
}
}
The corresponding decorators are @ApiBearerAuth('access-token'), @ApiOAuth2(['orders:read'], 'oauth2-name'), @ApiSecurity('api-key'), and @ApiBasicAuth(). Place them on a controller to protect all routes, or on individual handlers for finer control.
The string passed to
@ApiBearerAuth,@ApiSecurity, and friends must match the scheme name fromDocumentBuildercharacter for character. A typo produces no error — the lock icon simply never appears, which is a common and confusing gotcha.
Persisting the token in Swagger UI
By default Swagger UI forgets your token on a page refresh. Enable persistAuthorization so the Authorize value survives reloads during development. For OAuth2 you can also pre-fill the client ID.
SwaggerModule.setup('api', app, document, {
swaggerOptions: {
persistAuthorization: true,
oauth2RedirectUrl: 'http://localhost:3000/api/oauth2-redirect.html',
initOAuth: {
clientId: 'swagger-ui',
scopes: ['orders:read', 'orders:write'],
},
},
});
Verifying the security requirement
After wiring everything up, inspect the JSON spec to confirm the scheme is registered and the operation references it.
curl -s http://localhost:3000/api-json | head -n 18
Output:
{
"openapi": "3.0.0",
"components": {
"securitySchemes": {
"access-token": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": {
"/orders": {
"get": {
"security": [{ "access-token": [] }]
}
}
}
}
A padlock icon now appears next to each protected route in Swagger UI, and clicking Authorize lets you supply the token once for the whole session.
Best Practices
- Give every scheme an explicit name in
DocumentBuilderand reuse that exact name in the route decorators to avoid silent mismatches. - Apply
@ApiBearerAuthat the controller level when an entire resource is protected, rather than repeating it on each handler. - Keep the OpenAPI security requirement in sync with your real guards — the decorator documents intent but does not enforce anything.
- Document a
@ApiUnauthorizedResponse()(and@ApiForbiddenResponse()where relevant) so consumers know what an auth failure looks like. - Enable
persistAuthorizationfor local development, but never commit real tokens or client secrets intoinitOAuth. - Scope OAuth2 declarations to the minimum permissions each route needs so the generated client requests only what it uses.