Documentação OpenAPI com Scalar

Como configurar e personalizar a documentação interativa em /doc com Scalar (OpenAPI via @nestjs/swagger).

Projetos gerados pelo Koala Nest expõem documentação interativa em /doc. O fluxo é em duas etapas:

  1. @nestjs/swagger — gera o spec OpenAPI a partir dos controllers e DTOs.
  2. @scalar/nestjs-api-reference — renderiza a UI do Scalar com esse spec.

O Swagger UI padrão fica desabilitado; apenas o Scalar é exibido.

Arquivo Função
src/host/open-api/define-documentation.ts Monta o spec e registra o Scalar
src/host/main.ts Chama defineDocumentation(app) no bootstrap
src/host/decorators/controller.decorator.ts Aplica rota Nest + tag Swagger
src/host/decorators/api-exclude-endpoint-diff-develop.decorator.ts Oculta endpoint fora de develop
src/host/decorators/api-property-only-develop.decorator.ts Documenta propriedade de DTO apenas em develop
src/host/decorators/api-property-enum.decorator.ts Documenta enums numéricos no Swagger
src/host/decorators/scalar-token-endpoint.decorator.ts Compõe decorators da rota scalar-token
Requests/responses em src/application/ Schemas documentados com @ApiProperty()
src/core/schemas/ Facilitadores Zod reutilizáveis (booleanSchema, nativeEnumSchema, etc.)
typescript
import { ApiExcludeEndpointDiffDevelop } from '@/host/decorators/api-exclude-endpoint-diff-develop.decorator';

@Delete(':id')
@ApiExcludeEndpointDiffDevelop()
handle(@Param('id') id: string) { ... }

Fora de NODE_ENV=develop, o endpoint deixa de aparecer no Scalar. Use @ApiPropertyOnlyDevelop() em DTOs internos e @ApiPropertyEnum({ enum: MyEnum }) para enums numéricos.

typescript
import { ApiPropertyOnlyDevelop } from '@/host/decorators/api-property-only-develop.decorator';
import { ApiPropertyEnum } from '@/host/decorators/api-property-enum.decorator';

export class MyRequest {
  @ApiPropertyOnlyDevelop({ example: 'internal-only' })
  debugField?: string;

  @ApiPropertyEnum({ enum: StatusEnum })
  status: StatusEnum;
}

Em src/core/schemas/:

Helper Uso
booleanSchema() Query booleans (?active=true)
nativeEnumSchema(enum) Enums numéricos em query/body
emailSchema(value, required?) Validação de e-mail
documentNumberSchema(value) CPF/CNPJ, incluindo CNPJ alfanumérico (via @koalarx/utils)
setMaskDocumentNumber(value) Máscara de documento (via @koalarx/utils/KlString)
LIST_QUERY_SCHEMA Paginação e ordenação

Exemplo em validators:

typescript
import { booleanSchema, LIST_QUERY_SCHEMA } from '@/core/schemas';

protected get schema() {
  return LIST_QUERY_SCHEMA.and(
    z.object({ active: booleanSchema() }),
  );
}

Em src/host/main.ts, após criar a aplicação:

typescript
import { defineDocumentation } from './open-api/define-documentation';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // applyHttpMiddleware(app) — ver Middleware HTTP

  defineDocumentation(app);

  await app.listen(process.env.PORT || 3000);
}

Com o servidor rodando:

text
http://localhost:3000/doc

A porta segue a variável PORT do .env.

Toda a personalização do Scalar começa em define-documentation.ts:

typescript
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import packageJson from '../../../package.json';
import { apiReference } from '@scalar/nestjs-api-reference';

export function defineDocumentation(app: INestApplication) {
  const documentBuilder = new DocumentBuilder()
    .setTitle(packageJson.name)
    .setVersion(packageJson.version)
    .build();

  const document = SwaggerModule.createDocument(app, documentBuilder);
  const docEndpoint = '/doc';

  SwaggerModule.setup(docEndpoint, app, document, { swaggerUiEnabled: false });

  app.use(
    docEndpoint,
    apiReference({
      spec: { content: document },
      metaData: {
        title: packageJson.name,
        version: packageJson.version,
      },
      hideModels: true,
      hideDownloadButton: true,
      hideClientButton: true,
      hiddenClients: [
        'libcurl',
        'clj_http',
        'restsharp',
        'native',
        'http1.1',
        'asynchttp',
        'nethttp',
        'okhttp',
        'unirest',
        'xhr',
        'request',
        'nsurlsession',
        'cohttp',
        'guzzle',
        'http1',
        'http2',
        'webrequest',
        'restmethod',
        'requests',
        'httr',
        'httpie',
        'wget',
        'undici',
      ],
    }),
  );
}

Use o DocumentBuilder do NestJS Swagger para definir o cabeçalho do spec:

