Codegen com o Swagger

Guilherme Bogo
Magrathea
Published in
6 min readAug 27, 2021

--

Codegen com o Swagger

Artigo por Gabriel Rohden

Este texto é sobre minha experiência tentando replicar a geração de código (e tipos) para interações entre frontend e backend, se você prefere partir pro código, pode pular pra parte da implementação.

Motivação

Há uns anos atras, trabalhei em um projeto GraphQL, eu amei demais as vantagens dele (apesar de reconhecer alguns de seus defeitos), mas uma das que mais se destacou foi a de ter um schema de tudo que o backend disponibiliza como API, o schema no GraphQL é algo assim:

type Query {
me: User
}
type User {
id: ID
name: String
}

No GraphQL, o schema informa todos os ‘endpoints’ de consulta disponíveis (type Query), suas rotas (me) , argumentos (se tiverem) e retornos (User). Além de ser uma documentação, o schema também me possibilita gerar código. Por exemplo, aplicando o graphql-code-generator no schema acima eu consigo gerar um código similar a este em Typescript:

type Query = {
me: User
}
type User = {
id: string
name: string
}

Porém quando precisei voltar a mexer em REST, lembrei que não era costume fazer essa geração de código, e, além disso, eu não conhecia muito sobre como documentar/formalizar APIs RESTful, mas eu me lembrava de já ter lidado com o Swagger pra isso.

Swagger e a OpenAPI Spec

Se você trabalha com REST com certeza já ouviu falar do Swagger, ele é uma ferramenta que te permite gerar uma página interativa com a documentação da sua API REST, uma das coisas que você pode não ter pensado é que o Swagger não foi feito pra ser usado somente com uma UI.

O Swagger é baseado em uma especificação, chamada de OpenAPI Specification (OAS), dela ele gera a sua interface amigável. Geralmente a OAS é escrita ou gerada em JSON e te permite fazer as mesmas coisas que você faria com o schema GraphQL: documentar toda a sua API e também gerar ‘coisas’ com base nela. Um exemplo da especificação OAS fica assim:

