Protocolos & APIs

Swagger / OpenAPI

Guia completo de OpenAPI 3.x: estrutura, paths, schemas, security, code generation, Spring Boot e Node.js

OpenAPI 3.x vs Swagger 2.x

AspectoSwagger 2.0 (OAS 2)OpenAPI 3.0 / 3.1
Formato de arquivoswagger: "2.0"openapi: "3.0.3" ou "3.1.0"
Hosts múltiplosApenas um host/basePathservers[] com múltiplas URLs
Request Bodybody paramrequestBody separado
Exemplosexample simplesexamples com múltiplos casos
Nullablex-nullable: truenullable: true (3.0) ou type: [string, null] (3.1)
LinksNão suportadolinks para relacionar responses
CallbacksNão suportadocallbacks para webhooks
JSON SchemaSubconjunto próprioAlinhado com JSON Schema (3.1 é compatível 100%)
Componentesdefinitions, parameterscomponents unificado
Content typesconsumes/producescontent 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: false

Header 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: string
parameters:
  - name: session_id
    in: cookie
    schema:
      type: string

Request 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: uri

Responses

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: integer

Schemas

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.00

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-fetch

Usando 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/json

Configuraçã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-express

Configuraçã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: defined

Exemplos 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
  • info com title, version, description e contato
  • Todos os endpoints têm operationId único
  • Todos os endpoints têm pelo menos uma tag
  • summary e description em todas as operações
  • Parâmetros obrigatórios marcados com required: true
  • Schemas com required e type definidos
  • example em 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