OpenAPI 3.x vs Swagger 2.x
| Aspecto | Swagger 2.0 (OAS 2) | OpenAPI 3.0 / 3.1 |
|---|---|---|
| Formato de arquivo | swagger: "2.0" | openapi: "3.0.3" ou "3.1.0" |
| Hosts múltiplos | Apenas um host/basePath | servers[] com múltiplas URLs |
| Request Body | body param | requestBody separado |
| Exemplos | example simples | examples com múltiplos casos |
| Nullable | x-nullable: true | nullable: true (3.0) ou type: [string, null] (3.1) |
| Links | Não suportado | links para relacionar responses |
| Callbacks | Não suportado | callbacks para webhooks |
| JSON Schema | Subconjunto próprio | Alinhado com JSON Schema (3.1 é compatível 100%) |
| Componentes | definitions, parameters | components unificado |
| Content types | consumes/produces | content por operação |
Recomendação: Use sempre OpenAPI 3.0.3 ou 3.1.0 para projetos novos.
Estrutura do Documento
# openapi: versão da especificação
openapi: "3.0.3"
# info: metadados da API
info:
title: "API de Gerenciamento Financeiro"
description: |
API REST para gerenciamento de contas, transações e cartões.
## Autenticação
Todas as rotas protegidas requerem Bearer JWT.
version: "1.5.0"
contact:
name: "Rafael Marques"
email: "rafael@exemplo.com"
url: "https://meusite.com"
license:
name: "MIT"
url: "https://opensource.org/licenses/MIT"
# servers: URLs base da API
servers:
- url: "https://api.meusite.com/v1"
description: "Produção"
- url: "https://staging-api.meusite.com/v1"
description: "Staging"
- url: "http://localhost:8080/api/v1"
description: "Desenvolvimento local"
# paths: todos os endpoints
paths:
/accounts:
get:
# ... operação GET
post:
# ... operação POST
# components: schemas, security schemes e outros reutilizáveis
components:
schemas:
# ...
securitySchemes:
# ...
parameters:
# ...
responses:
# ...
# security: aplica globalmente (pode ser sobrescrito por operação)
security:
- bearerAuth: []
# tags: agrupa operações para organização
tags:
- name: accounts
description: "Gerenciamento de contas bancárias"
- name: transactions
description: "Transações financeiras"
- name: cards
description: "Cartões de crédito/débito"Paths e Operações
Estrutura Completa de um Path Item
paths:
/accounts/{accountId}:
# Parâmetros comuns a todas as operações deste path
parameters:
- $ref: '#/components/parameters/AccountIdParam'
get:
operationId: getAccountById # ID único — usado em code generation
summary: "Buscar conta por ID" # Título curto
description: | # Descrição longa (markdown suportado)
Retorna os detalhes completos de uma conta.
Requer que o usuário seja o dono da conta ou admin.
tags: [accounts] # Agrupamento
deprecated: false # Marca como depreciado
parameters:
- name: include
in: query
description: "Inclui dados relacionados"
schema:
type: array
items:
type: string
enum: [transactions, cards, balance]
style: form
explode: false # ?include=transactions,cards
responses:
'200':
description: "Conta encontrada"
content:
application/json:
schema:
$ref: '#/components/schemas/AccountResponse'
examples:
conta_corrente:
summary: "Conta corrente ativa"
value:
id: "acc_01HXYZ"
name: "Conta Principal"
type: "checking"
balance: 1500.00
currency: "BRL"
'404':
$ref: '#/components/responses/NotFound'
'401':
$ref: '#/components/responses/Unauthorized'
put:
operationId: updateAccount
summary: "Atualizar conta"
tags: [accounts]
requestBody:
required: true
description: "Dados para atualizar a conta"
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateAccountRequest'
examples:
rename:
summary: "Renomear conta"
value:
name: "Conta Poupança Principal"
responses:
'200':
description: "Conta atualizada"
content:
application/json:
schema:
$ref: '#/components/schemas/AccountResponse'
'400':
$ref: '#/components/responses/ValidationError'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteAccount
summary: "Deletar conta"
tags: [accounts]
security:
- bearerAuth: [admin] # Sobrescreve security global — requer scope admin
responses:
'204':
description: "Conta deletada com sucesso"
'409':
description: "Conta possui saldo ou transações pendentes"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'Parameters
Os parâmetros podem estar em 4 lugares:
Path Parameter
parameters:
- name: userId
in: path # obrigatório em path params
required: true # sempre true para path params
description: "ID único do usuário (ULID)"
schema:
type: string
pattern: '^[0-9A-Z]{26}$' # ULID format
example: "01HXYZ123456789ABCDEFG"Query Parameter
parameters:
- name: page
in: query
required: false
description: "Número da página (começa em 1)"
schema:
type: integer
minimum: 1
default: 1
example: 1
- name: size
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: sort
in: query
description: "Campo e direção de ordenação"
schema:
type: string
pattern: '^[a-zA-Z]+:(asc|desc)$'
example: "createdAt:desc"
- name: status
in: query
description: "Filtrar por múltiplos status"
schema:
type: array
items:
type: string
enum: [active, inactive, pending, closed]
style: form # ?status=active&status=pending (explode: true — padrão)
explode: true
# Alternativa: ?status=active,pending (explode: false)
- name: status
in: query
schema:
type: array
items:
type: string
style: form
explode: falseHeader Parameter
parameters:
- name: X-Request-ID
in: header
required: false
description: "ID de correlação para rastreabilidade"
schema:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
- name: X-Tenant-ID
in: header
required: true
description: "Identificador do tenant multi-tenant"
schema:
type: stringCookie Parameter
parameters:
- name: session_id
in: cookie
schema:
type: stringRequest Body
requestBody:
required: true
content:
# JSON
application/json:
schema:
$ref: '#/components/schemas/CreateTransactionRequest'
examples:
despesa_cartao:
summary: "Despesa no cartão de crédito"
value:
description: "Restaurante Silva"
amount: 85.90
type: "expense"
categoryId: "cat_food"
date: "2024-01-15"
cardId: "card_123"
receita_salario:
summary: "Recebimento de salário"
value:
description: "Salário Janeiro"
amount: 5000.00
type: "income"
categoryId: "cat_salary"
date: "2024-01-05"
# Form data (upload de arquivo)
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
description: "Arquivo CSV do extrato bancário"
bank:
type: string
enum: [itau, bradesco, nubank, inter]
description: "Banco de origem do extrato"
encoding:
file:
contentType: text/csv, application/octet-stream
# URL-encoded form
application/x-www-form-urlencoded:
schema:
type: object
properties:
grant_type:
type: string
enum: [authorization_code, refresh_token, client_credentials]
code:
type: string
redirect_uri:
type: string
format: uriResponses
responses:
# 200 — com schema
'200':
description: "Lista de transações"
headers:
X-Total-Count:
description: "Total de registros"
schema:
type: integer
X-Page:
description: "Página atual"
schema:
type: integer
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Transaction'
pagination:
$ref: '#/components/schemas/Pagination'
# 201 — criado com Location header
'201':
description: "Recurso criado com sucesso"
headers:
Location:
description: "URL do recurso criado"
schema:
type: string
format: uri
example: "https://api.meusite.com/v1/accounts/acc_01HXYZ"
content:
application/json:
schema:
$ref: '#/components/schemas/AccountResponse'
# 204 — sem body
'204':
description: "Operação realizada com sucesso (sem conteúdo)"
# 400 — validação
'400':
description: "Dados inválidos"
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
example:
code: "VALIDATION_ERROR"
message: "Dados inválidos"
errors:
- field: "amount"
message: "Deve ser maior que zero"
- field: "date"
message: "Formato inválido, use YYYY-MM-DD"
# 401
'401':
description: "Não autenticado"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "UNAUTHORIZED"
message: "Token ausente ou inválido"
# 403
'403':
description: "Sem permissão"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
# 404
'404':
description: "Recurso não encontrado"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "NOT_FOUND"
message: "Conta não encontrada"
# 409 — conflito
'409':
description: "Conflito com estado atual do recurso"
# 422 — entidade não processável
'422':
description: "Regra de negócio violada"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
# 429 — rate limit
'429':
description: "Muitas requisições"
headers:
Retry-After:
schema:
type: integer
description: "Segundos para aguardar antes de tentar novamente"
X-RateLimit-Limit:
schema:
type: integer
X-RateLimit-Remaining:
schema:
type: integerSchemas
Types e Formats
schemas:
# Tipos primitivos com formats
ExampleTypes:
type: object
properties:
# Strings
id:
type: string
format: ulid # custom format
email:
type: string
format: email
website:
type: string
format: uri
createdAt:
type: string
format: date-time # ISO 8601: 2024-01-15T10:30:00Z
birthDate:
type: string
format: date # ISO 8601: 2024-01-15
avatar:
type: string
format: binary # para upload
encodedData:
type: string
format: byte # base64 encoded
uuid:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
status:
type: string
enum: [active, inactive, pending, closed]
default: active
name:
type: string
minLength: 2
maxLength: 100
pattern: '^[a-zA-ZÀ-ÿ\s]+$'
# Números
amount:
type: number
format: double
minimum: 0
exclusiveMinimum: true # estritamente maior que 0
multipleOf: 0.01 # centavos
example: 1500.50
count:
type: integer
format: int32
minimum: 0
maximum: 2147483647
bigId:
type: integer
format: int64
# Boolean
active:
type: boolean
default: true
# Arrays
tags:
type: array
items:
type: string
minItems: 0
maxItems: 20
uniqueItems: true
# Object aninhado
address:
type: object
properties:
street:
type: string
city:
type: string
zipCode:
type: string
pattern: '^\d{5}-\d{3}$'
required: [street, city]
# Nullable (OpenAPI 3.0)
deletedAt:
type: string
format: date-time
nullable: true
# Nullable (OpenAPI 3.1 — JSON Schema nativo)
# deletedAt:
# type: [string, "null"]
# format: date-time
# Free-form object (qualquer chave/valor)
metadata:
type: object
additionalProperties: true
# Mapa de strings
labels:
type: object
additionalProperties:
type: string
example:
env: "production"
region: "br-east-1"Composição com allOf, oneOf, anyOf
schemas:
# allOf — herança / extensão de schema
AdminUser:
allOf:
- $ref: '#/components/schemas/BaseUser' # inclui todos os campos de BaseUser
- type: object
properties:
permissions:
type: array
items:
type: string
lastLogin:
type: string
format: date-time
required: [permissions]
# oneOf — exatamente um dos schemas deve ser válido (discriminator ajuda)
Transaction:
oneOf:
- $ref: '#/components/schemas/IncomeTransaction'
- $ref: '#/components/schemas/ExpenseTransaction'
- $ref: '#/components/schemas/TransferTransaction'
discriminator:
propertyName: type # campo que determina qual schema usar
mapping:
income: '#/components/schemas/IncomeTransaction'
expense: '#/components/schemas/ExpenseTransaction'
transfer: '#/components/schemas/TransferTransaction'
IncomeTransaction:
type: object
required: [type, amount, description, categoryId]
properties:
type:
type: string
enum: [income]
amount:
type: number
minimum: 0.01
description:
type: string
categoryId:
type: string
# anyOf — um ou mais schemas podem ser válidos
SearchFilter:
anyOf:
- $ref: '#/components/schemas/DateRangeFilter'
- $ref: '#/components/schemas/AmountRangeFilter'
- $ref: '#/components/schemas/CategoryFilter'Components (Reutilização)
components:
# Schemas reutilizáveis
schemas:
Pagination:
type: object
properties:
page:
type: integer
example: 1
size:
type: integer
example: 20
total:
type: integer
example: 150
totalPages:
type: integer
example: 8
hasNext:
type: boolean
hasPrevious:
type: boolean
ErrorResponse:
type: object
required: [code, message]
properties:
code:
type: string
example: "NOT_FOUND"
message:
type: string
example: "Recurso não encontrado"
details:
type: object
additionalProperties: true
requestId:
type: string
format: uuid
ValidationErrorResponse:
allOf:
- $ref: '#/components/schemas/ErrorResponse'
- type: object
properties:
errors:
type: array
items:
type: object
properties:
field:
type: string
example: "email"
message:
type: string
example: "Email inválido"
code:
type: string
example: "INVALID_FORMAT"
# Parâmetros reutilizáveis
parameters:
AccountIdParam:
name: accountId
in: path
required: true
description: "ID da conta"
schema:
type: string
example: "acc_01HXYZ123456"
PageParam:
name: page
in: query
required: false
schema:
type: integer
minimum: 1
default: 1
SizeParam:
name: size
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
RequestIdHeader:
name: X-Request-ID
in: header
required: false
schema:
type: string
format: uuid
# Responses reutilizáveis
responses:
NotFound:
description: "Recurso não encontrado"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
code: "NOT_FOUND"
message: "Recurso não encontrado"
Unauthorized:
description: "Não autenticado"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: "Sem permissão"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
ValidationError:
description: "Dados de entrada inválidos"
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
TooManyRequests:
description: "Rate limit excedido"
headers:
Retry-After:
schema:
type: integer
# Request bodies reutilizáveis
requestBodies:
CreateUserBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'Security Schemes
components:
securitySchemes:
# Bearer JWT (mais comum)
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: "JWT obtido via login ou OAuth 2.0"
# API Key no header
apiKeyHeader:
type: apiKey
in: header
name: X-API-Key
description: "API Key para acesso programático"
# API Key na query
apiKeyQuery:
type: apiKey
in: query
name: api_key
# OAuth 2.0 Authorization Code
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.meusite.com/authorize
tokenUrl: https://auth.meusite.com/token
refreshUrl: https://auth.meusite.com/token
scopes:
openid: "Identificação do usuário"
profile: "Dados básicos do perfil"
email: "Endereço de email"
read:accounts: "Leitura de contas"
write:accounts: "Criação e atualização de contas"
read:transactions: "Leitura de transações"
write:transactions: "Criação de transações"
admin: "Acesso administrativo completo"
clientCredentials:
tokenUrl: https://auth.meusite.com/token
scopes:
read:accounts: "Leitura de contas"
write:transactions: "Criação de transações"
# OpenID Connect
openIdConnect:
type: openIdConnect
openIdConnectUrl: https://auth.meusite.com/.well-known/openid-configuration
# HTTP Basic
basicAuth:
type: http
scheme: basic
# Aplicar globalmente
security:
- bearerAuth: []
# Por operação — sobrescreve o global
paths:
/public/health:
get:
security: [] # rota pública — sem autenticação
responses:
'200':
description: "OK"
/admin/users:
get:
security:
- bearerAuth: []
- oauth2: [admin] # requer scope admin
responses:
'200':
description: "Lista de usuários"Exemplos Inline vs $ref
# Inline — bom para exemplos simples, um único caso
/accounts:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Account'
example:
id: "acc_01HXYZ"
name: "Conta Corrente"
balance: 1500.00
# Múltiplos exemplos com nome
/accounts:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Account'
examples:
conta_corrente:
summary: "Conta corrente"
description: "Exemplo de conta corrente ativa"
value:
id: "acc_01HXYZ"
name: "Conta Corrente Principal"
type: "checking"
balance: 1500.00
conta_investimento:
summary: "Conta de investimento"
value:
id: "acc_02ABCD"
name: "CDB Banco X"
type: "investment"
balance: 50000.00
# Exemplo via referência externa
conta_externa:
$ref: '#/components/examples/ContaExternaExample'
components:
examples:
ContaExternaExample:
summary: "Conta no banco exterior"
value:
id: "acc_03EFGH"
name: "USD Account"
type: "checking"
currency: "USD"
balance: 2000.00Links e Callbacks
Links (relacionamento entre operações)
paths:
/accounts:
post:
operationId: createAccount
responses:
'201':
description: "Conta criada"
content:
application/json:
schema:
$ref: '#/components/schemas/Account'
links:
# Ao criar uma conta, o ID pode ser usado para buscar ela
GetAccountById:
operationId: getAccountById
parameters:
accountId: '$response.body#/id' # JSONPath na resposta
description: "Buscar a conta recém-criada"
ListTransactions:
operationId: listTransactions
parameters:
accountId: '$response.body#/id'Callbacks (Webhooks)
paths:
/webhooks/subscribe:
post:
summary: "Registrar URL para receber webhooks"
requestBody:
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
events:
type: array
items:
type: string
enum: [transaction.created, account.balance_changed]
callbacks:
onTransactionCreated:
# {$url} referencia o callbackUrl do request body
'{$request.body#/callbackUrl}':
post:
summary: "Notificação de nova transação"
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/WebhookPayload'
responses:
'200':
description: "Webhook recebido com sucesso"Code Generation (openapi-generator)
# Instalar
npm install -g @openapitools/openapi-generator-cli
# Listar generators disponíveis
openapi-generator-cli list
# Gerar client Java (Spring)
openapi-generator-cli generate \
-i openapi.yaml \
-g java \
-o ./generated/java-client \
--additional-properties=library=resttemplate,apiPackage=com.exemplo.api,modelPackage=com.exemplo.model
# Gerar client TypeScript (Axios)
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-axios \
-o ./generated/ts-client \
--additional-properties=npmName=@meusite/api-client,npmVersion=1.0.0,supportsES6=true
# Gerar server Spring Boot
openapi-generator-cli generate \
-i openapi.yaml \
-g spring \
-o ./generated/spring-server \
--additional-properties=library=spring-boot,artifactId=api-server,basePackage=com.exemplo,apiPackage=com.exemplo.api,modelPackage=com.exemplo.model,interfaceOnly=true
# Gerar client Python
openapi-generator-cli generate \
-i openapi.yaml \
-g python \
-o ./generated/python-client \
--additional-properties=packageName=meusite_api,projectName=meusite-api-client
# Via Docker (sem instalação)
docker run --rm \
-v $(pwd):/local openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g typescript-fetch \
-o /local/generated/ts-fetchUsando o Client TypeScript Gerado
// Cliente gerado automaticamente
import { AccountsApi, Configuration } from '@meusite/api-client';
const config = new Configuration({
basePath: 'https://api.meusite.com/v1',
accessToken: () => getAccessToken(), // função que retorna o token atual
});
const accountsApi = new AccountsApi(config);
// Type-safe, com todos os parâmetros definidos
const account = await accountsApi.getAccountById({
accountId: 'acc_01HXYZ',
include: ['transactions', 'balance'],
});
const created = await accountsApi.createAccount({
createAccountRequest: {
name: 'Nova Conta',
type: 'checking',
currency: 'BRL',
},
});Swagger UI e Redoc
Swagger UI (HTML standalone)
<!DOCTYPE html>
<html>
<head>
<title>API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: '/openapi.yaml', // ou url: '/api-docs' para JSON
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
deepLinking: true,
persistAuthorization: true, // mantém o token ao recarregar
displayOperationId: true,
defaultModelsExpandDepth: 2,
defaultModelExpandDepth: 2,
tryItOutEnabled: true, // permite testar direto na UI
filter: true, // busca de operações
withCredentials: true,
});
</script>
</body>
</html>Redoc (alternativa mais elegante)
<!DOCTYPE html>
<html>
<head>
<title>API Reference</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<redoc spec-url='/openapi.yaml'
expand-responses="200,201"
hide-download-button
no-auto-auth
theme='{"colors": {"primary": {"main": "#6200EE"}}}'
></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>Spring Boot — springdoc-openapi
Dependência
// build.gradle
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'# application.yaml
springdoc:
api-docs:
path: /api-docs # JSON em /api-docs
enabled: true
swagger-ui:
path: /swagger-ui.html # UI em /swagger-ui.html
enabled: true
operations-sorter: alpha
tags-sorter: alpha
try-it-out-enabled: true
persist-authorization: true
show-actuator: false
default-produces-media-type: application/jsonConfiguração Global
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("API Financeira")
.version("1.5.0")
.description("API REST para gerenciamento financeiro pessoal")
.contact(new Contact()
.name("Rafael Marques")
.email("rafael@exemplo.com")
.url("https://meusite.com"))
.license(new License()
.name("MIT")
.url("https://opensource.org/licenses/MIT")))
.servers(List.of(
new Server().url("https://api.meusite.com/v1").description("Produção"),
new Server().url("http://localhost:8080/api/v1").description("Local")
))
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("Token JWT de autenticação")));
}
}Anotações nos Controllers
@RestController
@RequestMapping("/api/v1/accounts")
@Tag(name = "accounts", description = "Gerenciamento de contas bancárias")
@SecurityRequirement(name = "bearerAuth")
public class AccountController {
@Operation(
operationId = "listAccounts",
summary = "Listar contas do usuário",
description = "Retorna todas as contas do usuário autenticado, paginadas.",
responses = {
@ApiResponse(responseCode = "200", description = "Lista de contas",
content = @Content(schema = @Schema(implementation = AccountPageResponse.class))),
@ApiResponse(responseCode = "401", description = "Não autenticado",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
}
)
@GetMapping
public ResponseEntity<Page<AccountDTO>> listAccounts(
@Parameter(description = "Número da página", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Tamanho da página", example = "20")
@RequestParam(defaultValue = "20") int size
) {
// ...
}
@Operation(
operationId = "createAccount",
summary = "Criar nova conta"
)
@ApiResponse(responseCode = "201", description = "Conta criada")
@ApiResponse(responseCode = "400", description = "Dados inválidos")
@PostMapping
public ResponseEntity<AccountDTO> createAccount(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Dados da nova conta",
required = true,
content = @Content(
examples = @ExampleObject(
name = "exemplo",
value = """
{
"name": "Conta Corrente",
"type": "checking",
"currency": "BRL",
"initialBalance": 0.00
}
"""
)
)
)
@RequestBody @Valid CreateAccountRequest request
) {
// ...
}
}Anotações nos Schemas (Models)
@Schema(description = "Dados de uma conta bancária")
public class AccountDTO {
@Schema(description = "ID único da conta", example = "acc_01HXYZ123456", readOnly = true)
private String id;
@Schema(description = "Nome da conta", example = "Conta Corrente Principal",
minLength = 2, maxLength = 100, requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
@Schema(description = "Tipo da conta",
allowableValues = {"checking", "savings", "investment", "credit"},
example = "checking")
private String type;
@Schema(description = "Saldo atual", example = "1500.50",
minimum = "0", format = "double")
private BigDecimal balance;
@Schema(description = "Código da moeda (ISO 4217)", example = "BRL", defaultValue = "BRL")
private String currency;
@Schema(description = "Data de criação", accessMode = Schema.AccessMode.READ_ONLY)
private LocalDateTime createdAt;
@Schema(description = "Conta está ativa", defaultValue = "true")
private boolean active;
}Node.js — swagger-jsdoc + swagger-ui-express
Setup
npm install swagger-jsdoc swagger-ui-express
npm install --save-dev @types/swagger-jsdoc @types/swagger-ui-expressConfiguração
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import { Express } from 'express';
const options: swaggerJsdoc.Options = {
definition: {
openapi: '3.0.3',
info: {
title: 'API Financeira',
version: '1.5.0',
description: 'API REST para gerenciamento financeiro',
},
servers: [
{ url: 'http://localhost:3000/api/v1', description: 'Local' },
{ url: 'https://api.meusite.com/v1', description: 'Produção' },
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [{ bearerAuth: [] }],
},
// Glob para encontrar arquivos com JSDoc/TSDoc
apis: ['./src/routes/**/*.ts', './src/schemas/**/*.ts'],
};
const specs = swaggerJsdoc(options);
export function setupSwagger(app: Express) {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
explorer: true,
customSiteTitle: 'Financeiro API Docs',
swaggerOptions: {
persistAuthorization: true,
},
}));
// Servir o spec JSON/YAML
app.get('/openapi.json', (req, res) => res.json(specs));
}Documentando rotas com JSDoc
// src/routes/accounts.ts
/**
* @openapi
* /accounts:
* get:
* operationId: listAccounts
* summary: Listar contas
* tags: [accounts]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: size
* schema:
* type: integer
* default: 20
* responses:
* 200:
* description: Lista de contas
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/Account'
* pagination:
* $ref: '#/components/schemas/Pagination'
* 401:
* $ref: '#/components/responses/Unauthorized'
*/
router.get('/', authMiddleware, async (req, res) => {
// implementação...
});
/**
* @openapi
* components:
* schemas:
* Account:
* type: object
* required: [id, name, type, balance]
* properties:
* id:
* type: string
* example: acc_01HXYZ
* name:
* type: string
* example: Conta Corrente
* type:
* type: string
* enum: [checking, savings, investment]
* balance:
* type: number
* format: double
* example: 1500.50
*/Validação de Spec com Spectral
# Instalar
npm install -g @stoplight/spectral-cli
# Validar com ruleset padrão OAS
spectral lint openapi.yaml
# Validar com ruleset customizado
spectral lint openapi.yaml --ruleset .spectral.yaml# .spectral.yaml — ruleset customizado
extends:
- spectral:oas # regras padrão OpenAPI
- spectral:asyncapi # (opcional) para AsyncAPI
rules:
# Toda operação deve ter operationId
operation-operationId: error
# Toda operação deve ter pelo menos uma tag
operation-tags: error
# Resposta 4xx deve ter content
operation-4xx-response:
message: "Operações devem ter ao menos uma resposta 4xx"
severity: warn
given: "$.paths[*][get,post,put,patch,delete]"
then:
function: schema
functionOptions:
schema:
properties:
responses:
anyOf:
- required: ['400']
- required: ['401']
- required: ['403']
- required: ['404']
# Nomes de operationId em camelCase
operation-operationId-camel-case:
message: "operationId deve estar em camelCase"
severity: warn
given: "$.paths[*][*].operationId"
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
# Exemplos obrigatórios para campos de schema
schema-properties-examples:
message: "Propriedades de schema devem ter exemplo"
severity: hint
given: "$.components.schemas[*].properties[*]"
then:
field: example
function: definedExemplos Completos de Endpoints
CRUD Completo
# Trecho de openapi.yaml com CRUD completo de transactions
paths:
/transactions:
get:
operationId: listTransactions
summary: Listar transações
tags: [transactions]
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/SizeParam'
- name: accountId
in: query
schema:
type: string
- name: startDate
in: query
schema:
type: string
format: date
example: "2024-01-01"
- name: endDate
in: query
schema:
type: string
format: date
example: "2024-01-31"
- name: type
in: query
schema:
type: string
enum: [income, expense, transfer]
- name: minAmount
in: query
schema:
type: number
minimum: 0
- name: maxAmount
in: query
schema:
type: number
minimum: 0
responses:
'200':
description: Lista paginada de transações
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Transaction'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
post:
operationId: createTransaction
summary: Criar transação
tags: [transactions]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTransactionRequest'
responses:
'201':
description: Transação criada
headers:
Location:
schema:
type: string
format: uri
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
description: Regra de negócio violada (saldo insuficiente, etc.)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/transactions/{transactionId}:
parameters:
- name: transactionId
in: path
required: true
schema:
type: string
get:
operationId: getTransactionById
summary: Buscar transação
tags: [transactions]
responses:
'200':
description: Transação encontrada
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'404':
$ref: '#/components/responses/NotFound'
patch:
operationId: patchTransaction
summary: Atualizar parcialmente
tags: [transactions]
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchTransactionRequest'
responses:
'200':
description: Transação atualizada
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteTransaction
summary: Deletar transação
tags: [transactions]
responses:
'204':
description: Deletado com sucesso
'404':
$ref: '#/components/responses/NotFound'
'409':
description: Transação não pode ser deletada (ex: mês fechado)Checklist de Qualidade da Spec
- Versão
openapi: "3.0.3"ou superior -
infocom title, version, description e contato - Todos os endpoints têm
operationIdúnico - Todos os endpoints têm pelo menos uma
tag -
summaryedescriptionem todas as operações - Parâmetros obrigatórios marcados com
required: true - Schemas com
requiredetypedefinidos -
exampleem propriedades de schema - Respostas de erro (401, 403, 404, 422) documentadas
- Security schemes definidos e aplicados
- Components reutilizados via
$ref(sem duplicação) - Spec validada com Spectral sem erros
- Code generation testado e cliente funcional
- Swagger UI ou Redoc acessível no ambiente de desenvolvimento