{
"openapi": "3.0.0",
"info": {
"title": "My API Documentation",
"description": "My API documentation and playground",
"version": "1.0",
"contact": {}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}
}
},
"paths": {
"/me": {
"get": {
"operationId": "getMe",
"summary": "Returns the logged user data",
"parameters": [],
"responses": {
"200": {
"description": "Logged user info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
}
}
}

É só um “pouquinho” maior que o schema do GraphQL, porém ela te permite documentar mais algumas coisas, como por exemplo os tipos de autenticações disponíveis na sua API.

OpenAPI Typescript Codegen

Bom, tendo a especificação da minha API só falta achar uma ferramenta que consiga fazer a mesma geração de código que eu tinha antes. Nas minhas buscas eu achei algumas:

Eu acabei testando o openapi-generator e o openapi-typescript-codegen porque em alguns clients as docs não ajudavam e outros eu não achei que seriam fáceis de usar. Enfim, dos dois, o mais facil de usar foi o openapi-typescript-codegen e acabei optando por ele porque o openapi-generator depende do java e só gera enums, o que complicou a situação com a spec gerada pelo NestJS do nosso backend.

Inclusive antes de achar o openapi-typescript-codegen eu acabei escrevendo eu mesmo um code-generator:

Foi bem legal escrever um :D

Implementando

Antes de implementar, pra simplificar, a partir daqui vou me referir ao openapi-typescript-codegen como generator.

Implementar o generator é tão simples quanto rodar um comando. Bom, na verdade é exatamente rodar um comando:

npx openapi-typescript-codegen \\
# podemos passar a URL da spec aqui, no caso do NestJS
# geralmente fica em /docs-json
--input "./my-api-schema.json"\\
--output ./my-generated-api

Existem algumas opções de personalizações pro generator, recomendo ler as docs aqui.

Aplicando o código gerado

O comando do generator vai gerar uma estrutura de pastas e arquivos assim:

- index.ts
- core/
- ApiError.ts
- ApiRequestOptions.ts
- ApiResult.ts
- OpenAPI.ts
- request.ts
- models/
- User.ts
- services/
- Service.ts

As partes importantes são:

  • request.ts que é onde as requisições acontecem de verdade através do fetch
  • models aqui estão todos tipos retornados ou enviados aos endpoints
  • services aqui estão os arquivos que proveem a interação com o backend

Pra usar o código gerado é simples, é só importar o service e usar:

import { Service } from './my-generated-api'Service.getMe().then(user => {
// user ta tipado como User aqui :D
user.name // string
})

Pra adicionar a baseUrl e configurar alguns aspectos das requisições, é só ‘mutar’ o objeto OpenAPI

import { OpenAPI } from './my-generated-api';const getToken = () => Promise.resolve('token');OpenAPI.BASE = '<http://api.my-service.com/>';
OpenAPI.TOKEN = getToken;

Pontos pra se atentar

Antes de adotar seriamente em um projeto é bom se atentar a esses pontos:

  • É preferível unions no lugar de enums:

O generator não tem como saber que dois enums com os mesmos valores são a mesma coisa — afinal, eles realmente podem não ser — e a OAS deixa um enum ser especificado sem nome, o que faz o generator tentar gerar enums iguais com nomes diferentes, para o TS eles são tratados como símbolos diferentes e você não vai poder usar um no lugar do outro. Pra isso é preferível configurar o generator pra usar unions --useUnionTypes

  • Isole os usos da lib gerada se possivel

Isso serve pra qualquer lib, mas é bom isolar/centralizar as chamadas da lib gerada, assim, se você trocar de generator, não vai ter tanta dor de cabeça nos refactors

Desvantagens

Nem tudo são dales:

  • Você vai ter que seguir corretamente a OAS:

Bom, essa não é uma desvantagem, usar o generator vai te forçar a manter a documentação da sua API atualizada, mas nem sempre uma UI do swagger que já funciona corretamente está correta nos termos da OAS, então fica um pouco difícil de adotar a geração de código de imediato.

  • Pode te impedir de mergear MR/PR

Caso você decida sempre gerar o código em suas pipes, se alguém liberar uma OAS incorreta no backend pode ser que a sua pipe fique travada no service que está gerando o código, te forçando a deixar correto no backend, mesmo que isso não tenha a ver com o seu MR/PR. Isso pode ser solucionado facilmente adicionando validações nos MR/PRs do seu backend pra impedir merges de OAS incorretas.

  • O código gerado não vai ser exatamente como você quer

Como um exemplo, temos essa issue aqui, e no geral alguns aspectos de todos os generators me desagradam, pra resolver isso você pode abrir PRs ou fazer o seu próprio fork.

  • Ainda não tem um bom suporte a interceptors e múltiplos backends

Isso provavelmente vai ser resolvido por esse PR, mas se você precisa adicionar interceptors (lidar com 403 e matar uma sessão por exemplo), vai ter que criar um proxy pra todas chamadas ou lidar com elas toda vez que chamar as requests.

Vantagens

Mas ainda sim, vale a pena adotar o generator, já que o codegen vai te trazer algumas vantagens como:

  • Manter a documentação das APIs atualizadas

Caso você use anotações no backend pra gerar as especificações do OpenAPI, usá-la diretamente vai te forçar a manter as documentações atualizadas corretamente a cada mudança

  • Facilitar a deteção de breaking changes

Se você usa o Typescript em modo estrito, vai ser fácil detectar se uma mudança na API vai quebrar algum lugar da sua aplicação, tudo que você vai precisar fazer é re-gerar o código e validar com um tsc —noEmit

  • Menos código escrito manualmente

Fora a parte de não perder tempo tendo que caçar o endpoint, seus argumentos e retorno, você não vai precisar mais escrever as chamadas diretamente, isso vai diminuir bastante as chances de typos tanto nas URLs quanto nos parâmetros

Conclusão

A adoção do generator nos projetos em que trabalhei me ajudou a identificar vários errinhos de tipos e possíveis bugs escondidos. Ela também me possibilitou ver coisas incorretas nas documentações que geramos dos nossos endpoints. A adoção foi um pouco complicada por que tínhamos muitas coisas estranhas sendo reveladas pelo sistema de tipos, mas eventualmente conseguimos adotá-la e o tempo dedicado a isso valeu a pena.

Ah, um adicional aqui, se você precisa muuuito usar o axios ou quer gerar clients em outras linguagens que não as do ecossistema js (pra interação entre services), recomendo usar o openapi-codegenerator. Se não achar uma solução dedicada, ele é um pouco complicado mas funciona.

Artigo publicado originalmente no blog da Magrathea.

--

--