Método Uso
setTitle() Nome exibido no Scalar (padrão: name do package.json)
setDescription() Texto introdutório da API (opcional)
setVersion() Versão do spec (padrão: package.json)

Exemplos de extensão:

typescript
const documentBuilder = new DocumentBuilder()
  .setTitle('Minha API')
  .setDescription('API interna do produto X')
  .setVersion('1.2.0')
  .addServer('http://localhost:3000', 'Desenvolvimento')
  .addServer('https://api.exemplo.com', 'Produção')
  .build();

Com autenticação instalada, define-documentation.ts aplica .addBearerAuth() e .addSecurityRequirements('bearer') globalmente. Rotas com @IsPublic() ficam sem segurança no spec — o mesmo critério do AuthGuard global.

O template usa /doc por padrão (DOC_ENDPOINT em define-documentation.ts). Para mudar a URL:

typescript
const docEndpoint = '/doc'; // padrão do template
// const docEndpoint = '/api-docs'; // exemplo de URL alternativa

Registre o mesmo valor em SwaggerModule.setup e em app.use.

Opção Valor no template Efeito
spec.content documento gerado pelo Swagger Alimenta a UI com o OpenAPI
metaData title, description, version Cabeçalho exibido no Scalar
hideModels true Oculta painel de schemas/models
hideDownloadButton true Remove botão de download do spec
hideClientButton true Remove seletor de client HTTP
hiddenClients lista de clientes Esconde geradores de código (curl, fetch, etc.)

Para exibir models ou o botão de download, passe false nas flags correspondentes.

O template oculta clientes HTTP porque a documentação é voltada a testar endpoints na própria UI, não a gerar snippets em outras linguagens.

typescript
SwaggerModule.setup(docEndpoint, app, document, { swaggerUiEnabled: false });

O SwaggerModule ainda é necessário para gerar o documento OpenAPI — apenas a interface padrão do Swagger fica desligada.

O Scalar não declara rotas manualmente. Tudo vem dos controllers registrados nos módulos Nest e dos decoradores Swagger neles.

O decorador @Controller do Koala Nest aplica rota e tag automaticamente:

typescript
// src/host/decorators/controller.decorator.ts
export function Controller(config: RouterConfigBase) {
  return function (target: any) {
    NestController(config.group)(target);
    ApiTags(config.tag)(target);
  };
}

Configuração do recurso:

typescript
class PersonRouterConfig extends RouterConfigBase {
  constructor() {
    super('Person', '/person'); // tag, prefixo
  }
}

export const PERSON_ROUTER_CONFIG = new PersonRouterConfig();

Todos os controllers de Person usam @Controller(PERSON_ROUTER_CONFIG) e aparecem agrupados na tag Person em /doc.

Documente cada operação no controller:

typescript
@Post()
@ApiCreatedResponse({ type: CreatePersonResponse })
@HttpCode(HttpStatus.CREATED)
handle(@Body() request: CreatePersonRequest): Promise<CreatePersonResponse> {
  return this.handler.handle(request);
}
typescript
@Get(':id')
@ApiOkResponse({ type: ReadPersonResponse })
async handle(@Param('id') id: string): Promise<ReadPersonResponse> {
  return await this.handler.handle(+id);
}

Para respostas vazias (update/delete), @ApiOkResponse() sem type é suficiente.

O Scalar infere query params a partir da classe usada em @Query():

typescript
@Get()
@ApiOkResponse({ type: ReadManyPersonResponse })
async handle(@Query() query: ReadManyPersonRequest): Promise<ReadManyPersonResponse> {
  return await this.handler.handle(query);
}

As propriedades de ReadManyPersonRequest (incluindo as herdadas de PaginationRequest) aparecem como parâmetros de query na documentação.

Requests e responses devem usar @ApiProperty() em cada campo exposto:

typescript
export class CreatePersonRequest extends ObjectClass<CreatePersonRequest> {
  @ApiProperty({ example: 'John Doe' })
  @AutoMap()
  name: string;

  @ApiProperty({ type: CreatePersonAddressRequest })
  @AutoMap()
  address: CreatePersonAddressRequest;

  @ApiProperty({ type: CreatePersonContactRequest, isArray: true })
  @AutoMap({ type: () => CreatePersonContactRequest })
  contacts: CreatePersonContactRequest[];
}

Boas práticas:

  • use example para facilitar o "Try it" no Scalar;
  • use type ou isArray: true em objetos aninhados e listas;
  • em campos opcionais, required: false (padrão em query params);
  • mantenha @ApiProperty() alinhado ao que o validator Zod realmente aceita.

Entidades de domínio (src/domain/entities/) não precisam de @ApiProperty() — apenas DTOs de entrada/saída na camada application.

