Fix: NestJS Swagger UI Not Showing — /api-docs Returns 404 or Blank Page
Quick Answer
How to fix NestJS Swagger UI not displaying — SwaggerModule setup, DocumentBuilder, decorators not appearing, guards blocking the docs route, and Fastify vs Express differences.
The Problem
NestJS Swagger UI returns a 404 or blank page:
GET /api-docs → 404 Not Found
GET /api-docs → 200 OK but empty page (no operations listed)Or the Swagger UI loads but shows no endpoints:
{
"openapi": "3.0.0",
"paths": {},
"info": { "title": "API", "version": "1.0" }
}Or decorators like @ApiProperty() aren’t reflected in the schema:
export class CreateUserDto {
@ApiProperty({ description: 'User email' })
email: string;
// email doesn't appear in Swagger UI request body schema
}Or Swagger works in development but breaks in production.
Why This Happens
NestJS Swagger relies on several pieces working together. Common failure points:
SwaggerModule.setup()not called — the setup call creates the/api-docsroute. Without it, the route doesn’t exist.- Setup called after
app.listen()— Swagger must be set up before the app starts listening. Afterlisten(), the route map is frozen. @nestjs/swaggernot installed or wrong version — the package must be installed and compatible with the NestJS version.- Guards or middleware blocking the docs route — a global
AuthGuardor rate limiter applied before Swagger setup can block the/api-docsroute. - Fastify adapter requires different static assets setup — the default setup works for Express; Fastify needs
@fastify/staticinstalled separately. emitDecoratorMetadatadisabled — without this TypeScript option,@ApiProperty()and similar decorators don’t emit type information that Swagger reads.
Fix 1: Correct SwaggerModule Setup
The Swagger module must be set up in main.ts before app.listen():
// main.ts — CORRECT setup
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Set up Swagger BEFORE app.listen()
const config = new DocumentBuilder()
.setTitle('My API')
.setDescription('API documentation')
.setVersion('1.0')
.addBearerAuth() // Adds Authorization header to UI
.addTag('users')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
// Swagger UI available at: http://localhost:3000/api-docs
// OpenAPI JSON at: http://localhost:3000/api-docs-json
await app.listen(3000); // AFTER SwaggerModule.setup()
}
bootstrap();WRONG — setup after listen:
// WRONG ORDER
await app.listen(3000); // Routes frozen here
SwaggerModule.setup('api-docs', app, document); // Too late — route not registeredVerify the route is accessible:
curl http://localhost:3000/api-docs-json
# Should return OpenAPI JSON with your endpoints
curl http://localhost:3000/api-docs
# Should return HTML for Swagger UIFix 2: Install Required Packages
# Install swagger packages
npm install @nestjs/swagger
# swagger-ui-express is required for Express (default NestJS adapter)
npm install swagger-ui-express
# For Fastify:
npm install @fastify/static fastify-swagger
# Verify installed versions
npm list @nestjs/swagger swagger-ui-expressCheck version compatibility:
// package.json — compatible versions (as of 2026)
{
"@nestjs/common": "^10.x",
"@nestjs/swagger": "^7.x",
"swagger-ui-express": "^5.x"
}Fix 3: Enable TypeScript Decorator Metadata
Without emitDecoratorMetadata, Swagger can’t read type information from decorators:
// tsconfig.json — required options
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // ← Required for @ApiProperty() to work
"strictPropertyInitialization": false
}
}After enabling, verify @ApiProperty() decorators emit type info:
export class CreateUserDto {
@ApiProperty({
description: 'User email address',
example: '[email protected]',
})
email: string;
@ApiProperty({
description: 'User age',
minimum: 0,
maximum: 120,
example: 30,
})
age: number;
@ApiPropertyOptional({ // For optional fields
description: 'User bio',
})
bio?: string;
}Fix 4: Fix Guards Blocking the Swagger Route
A global AuthGuard blocks unauthenticated access to Swagger UI. Exclude the docs route:
// main.ts — exclude Swagger from global guard
import { NestFactory, Reflector } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { APP_GUARD } from '@nestjs/core';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Option 1 — use @Public() decorator on the swagger endpoint
// (requires setting up a custom guard that checks for @Public())
// Option 2 — don't apply global guard to docs path
// Use route-level guards instead of global guards for APIs that need public docs
}Better approach — use @Public() decorator with a custom guard:
// auth/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// auth/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
}For Swagger itself — in main.ts, set up Swagger routes before applying global guards, or explicitly exclude swagger paths in middleware:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Apply global prefix to API routes only (not swagger)
app.setGlobalPrefix('api', {
exclude: ['api-docs', 'api-docs-json', 'api-docs(.*)'],
});
// Set up Swagger
const config = new DocumentBuilder().setTitle('API').setVersion('1.0').build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document, {
swaggerOptions: {
persistAuthorization: true, // Keeps auth token between page reloads
},
});
await app.listen(3000);
}Fix 5: Fastify Adapter Setup
The default Swagger setup works with Express. Fastify requires additional configuration:
npm install @nestjs/platform-fastify @fastify/static// main.ts — Fastify adapter
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
const config = new DocumentBuilder()
.setTitle('API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
await app.listen(3000, '0.0.0.0');
}
bootstrap();Fix 6: Annotate Controllers and DTOs
Swagger only shows endpoints with proper decorators. Endpoints without @ApiTags, @ApiOperation, or response decorators may still appear, but DTOs need @ApiProperty to show in request bodies:
// users.controller.ts — complete Swagger annotations
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiBearerAuth,
ApiBody,
} from '@nestjs/swagger';
@ApiTags('users') // Groups endpoints under "users" in Swagger UI
@ApiBearerAuth() // Shows lock icon — requires Authorization header
@Controller('users')
export class UsersController {
@Get()
@ApiOperation({ summary: 'Get all users', description: 'Returns paginated user list' })
@ApiResponse({ status: 200, description: 'Users retrieved', type: [UserResponseDto] })
@ApiResponse({ status: 401, description: 'Unauthorized' })
findAll() {
return this.usersService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID' })
@ApiParam({ name: 'id', type: 'number', description: 'User ID' })
@ApiResponse({ status: 200, type: UserResponseDto })
@ApiResponse({ status: 404, description: 'User not found' })
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Post()
@ApiOperation({ summary: 'Create new user' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, type: UserResponseDto })
@ApiResponse({ status: 400, description: 'Validation error' })
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}DTO with full Swagger annotations:
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({
description: 'User email address',
example: '[email protected]',
format: 'email',
})
email: string;
@ApiProperty({
description: 'Password (min 8 chars)',
example: 'SecurePass123!',
minLength: 8,
})
password: string;
@ApiPropertyOptional({
description: 'Display name',
example: 'Alice Smith',
})
name?: string;
@ApiProperty({
description: 'User role',
enum: ['admin', 'user', 'guest'],
default: 'user',
})
role: 'admin' | 'user' | 'guest';
}Fix 7: Disable Swagger in Production
Swagger UI should typically be disabled in production (or protected):
// main.ts — conditional Swagger setup
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Only enable Swagger in non-production environments
if (process.env.NODE_ENV !== 'production') {
const config = new DocumentBuilder()
.setTitle('API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
console.log(`Swagger UI: http://localhost:${port}/api-docs`);
}
const port = process.env.PORT || 3000;
await app.listen(port);
}Or protect it with basic auth in staging:
import * as basicAuth from 'express-basic-auth';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Protect Swagger with basic auth
app.use(
['/api-docs', '/api-docs-json'],
basicAuth({
challenge: true,
users: {
[process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
},
}),
);
const config = new DocumentBuilder().setTitle('API').setVersion('1.0').build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api-docs', app, document);
await app.listen(3000);
}Still Not Working?
paths: {} in the JSON output — if the OpenAPI JSON shows empty paths, your controllers aren’t being discovered. Make sure they’re imported in AppModule (or the relevant module). Controllers not registered in any module won’t appear in Swagger.
Circular dependency in DTOs — if DTO A references DTO B which references DTO A, Swagger’s schema generation may produce an empty or partial schema. Use () => RelatedDto (lazy reference) with @ApiProperty({ type: () => RelatedDto }).
@ApiHideProperty() — fields decorated with @ApiHideProperty() are intentionally excluded from Swagger. Check if this decorator was accidentally applied.
Global prefix — if you set app.setGlobalPrefix('api'), your routes become /api/users etc. Swagger’s setup path is unaffected by the global prefix, but the paths shown in the UI will include the prefix. Ensure the prefix is set before SwaggerModule.setup().
For related NestJS issues, see Fix: NestJS ValidationPipe Not Working and Fix: NestJS Guard Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NestJS Nest can't resolve dependencies — Provider Not Found Error
How to fix NestJS dependency injection errors — module imports, provider exports, circular dependencies, dynamic modules, and the most common 'can't resolve dependencies' patterns.
Fix: NestJS ValidationPipe Not Working — class-validator Decorators Ignored
How to fix NestJS ValidationPipe not validating requests — global pipe setup, class-transformer, whitelist and transform options, custom validators, and DTO inheritance issues.
Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called
How to fix NestJS guards not working — applying guards globally vs controller vs method level, JWT AuthGuard, metadata with Reflector, public routes, and guard execution order.
Fix: NestJS Interceptor Not Triggered — Interceptors Not Running
How to fix NestJS interceptors not being called — global vs controller vs method binding, response transformation, async interceptors, execution context, and interceptor ordering.