Ao criar um recurso, a documentação é atualizada automaticamente se você:

  1. Criar router.config.ts com tag e prefixo.
  2. Usar @Controller(RECURSO_ROUTER_CONFIG) em cada controller.
  3. Decorar requests/responses com @ApiProperty().
  4. Decorar handlers HTTP com @ApiOkResponse, @ApiCreatedResponse, etc.
  5. Registrar os controllers no módulo Nest (<Recurso>Module).

Não é necessário editar define-documentation.ts por recurso.

Edite o DocumentBuilder em define-documentation.ts. A versão pode continuar vindo do package.json ou ser fixa.

Altere docEndpoint e reinicie o servidor.

typescript
apiReference({
  // ...
  hideModels: false,
});
typescript
apiReference({
  // ...
  hideDownloadButton: false,
});
typescript
.addServer(process.env.API_URL ?? 'http://localhost:3000', 'API')

Defina API_URL no envSchema se quiser configurar por ambiente.

Com o módulo de autenticação instalado, o template configura o Scalar para obter o JWT pela própria UI — sem copiar token manualmente. Os esquemas OAuth2 são montados no documento OpenAPI via define-documentation.ts.

Arquivo / rota Função
src/host/open-api/define-documentation.ts Monta esquemas OAuth2 no OpenAPI e configura o Scalar quando IJwtTokenService está registrado
POST /auth/login Fluxo password usado pelo Scalar (JWT)
POST /oauth2/scalar-token Fluxo authorization code usado pelo Scalar (OAuth2)
GET /sso/callback Callback OAuth para o Scalar (code + state na query)

Endpoints auxiliares ficam ocultos fora de develop (@ApiExcludeEndpointDiffDevelop).

O Scalar exibe o esquema JWT com fluxo password:

  • username → e-mail do usuário (User.email)
  • password → senha do usuário
  • tokenUrlPOST /auth/login
  • refreshUrlPOST /auth/refresh
  • x-tokenNameaccessToken (Scalar envia Bearer automaticamente)

Em develop, credenciais de exemplo são pré-preenchidas (admin@example.com / admin123). Em produção, os campos ficam vazios.

Uso em /doc:

  1. Abra /doc
  2. Clique em Authenticate
  3. Selecione JWT, confirme username/password e autorize
  4. Use Try it nos endpoints protegidos

Para cada provider em OAUTH2_PROVIDERS, o template registra um esquema (ex.: auth0):

  • authorizationUrl — link completo gerado por OAuth2AuthService.authLink()
  • tokenUrlPOST /oauth2/scalar-token (troca code, emite JWT e devolve { accessToken, refreshToken })
  • x-scalar-client-id, x-scalar-redirect-uri, x-scalar-security-body — extensões do Scalar para o fluxo authorization code
  • x-tokenName: accessToken

Uso em /doc:

  1. Authenticate → selecione o provider (ex.: auth0)
  2. Conclua o login no provedor
  3. O Scalar troca o código, recebe o JWT e aplica nos requests

A lógica fica em buildDocAuthorizations(app) dentro de define-documentation.ts:

typescript
// Esquema JWT (password → /auth/login) quando LoginController existe
// Esquemas por provider OAuth2 (authorization code → /oauth2/scalar-token)

documentBuilder.addBearerAuth(authorization.config, authorization.name);
syncIsPublicRoutesInOpenApi(document);
applyAuthSecurityToProtectedRoutes(document, schemeNames);

Rotas com @IsPublic() recebem security: [] no spec; demais operações herdam os esquemas registrados.

typescript
import { IsPublic } from '@/host/decorators/is-public.decorator';

@Post('token')
@IsPublic()
handle() { ... }
Extensão Função
x-tokenName Campo da resposta usado como Bearer (accessToken)
x-scalar-client-id Pré-preenche Client ID
x-scalar-redirect-uri URI de callback do authorization code
x-scalar-security-body Body extra no tokenUrl (ex.: provider)

Referência: Scalar — Authentication.

Se um endpoint não aparece em /doc:

  • o controller está registrado em controllers do módulo Nest?
  • o módulo está importado no AppModule?
  • o método HTTP (@Get, @Post, etc.) está no controller?
  • a aplicação reiniciou após as alterações?

Se o schema de um campo está incompleto:

  • falta @ApiProperty() na propriedade?
  • objetos aninhados precisam de type: ClasseFilha?
  • arrays precisam de isArray: true?

Koala Nest

Facilitador para criar APIs NestJS com arquitetura DDD. Código copiado para o seu repositório — legível, adaptável e sob seu controle.

Creator

igordrangel.com.br

Design, back-end e estratégia de produto.

Comandos rápidos

CLI global e scripts no projeto gerado

  • bun install -g @koalarx/nest
  • kl-nest new
  • kl-nest add cache
  • bun run migration:run # template CRUD
  • kl-nest --help
© 2026 Koala NestFeito para desenvolvedores NestJS e fluxos assistidos por IA.