Apostila completa de API Design: REST, GraphQL e gRPC
Uma API é um contrato: outras pessoas escrevem código contra ela e você não pode quebrá-lo à toa. Esta apostila cobre a semântica do HTTP, o design RESTful de verdade, o contrato OpenAPI, um modelo de erros consistente, GraphQL e gRPC com seus trade-offs, a segurança segundo o OWASP API Top 10, e a evolução sem quebrar clientes — sempre com o olho no que empresas pedem em vagas de back-end e plataforma.
A API como contrato e como produto
Objetivo: entender por que design de API é uma decisão de longo prazo, o que significa "API-first", e o panorama dos estilos disponíveis.
1.1 Uma API é um contrato
Quando você publica uma API, outras equipes (ou empresas, ou o seu app mobile de 2 anos atrás) escrevem código que depende do formato exato das suas requisições e respostas. Mudar um nome de campo, um código de status, o tipo de um valor — quebra o código de quem confiou no contrato. Diferente de uma função interna, que você refatora à vontade, uma API pública tem inércia enorme: clientes que você não controla, que atualizam devagar ou nunca.
Projete a API como se ela fosse durar 10 anos e nunca poder quebrar. Isso força as decisões certas: contrato explícito, nomes claros e estáveis, erros consistentes, extensibilidade sem breaking change, e versionamento pensado desde o dia 1. "A gente arruma depois" custa caríssimo em API.
1.2 API como produto
Para quem consome, a sua API é o seu produto. As qualidades que importam:
- Previsível: convenções consistentes — se um endpoint pagina de um jeito, todos paginam igual.
- Descobrível e bem documentada: spec (OpenAPI), exemplos, guia de início, changelog.
- Fácil de errar com segurança: mensagens de erro úteis, validação clara, sandbox.
- Estável: mudanças comunicadas, deprecações longas, compatibilidade retroativa.
- Performática e confiável: latência baixa, limites claros, disponibilidade.
1.3 API-first (design-first)
Em vez de codar o serviço e "gerar a API do que saiu", você projeta o contrato primeiro (OpenAPI/schema), revisa com os consumidores, e só então implementa — cliente e servidor trabalham em paralelo contra o mesmo contrato, com mocks. Reduz retrabalho, alinha as equipes e produz uma API pensada para quem usa, não para o esquema do banco.
1.4 O panorama de estilos
| Estilo | Ideia | Brilha em |
|---|---|---|
| REST / HTTP | Recursos identificados por URL, manipulados por métodos HTTP | APIs públicas, CRUD, integração entre partes heterogêneas, cacheável |
| GraphQL | Um schema tipado; o cliente pede exatamente os campos que quer em uma query | Front-ends variados (web/mobile) que precisam de dados de várias fontes sem N chamadas |
| gRPC / RPC | Chamar funções remotas com contrato binário (Protobuf) sobre HTTP/2 | Comunicação interna entre serviços, alta performance, streaming, polyglot |
| Eventos / Webhooks / Async | O servidor notifica o cliente quando algo acontece | Integrações reativas, evitar polling, fluxos assíncronos |
Não é "um vence" — sistemas maduros usam vários: REST para a API pública, gRPC entre serviços internos, GraphQL para o BFF do app, webhooks para integrações. Os módulos 3, 6 e 7 aprofundam cada um e o módulo 7 fecha a comparação.
Design de API é competência central de back-end e o gargalo em muitas vagas de plataforma. Perguntas de abertura: "Por que uma API é diferente de uma função interna?" (contrato com clientes que você não controla), "O que é API-first?", "Quando você escolheria GraphQL em vez de REST?". Demonstrar que você pensa em quem consome e em evolução vale mais que decorar convenções.
✏️ Exercício 1 — Contrato ou implementação?
Para cada mudança, diga se ela quebra o contrato (breaking) ou não: (a) adicionar um campo opcional na resposta; (b) renomear user_name para username; (c) tornar obrigatório um campo de entrada que era opcional; (d) mudar o código de sucesso de 200 para 201; (e) trocar o tipo de id de número para string; (f) adicionar um novo endpoint.
Gabarito: Não-breaking: (a) e (f) — adição de campo opcional e de endpoint são compatíveis (se os clientes ignoram o que não conhecem). Breaking: (b) renomear campo; (c) novo obrigatório rejeita requisições antes válidas; (d) clientes que checam == 200 quebram; (e) mudança de tipo quebra parsing. Regra: adicionar é seguro, mudar/remover/tornar-obrigatório não é.
HTTP a fundo
Objetivo: dominar a semântica que sustenta toda API HTTP — métodos, códigos de status, headers, negociação de conteúdo e cache — porque usá-los errado é o erro mais comum.
2.1 Métodos e suas propriedades
| Método | Uso | Safe? | Idempotente? |
|---|---|---|---|
GET | Ler um recurso; sem efeito colateral | Sim | Sim |
HEAD | Como GET, só os headers | Sim | Sim |
POST | Criar; ação não idempotente; operações que não cabem em CRUD | Não | Não |
PUT | Substituir o recurso inteiro (ou criar com id conhecido) | Não | Sim |
PATCH | Atualização parcial | Não | Não necessariamente |
DELETE | Remover | Não | Sim |
- Safe: não altera estado — pode ser feito sem consequência. GETs safe permitem cache, prefetch, retry livre.
- Idempotente: repetir N vezes tem o mesmo efeito de uma. Fundamental para retries: um
PUTrepetido é seguro; umPOSTde "criar pedido" repetido cria dois — por issoPOSTque cria precisa de chave de idempotência (headerIdempotency-Key).
2.2 Códigos de status: as famílias e os que importam
| Família | Significado | Os que você usa |
|---|---|---|
| 2xx | Sucesso | 200 OK, 201 Created (+ Location), 202 Accepted (async), 204 No Content |
| 3xx | Redirecionamento | 301/308 permanente, 304 Not Modified (cache) |
| 4xx | Erro do cliente | 400 (inválido), 401 (não autenticado), 403 (sem permissão), 404, 405, 409 (conflito), 410 (foi-se), 422 (semântica inválida), 429 (rate limit) |
| 5xx | Erro do servidor | 500 (bug), 502/503/504 (indisponível/timeout — o cliente pode retry) |
Retornar 200 com {"error": "..."} no corpo (quebra todo cliente e todo proxy que confia no status); usar 401 quando é 403 (autenticado mas sem permissão) e vice-versa; 404 para "sem permissão de ver" pode ser proposital (não revelar existência) — decida conscientemente; 500 para erro de validação do cliente (é 400/422). Use o status certo; o corpo dá o detalhe.
2.3 Headers que importam
- Content negotiation:
Accept(o cliente diz o formato que quer),Content-Type(o formato do corpo enviado),Accept-Language. - Cache:
Cache-Control(max-age, no-store, private/public),ETag+If-None-Match(validação condicional → 304),Last-Modified+If-Modified-Since. - Concorrência otimista:
ETag+If-MatchnumPUT/PATCH→ 412 se o recurso mudou desde que o cliente leu (evita "lost update"). - Correlação:
X-Request-Id/traceparentpara rastrear a requisição ponta a ponta. - Rate limit:
RateLimit-*/Retry-Afterpara o cliente se comportar.
2.4 CORS (o que confunde todo mundo)
CORS é uma proteção do navegador: por padrão, JavaScript numa origem não pode ler a resposta de outra origem, a menos que o servidor mande headers Access-Control-Allow-Origin etc. autorizando. Não é segurança da API (um cliente não-navegador ignora CORS) — é controle de quais sites no browser podem chamá-la. Requisições "não simples" disparam um preflight OPTIONS. Configure a allowlist de origens explicitamente; nunca * junto com credenciais.
Perguntas quase garantidas: "Diferença entre PUT e PATCH; e entre PUT e POST", "O que significa um método ser idempotente e por que importa para retries?", "Quando 401 vs 403 vs 404?", "O que é CORS e por que 'funciona no Postman mas não no browser'?" (CORS é do navegador), "Como implementar concorrência otimista com ETag?".
✏️ Exercício 2 — Escolha método e status
Para cada operação, dê método + status de sucesso + um status de erro relevante: (a) criar um artigo; (b) marcar um artigo como publicado; (c) buscar um artigo que não existe; (d) editar o título de um artigo que outra pessoa alterou no meio-tempo; (e) apagar um artigo já apagado.
Gabarito: (a) POST /articles → 201 + Location; erro: 422 se o corpo é inválido, 409 se o slug já existe. (b) POST /articles/{id}/publish (ação) ou PATCH com {status:"published"} → 200; erro: 409 se já publicado / regra de negócio. (c) GET → 404. (d) PATCH com If-Match: "etag" → 200; erro: 412 Precondition Failed (o ETag não bate — recurso mudou). (e) DELETE → 204 (idempotente: deletar o já deletado ainda é "não existe mais" = sucesso), ou 404 se você prefere sinalizar; escolha e seja consistente.
Design de APIs REST
Objetivo: modelar recursos, URIs, coleções, filtros e paginação segundo as convenções que tornam uma API REST previsível — e saber onde o "REST puro" cede à praticidade.
3.1 Recursos e URIs
- Substantivos, não verbos:
/orders, não/getOrders. O verbo é o método HTTP. - Plural para coleções:
/orders(coleção),/orders/{id}(item),/orders/{id}/items(sub-coleção). - Hierarquia com moderação: aninhe quando há relação de posse forte (
/orders/{id}/items); evite aninhamento profundo (/users/{id}/orders/{id}/items/{id}/...) — prefira/order-items/{id}no topo. - kebab-case nos paths,
snake_caseoucamelCaseconsistente nos campos JSON (escolha um e nunca misture). - IDs opacos e estáveis (UUID/ULID) preferíveis a inteiros sequenciais expostos (evita enumeração e vazar volume de negócio).
3.2 Ações que não são CRUD
"Publicar um artigo", "reembolsar um pagamento", "arquivar" — nem tudo é criar/ler/atualizar/apagar. Opções, em ordem de preferência:
- Modelar como um sub-recurso:
PUT /articles/{id}/publication,POST /payments/{id}/refunds(o reembolso é um recurso). - Endpoint de ação explícito:
POST /articles/{id}/publish— pragmático e claro; muito comum, ninguém vai te processar por isso. - Campo de estado num PATCH:
PATCH {id} {"status": "published"}— bom se o estado é o modelo real.
3.3 Coleções: filtro, ordenação, paginação, campos
GET /orders?status=paid&created_after=2026-01-01&sort=-created_at&page[size]=50&fields=id,total,customer filtro → ?status=paid&min_total=100 (query params; nomes previsíveis) ordenação → ?sort=-created_at,total (prefixo - para desc) projeção → ?fields=id,total (sparse fieldsets — reduz payload) expansão → ?expand=customer,items (inclui recursos relacionados)
Paginação: as duas estratégias
Offset / page-number (?page=3&size=50) | Cursor / keyset (?after=eyJ...&limit=50) | |
|---|---|---|
| Simplicidade | Fácil de entender e de "pular para a página 7" | Cursor opaco; sem "pular para página N" |
| Performance | Degrada em offsets grandes (o banco varre e descarta) | Estável em qualquer profundidade (usa índice) |
| Consistência | Itens inseridos/removidos deslocam páginas (duplica ou pula) | Estável mesmo com escrita concorrente |
| Use quando | Datasets pequenos, UI com números de página | Feeds, listas grandes, scroll infinito, exportação — o padrão para escala |
Sempre retorne metadados de paginação (total quando barato, next/prev links ou next_cursor) e imponha um limit máximo.
3.4 O Richardson Maturity Model e o HATEOAS
- Nível 0: um endpoint, tudo via POST (RPC sobre HTTP disfarçado).
- Nível 1: múltiplos recursos com URIs.
- Nível 2: usa métodos HTTP e status codes corretamente. É aqui que vive ~99% das "APIs REST" reais e é um bom alvo.
- Nível 3 (HATEOAS): as respostas incluem links para as ações possíveis (
_links), tornando o cliente "descobrível". Elegante na teoria; raríssimo na prática porque os clientes acabam hard-codando URLs mesmo. Inclua links de paginação e de recursos relacionados (útil), mas não se obrigue ao HATEOAS completo.
Perguntas: "Como você modelaria a API de um blog?" (recursos, URIs, métodos, ação de publicar), "Offset ou cursor pagination, e por quê?" (cursor para escala — offset degrada e é inconsistente sob escrita), "O que é HATEOAS e você usa?" (nível 3 do RMM; na prática, links de paginação/relacionados sim, HATEOAS completo raramente), "Como fazer uma ação que não é CRUD de forma RESTful?".
✏️ Exercício 3 — Projete a API
Desenhe os endpoints para: uma biblioteca com livros e empréstimos. Um usuário pode pegar um livro emprestado, devolver, e renovar. Liste método + path para cada operação e como você pagina a lista de livros.
Gabarito (uma boa resposta): GET /books?author=&available=true&sort=title&limit=50&after=cursor (cursor pagination); GET /books/{id}; POST /loans {book_id, user_id} → 201 (empréstimo é um recurso; 409 se o livro não está disponível); GET /loans/{id}; GET /users/{id}/loans?status=active; devolver: POST /loans/{id}/return → 200 (ação) ou PATCH /loans/{id} {"status":"returned"}; renovar: POST /loans/{id}/renewals → 201 (renovação como sub-recurso, com regra de "máx. 2 renovações" → 409/422). IDs opacos.
Contrato e especificação: OpenAPI
Objetivo: descrever a API num contrato legível por máquina, escolher entre design-first e code-first, e aproveitar o ecossistema (docs, SDKs, mocks, lint).
4.1 OpenAPI (ex-Swagger)
Um documento YAML/JSON que descreve cada endpoint: path, método, parâmetros, corpo de requisição e resposta (com JSON Schema), códigos de status, autenticação e exemplos. É o padrão de fato para APIs HTTP.
# trecho de openapi.yaml paths: /orders/{orderId}: get: operationId: getOrder parameters: - { name: orderId, in: path, required: true, schema: { type: string, format: uuid } } responses: '200': description: O pedido content: application/json: schema: { $ref: '#/components/schemas/Order' } '404': $ref: '#/components/responses/NotFound' components: schemas: Order: type: object required: [id, status, total] properties: id: { type: string, format: uuid } status: { type: string, enum: [pending, paid, shipped, cancelled] } total: { type: integer, description: 'em centavos' }
4.2 Design-first × code-first
| Design-first | Code-first |
|---|---|
| Escreve o OpenAPI à mão (ou com editor), revisa, depois implementa | Anota o código (decorators/atributos) e gera o OpenAPI a partir dele |
| + contrato pensado para o consumidor; cliente e servidor em paralelo; mocks desde já | + menos duplicação; a spec nunca "mente" sobre o código |
| − risco de a implementação divergir da spec (mitigar com contract testing) | − a API acaba refletindo o modelo interno; refino de design fica em segundo plano |
Muitos times fazem híbrido: design-first para o formato e as decisões, geração a partir do código para os detalhes, e contract testing (Dredd, Schemathesis, Pact) garantindo que implementação e spec batem.
4.3 O que o contrato te dá "de graça"
- Documentação interativa (Swagger UI, Redoc, Scalar) sempre atualizada.
- SDKs de cliente gerados em várias linguagens (openapi-generator).
- Servidores de mock a partir da spec (Prism) — front-end começa sem back-end.
- Validação de requisição/resposta em runtime contra o schema.
- Lint e style guide (Spectral): regras automatizadas — "todo endpoint tem 401 documentado", "nomes em camelCase", "toda coleção pagina". Roda no CI.
- Testes de fuzzing baseados no schema (Schemathesis).
Uma API grande feita por muitos times só fica coerente com um guia de estilo escrito (nomenclatura, paginação, formato de erro, datas, versionamento) e um linter que o aplica no CI. As guidelines públicas de Google, Microsoft, Stripe e Zalando são ótimas bases para adaptar.
Perguntas: "O que é OpenAPI e para que serve?", "Design-first ou code-first?" (trade-offs; híbrido + contract testing), "Como você mantém consistência numa API com 200 endpoints e 10 times?" (style guide + Spectral no CI), "O que é contract testing?". Ter um repo com um OpenAPI bem feito no portfólio já sinaliza senioridade.
✏️ Exercício 4 — Regras de lint
Escreva (em português, como regras) 6 itens de um style guide de API que um linter poderia checar automaticamente.
Gabarito (exemplos): (1) Todo endpoint deve documentar as respostas 400, 401 e 500. (2) Coleções (paths no plural sem {id} no fim) devem aceitar limit e um parâmetro de cursor. (3) Nomes de propriedade em camelCase (ou snake_case) — um só, sem exceção. (4) Datas em string com format: date-time (ISO 8601, UTC). (5) Todo POST que cria recurso responde 201 com header Location. (6) Toda resposta de erro usa o schema ProblemDetails. (7) Nenhum path com mais de 3 níveis de aninhamento. (8) operationId presente e único.
Erros, validação e formatos
Objetivo: um modelo de erro consistente e útil, validação de entrada robusta, e convenções de formato (datas, dinheiro, ids, null) que evitam ambiguidade.
5.1 Um modelo de erro consistente
Nada frustra mais um integrador do que cada endpoint errar de um jeito. Adote um formato para toda a API. O padrão recomendado é o Problem Details for HTTP APIs (RFC 9457, ex-7807):
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.exemplo.com/problems/validation-error",
"title": "Corpo da requisição inválido",
"status": 422,
"detail": "O campo 'email' não é um e-mail válido.",
"instance": "/orders",
"errors": [
{ "field": "email", "code": "invalid_format", "message": "formato inválido" },
{ "field": "items", "code": "min_length", "message": "pelo menos 1 item" }
],
"traceId": "abc-123"
}
typeé uma URI estável que identifica a classe do erro — o cliente pode ramificar por ela (mais estável que a mensagem).- Código de erro legível por máquina por campo (
invalid_format), separado da mensagem legível por humano. - Nunca vaze stack trace, SQL, caminhos internos em produção — isso é superfície de ataque.
- Inclua um
traceIdpara o suporte cruzar com os logs. - Retorne todos os erros de validação de uma vez, não o primeiro que falhou.
5.2 Validação de entrada
- Valide contra o schema (tipos, obrigatórios, enums, ranges, formatos) na borda, antes da lógica de negócio.
- Rejeite o desconhecido (campos extras) ou ignore-os — decida a política e documente. Em geral, ignorar campos extras ajuda a evolução; rejeitar ajuda a pegar erros do cliente.
- Limites: tamanho máximo do corpo, profundidade de aninhamento, tamanho de arrays, tamanho de strings — proteção contra abuso (ver Módulo 8).
- Normalize (trim, lowercase de e-mail) de forma explícita e documentada.
5.3 Convenções de formato
| Tipo | Convenção |
|---|---|
| Data/hora | String ISO 8601 em UTC com offset (2026-08-30T14:00:00Z). Nunca timestamps ambíguos, nunca só "data" quando é datetime. |
| Dinheiro | Inteiro em unidade mínima (centavos) + código de moeda ({"amount": 1990, "currency": "BRL"}). Float para dinheiro é bug esperando acontecer. |
| IDs | String opaca (UUID/ULID). Não prometa que é numérico nem que é ordenável. |
| Enums | Strings minúsculas estáveis ("in_transit"); documentadas; adicionar valor novo é potencialmente breaking para clientes que fazem switch exaustivo — comunique. |
| Booleanos | true/false reais, não "true"/1/"Y". |
| Ausência | Decida: omitir a chave vs null explícito. Em PATCH, essa distinção importa (omitido = não mexer; null = limpar) — considere JSON Merge Patch (RFC 7386) ou JSON Patch (RFC 6902). |
| Coleções vazias | [], nunca null. |
5.4 Envelope ou não?
Envelopar toda resposta em {"data": ..., "meta": ...} vs retornar o recurso direto. Prós do envelope: espaço padronizado para paginação, warnings, metadados; consistência. Contras: verbosidade, um nível a mais. Escolha um e aplique a toda a API. Comum: recurso direto para item único, envelope leve para coleções (por causa da paginação).
Perguntas: "Como você padroniza erros numa API?" (Problem Details / RFC 9457, código legível por máquina + mensagem humana, traceId, todos os erros de validação de uma vez, sem stack trace), "Como representar dinheiro em JSON?" (inteiro em centavos + moeda), "Como um PATCH distingue 'não mexer' de 'limpar o campo'?" (omitido vs null; JSON Merge/JSON Patch).
✏️ Exercício 5 — Conserte a resposta de erro
Uma API retorna, para um e-mail inválido: HTTP 200, corpo {"success": false, "msg": "erro"}. Aponte os problemas e escreva a versão correta.
Gabarito: Problemas: status 200 para erro (quebra clientes e proxies), "success: false" no corpo obriga o cliente a inspecionar sempre, "erro" não diz nada, não há código legível por máquina, não indica qual campo, não há traceId. Correto: HTTP 422, Content-Type: application/problem+json, corpo com type (URI da classe de erro), title, status: 422, detail humano, errors: [{field:"email", code:"invalid_format", message:"..."}] e traceId.
GraphQL
Objetivo: entender o modelo do GraphQL, os problemas que ele resolve (over/under-fetching) e os que ele cria (N+1, cache, segurança), e quando ele é a escolha certa.
6.1 O modelo
Você publica um endpoint (POST /graphql) e um schema tipado. O cliente manda uma query declarando exatamente a árvore de campos que quer; o servidor resolve cada campo com uma função resolver e devolve exatamente aquela forma.
# schema type Query { order(id: ID!): Order } type Order { id: ID! total: Int! customer: Customer! items: [OrderItem!]! } # query do cliente — pede só o que precisa, de várias entidades, numa ida só query { order(id: "o_123") { total customer { name email } items { productName quantity } } }
- Query (ler), Mutation (escrever), Subscription (stream de eventos via WebSocket).
- Resolve over-fetching (REST devolve campos que você não quer) e under-fetching/waterfall (REST exige N chamadas para montar uma tela).
- Schema é introspectível — ferramental excelente (GraphiQL, codegen tipado).
6.2 Os problemas que o GraphQL traz
| Problema | Explicação | Mitigação |
|---|---|---|
| N+1 | Resolver items de 100 orders dispara 100 queries ao banco, uma por order | DataLoader: agrupa (batch) e faz cache das buscas por chave dentro de uma request |
| Cache HTTP | Tudo é POST /graphql — o cache de GET do HTTP/CDN não funciona de graça | Persisted queries (o cliente envia um hash; vira GET cacheável), cache no nível de campo/entidade (Apollo), APQ |
| Consultas abusivas | Um cliente pode pedir uma query profundamente aninhada e cara (ataque ou acidente) | Limite de profundidade, análise de complexidade/custo, timeout, paginação obrigatória, allowlist de queries |
| Erros | GraphQL retorna 200 com {data, errors} — sucesso parcial é possível e confunde | Convenção de erros clara (errors como dados tipados nas mutations — "errors as data"), documentar o contrato |
| Observabilidade/rate limit | Um path (/graphql) — métricas por endpoint e rate limit por rota não distinguem operações | Instrumentar por operationName/campo; rate limit por custo |
6.3 Paginação no GraphQL
A convenção madura é cursor connections (padrão Relay): edges { node cursor } + pageInfo { hasNextPage endCursor }. É cursor-based (Módulo 3), padronizado e componível.
6.4 Quando usar (e não usar) GraphQL
| GraphQL brilha | Prefira REST/gRPC |
|---|---|
| Muitos clientes (web, iOS, Android) com necessidades de dados diferentes | API pública simples, CRUD, para integradores heterogêneos |
| Telas que agregam dados de vários serviços (BFF, gateway de dados) | Você quer cache HTTP/CDN sem esforço |
| Evolução rápida do front sem redeploy do back a cada campo novo | Uploads de arquivo, streaming binário, latência mínima entre serviços (gRPC) |
| Grafo de dados naturalmente interconectado | Time pequeno sem apetite para operar o runtime, DataLoader, custo, segurança |
Perguntas: "Que problema o GraphQL resolve em relação ao REST?" (over/under-fetching; um roundtrip para dados de várias fontes), "O que é o problema N+1 e como o DataLoader ajuda?", "Por que cache é mais difícil em GraphQL?" (tudo é POST num endpoint; persisted queries), "Como você protege um endpoint GraphQL de queries abusivas?" (depth/complexity limiting, timeout, allowlist).
✏️ Exercício 6 — Diagnóstico
Um resolver de User.posts num feed que lista 50 usuários está fazendo 51 queries ao banco e a latência é 1,2 s. Explique e corrija. Depois: o time reclama que o CDN não cacheia nada — por quê e o que fazer?
Gabarito: (1) N+1: 1 query para os 50 usuários + 1 por usuário para os posts. Corrigir com um DataLoader de posts por userId: dentro da request, ele acumula os 50 ids e faz SELECT ... WHERE user_id IN (...) numa query, com cache por chave. Latência cai para ~2 queries. (2) CDN não cacheia porque toda operação é POST /graphql com corpo variável — o cache de GET não se aplica. Solução: automatic/trusted persisted queries (o cliente manda um hash da query; o servidor serve via GET cacheável), e/ou cache de entidade no cliente (Apollo/urql) e cache de resposta no gateway por operação.
gRPC e RPC
Objetivo: entender o modelo RPC com contrato Protobuf, os modos de streaming, a evolução de schema, e onde gRPC ganha de REST e de GraphQL.
7.1 O modelo
gRPC é "chamar uma função remota": você define o serviço e as mensagens em Protocol Buffers (.proto), o compilador gera stubs de cliente e servidor em várias linguagens, e a chamada trafega serializada em binário sobre HTTP/2 (multiplexação, header compression, streaming).
// pedido.proto — o contrato, versionável e polyglot syntax = "proto3"; service OrderService { rpc GetOrder(GetOrderRequest) returns (Order); rpc WatchOrders(WatchRequest) returns (stream OrderEvent); // server streaming } message GetOrderRequest { string order_id = 1; } message Order { string id = 1; int64 total_cents = 2; Status status = 3; enum Status { PENDING = 0; PAID = 1; SHIPPED = 2; } }
7.2 Os quatro modos de chamada
- Unary: request → response (como uma chamada normal).
- Server streaming: request → stream de responses (feed de eventos, resultados grandes em pedaços).
- Client streaming: stream de requests → response (upload em chunks, telemetria).
- Bidirecional: os dois streams ao mesmo tempo (chat, sincronização ao vivo).
7.3 Evolução de schema em Protobuf
Compatibilidade se ganha respeitando as regras dos números de campo (a "tag" que vai no wire, não o nome):
- Nunca reutilize nem mude o número de um campo. Adicionar campo = novo número — clientes antigos ignoram o que não conhecem.
- Ao remover um campo, marque o número (e o nome) como
reservedpara ninguém reaproveitar. - Renomear é seguro no wire (o nome não trafega), mas quebra o código gerado — trate como mudança de API.
- Mudar o tipo de um campo geralmente quebra; alguns tipos são compatíveis no wire (int32/int64/bool), mas não conte com isso.
- Campos são opcionais no proto3; ausência = valor default — projete pensando nisso.
7.4 Quando gRPC
| gRPC ganha | Cuidados |
|---|---|
| Comunicação interna entre microserviços — baixa latência, payload compacto, contrato forte | Navegadores não falam gRPC direto — precisa de gRPC-Web + proxy (Envoy), com limitações |
| Ambientes polyglot (Go chama Java chama Python) com um contrato só | Menos "explorável" que REST — sem curl trivial; ferramentas: grpcurl, Buf |
| Streaming nativo nos dois sentidos | Debug e observabilidade exigem ferramental próprio |
| APIs de alta vazão (o Protobuf serializa/parseia muito mais rápido que JSON) | APIs públicas para terceiros: REST ainda é o esperado |
| Geração de código elimina o "cliente HTTP escrito à mão" | Governança do .proto (Buf Schema Registry, lint, breaking-change detection) |
7.5 A comparação final, agora informada
| Critério | REST | GraphQL | gRPC |
|---|---|---|---|
| Público / terceiros | ★★★ o padrão | ★★ ok | ★ raro |
| Front-end com dados de várias fontes | ★ | ★★★ | ★ |
| Entre microserviços internos | ★★ | ★ | ★★★ |
| Cache HTTP / CDN | ★★★ | ★ (persisted queries) | ★ |
| Streaming | ★ (SSE/WebSocket à parte) | ★★ (subscriptions) | ★★★ |
| Performance de serialização | ★★ (JSON) | ★★ (JSON) | ★★★ (binário) |
| Curva / ferramental universal | ★★★ | ★★ | ★★ |
Arquitetura típica: REST na fronteira pública, gRPC entre serviços, GraphQL no BFF que serve os apps.
Perguntas: "Quando gRPC em vez de REST?" (interno, alta vazão, streaming, polyglot; não para browser/público sem proxy), "Como evoluir um schema Protobuf sem quebrar clientes?" (não mudar/reusar números de campo; reserved ao remover; só adicionar), "Os 4 modos de streaming do gRPC", "Por que Protobuf é mais rápido que JSON?" (binário, schema conhecido, sem parsing de texto).
✏️ Exercício 7 — Evolua o contrato
Você tem message User { string id = 1; string name = 2; string email = 3; } em produção. Precisa: (a) adicionar phone; (b) remover email (foi para outro serviço); (c) dividir name em first_name/last_name. O que fazer com compatibilidade?
Gabarito: (a) Adicionar string phone = 4; — número novo, clientes antigos ignoram. (b) Remover o campo do código, mas manter reserved 3; reserved "email"; para o número 3 nunca ser reutilizado; clientes antigos que ainda pedem email recebem string vazia (default) — comunicar deprecação antes. (c) Não mexer no name (número 2) de imediato: adicionar first_name = 5 e last_name = 6, preencher os três por um tempo (transição), atualizar clientes, e só então marcar name como deprecated e, muito depois, reserved. Nunca trocar o que o número 2 significa.
Segurança de API
Objetivo: autenticação, autorização, limites e validação — e o OWASP API Security Top 10, cujo campeão (BOLA) é um erro de design, não de código.
8.1 Autenticação (quem é você)
| Mecanismo | Uso | Notas |
|---|---|---|
| API key | Server-to-server simples, identificar a aplicação | Fácil; não expira sozinha; rotacionável; não identifica o usuário final. Nunca no front-end público. |
| OAuth 2.0 / OIDC | Acesso delegado; usuários; apps de terceiros | O padrão. Authorization Code + PKCE para apps; Client Credentials para máquina-a-máquina. OIDC adiciona identidade (ID token). |
| JWT (Bearer) | Token de acesso auto-contido | Rápido (sem consulta), mas não revogável facilmente — use expiração curta + refresh token. Valide assinatura, iss, aud, exp. Nunca aceite alg: none. Não guarde dado sensível (é só base64). |
| mTLS | Serviços internos, parceiros de alta confiança | Autenticação mútua por certificado; forte; operacionalmente mais pesado. |
8.2 Autorização (o que você pode fazer)
- Scopes (OAuth): o token carrega
orders:read,orders:write— granularidade grossa, boa para consentimento de terceiros. - RBAC: papéis (
admin,support) mapeiam para permissões. - ABAC / policy engine (OPA, Cedar): decisão baseada em atributos (dono do recurso, tenant, horário, IP) — para regras finas.
- A verificação acontece no servidor, por requisição, sobre o recurso concreto — nunca confie que o cliente "só vai pedir o que pode".
8.3 OWASP API Security Top 10 (essencial)
| Risco | O que é |
|---|---|
| API1: BOLA (Broken Object Level Authorization) | O nº 1. GET /orders/123 retorna o pedido de qualquer um — o servidor não checa se o pedido 123 pertence ao usuário autenticado. Trocar o id (IDOR) vaza tudo. Defesa: checar posse/permissão do objeto em toda requisição; ids opacos ajudam mas não bastam. |
| API2: Autenticação quebrada | Fluxos de login/refresh/reset fracos, JWT mal validado, sem rate limit no login. |
| API3: Object Property Level Auth | Mass assignment (o cliente manda "role":"admin" e o servidor aceita) ou excessive data exposure (a resposta traz campos sensíveis que o front "não mostra"). Defesa: allowlist de campos na entrada e na saída (DTOs), nunca serializar o modelo do banco cru. |
| API4: Consumo irrestrito de recursos | Sem rate limit, sem paginação obrigatória, sem limite de tamanho — DoS e custo. (Ver apostilas de Arquitetura e de Otimização de Custos.) |
| API5: BFLA (Function Level Auth) | Endpoints administrativos acessíveis a usuário comum porque "não estão no menu". |
| API6–10 | Acesso irrestrito a fluxos de negócio sensíveis; SSRF; má configuração de segurança; gestão de inventário deficiente (APIs "fantasma", versões antigas no ar); consumo inseguro de APIs de terceiros. |
A maioria dos vazamentos grandes de API é BOLA: a autorização em nível de objeto foi esquecida em algum endpoint. Não dá para resolver com um middleware genérico — cada handler que recebe um id precisa responder "este usuário pode agir sobre este recurso?". Teste isso explicitamente (um usuário tentando acessar o recurso de outro deve receber 403/404).
8.4 Outros controles
- TLS sempre, HSTS; nada de API em HTTP.
- Rate limiting e quotas por cliente/usuário/IP, com
429eRetry-After; limites diferentes por tier. - Validação de entrada rígida (Módulo 5): tipos, tamanhos, profundidade; rejeitar payloads gigantes.
- CORS com allowlist explícita (Módulo 2), nunca
*com credenciais. - Segredos em cofre, rotacionáveis; nunca em URL (ficam em log), sempre em header.
- Logs sem PII/segredos; auditoria das ações sensíveis.
- Inventário: saiba quais APIs e versões estão no ar; desligue as órfãs (API9).
Perguntas: "O que é BOLA/IDOR e como preveni-lo?" (autorização em nível de objeto em toda requisição), "OAuth2 vs API key vs JWT — quando cada um?", "Perigos do JWT" (revogação, alg:none, dado sensível, expiração longa), "O que é mass assignment e como evitar?" (DTO/allowlist de campos), "Onde colocar a checagem de autorização?" (servidor, por requisição, sobre o recurso concreto).
✏️ Exercício 8 — Ache as falhas
Uma API tem: GET /invoices/{id} que só valida o token (não checa dono); o endpoint aceita o corpo inteiro do usuário e faz Object.assign(user, body); o login não tem rate limit; o JWT expira em 30 dias; a resposta de /users/{id} inclui passwordHash. Classifique cada uma no OWASP API Top 10 e corrija.
Gabarito: (1) GET /invoices/{id} sem checar dono = API1 BOLA → validar que a invoice pertence ao usuário/tenant; retornar 404/403 caso contrário; testar com usuário alheio. (2) Object.assign(user, body) = API3 mass assignment → aceitar só um DTO com campos permitidos. (3) Login sem rate limit = API2 → rate limit + lockout progressivo + captcha. (4) JWT de 30 dias = API2 → access token curto (minutos) + refresh token rotacionável e revogável. (5) passwordHash na resposta = API3 excessive data exposure → serializar via DTO de saída, nunca o modelo cru.
Versionamento, evolução e API em escala
Objetivo: evoluir uma API sem quebrar clientes — mudanças compatíveis, estratégias de versão, deprecação — e operar muitas APIs com gateway, observabilidade e governança.
9.1 Breaking × non-breaking
| Non-breaking (seguro) | Breaking (exige versão / negociação) |
|---|---|
| Adicionar endpoint, campo opcional na resposta, valor novo raramente em enum de saída | Remover/renomear campo ou endpoint; mudar tipo |
| Adicionar parâmetro opcional com default | Tornar um parâmetro obrigatório; mudar o default |
| Afrouxar validação; adicionar novo código de erro específico | Endurecer validação; mudar código de status de um caso existente |
| Novo scope opcional | Mudar semântica de um campo/endpoint sem mudar a forma |
Regra do consumidor tolerante (robustness principle): clientes devem ignorar campos que não conhecem — isso é o que torna a adição de campos segura. Documente essa expectativa.
9.2 Estratégias de versionamento
| Estratégia | Exemplo | Prós / contras |
|---|---|---|
| Na URI | /v2/orders | Explícito, fácil de rotear e cachear; "polui" a URL e sugere versionar o recurso todo. O mais comum. |
| Header / media type | Accept: application/vnd.api+json; version=2 | URL limpa, versiona por representação; menos visível, cache exige Vary. |
| Query param | ?version=2 | Simples; fácil de esquecer; mistura versão com filtro. |
| Baseada em data (estilo Stripe) | Stripe-Version: 2026-08-01 fixada por conta | Mudanças pequenas e frequentes sem "v3, v4, v5"; o servidor mantém camadas de transformação. Poderoso, mais trabalho. |
Versionar é caro (manter N versões, migrar clientes). Projete para extensão sem quebra — campos opcionais, novos endpoints, feature flags por campo — e reserve a mudança de versão para rupturas reais e raras. Quando lançar, tenha um plano de deprecação: anúncio, header Deprecation + Sunset (RFC 8594), janela longa (6–12+ meses), métricas de quem ainda usa, e comunicação ativa.
9.3 API Gateway
Um ponto de entrada único na frente das APIs, responsável por o que não deve estar espalhado: autenticação e verificação de token, rate limiting e quotas, roteamento e versionamento, terminação TLS, transformação de request/response, agregação, cache, CORS, logging e métricas. Ferramentas: Kong, Apigee, AWS API Gateway, Azure API Management, Tyk, Envoy/Gloo. O gateway não deve conter lógica de negócio.
9.4 Observabilidade de API
- Métricas RED por rota e por versão: Rate (req/s), Errors (% de 5xx e de 4xx relevantes), Duration (p50/p95/p99).
- Tracing distribuído (OpenTelemetry): propagar
traceparent, ver a requisição atravessar gateway → serviços → banco. - Logs estruturados com request id, sem PII/segredo.
- Métricas de negócio da API: uso por cliente, por endpoint, adoção da versão nova, deprecações ainda ativas.
- SLOs por API (disponibilidade, latência) com error budget.
9.5 Governança e o mundo assíncrono
- Governança em escala: style guide + linter no CI (Módulo 4), breaking-change detection (Buf para Protobuf, oasdiff para OpenAPI), catálogo/portal de APIs, processo de revisão de design, ownership claro.
- GraphQL federation (Apollo Federation): compor um supergraph a partir de subgraphs de vários times, sem um monólito de schema.
- Webhooks: o servidor faz
POSTnum URL do cliente quando um evento acontece. Boas práticas: assinatura HMAC do payload, retries com backoff, entrega at-least-once (cliente idempotente), evento com id e tipo, endpoint de listagem para reconciliação, echallengena inscrição. - APIs assíncronas / event-driven: descritas com AsyncAPI (o "OpenAPI de mensageria"); eventos padronizados com CloudEvents. Ver apostila de Arquitetura para os padrões (outbox, saga).
Perguntas sénior: "O que é uma mudança breaking e como você evita versionar?", "Estratégias de versionamento e trade-offs", "Como você deprecia um endpoint?" (Deprecation/Sunset headers, janela longa, métricas de uso, comunicação), "O que um API gateway faz e o que ele NÃO deve fazer?" (não lógica de negócio), "Boas práticas de webhook" (assinatura HMAC, retry, idempotência, reconciliação).
✏️ Exercício 9 — Planeje a mudança
Você precisa mudar o campo full_name (string) para first_name + last_name numa API pública com centenas de integradores. Descreva o plano sem quebrar ninguém.
Gabarito: (1) Fase aditiva: adicionar first_name e last_name na resposta (mantendo full_name); na entrada, aceitar os dois novos e o antigo, com regra de precedência documentada. Non-breaking. (2) Anunciar deprecação de full_name: changelog, header Deprecation: true + Sunset: <data +12 meses> nas respostas que ainda o incluem, e-mail aos integradores, doc atualizada. (3) Monitorar quantos clientes ainda enviam/leem full_name; contatar os retardatários. (4) Só depois do sunset e do uso perto de zero, considerar remover — provavelmente numa /v2 ou versão datada, mantendo /v1 por mais um tempo. Em nenhum momento full_name muda de significado ou some sem aviso.
Mercado de trabalho: roadmap, entrevistas e portfólio
Objetivo: converter o conteúdo dos módulos anteriores em aprovação — perfis, roadmap, banco de perguntas com respostas e projetos que geram entrevista.
10.1 Onde design de API pesa
- Back-end / full-stack (pleno+): projetar endpoints, contrato, erros, paginação, auth — parte de quase toda entrevista técnica.
- Platform / API Engineer: gateway, governança, style guide, versionamento, developer portal, SDKs.
- Developer Experience / DevRel técnico: qualidade do contrato, docs, exemplos, onboarding de integradores.
- Arquiteto/Staff: escolher entre REST/GraphQL/gRPC/eventos e definir os padrões da organização.
10.2 Roadmap de estudo (6–8 semanas)
| Semanas | Foco | Prática |
|---|---|---|
| 1 | HTTP a fundo: métodos, status, headers, cache, CORS (Módulo 2) | Explorar uma API pública com curl; anotar semântica de cada resposta |
| 2 | Design REST: recursos, coleções, paginação, ações (Módulos 1, 3) | Desenhar no papel a API de um domínio (biblioteca, e-commerce) inteira |
| 3 | OpenAPI + erros + formatos (Módulos 4–5) | Escrever o openapi.yaml dessa API, com Problem Details e exemplos; rodar Spectral |
| 4 | Implementar: um serviço real contra o contrato, com validação, paginação cursor, ETag | Mock com Prism; contract test com Schemathesis |
| 5 | GraphQL: schema, resolvers, DataLoader, cursor connections, segurança (Módulo 6) | Expor o mesmo domínio em GraphQL; provocar e corrigir um N+1 |
| 6 | gRPC: .proto, streaming, evolução de schema (Módulo 7) | Um serviço gRPC com server streaming; simular uma mudança de schema compatível |
| 7 | Segurança: OAuth2/OIDC, JWT, OWASP API Top 10 (Módulo 8) | Adicionar auth + checagem de BOLA + rate limit; escrever testes de autorização negativa |
| 8 | Versionamento, gateway, observabilidade, webhooks (Módulo 9); portfólio | Plano de deprecação de um campo; métricas RED; um endpoint de webhook assinado |
10.3 Banco de perguntas de entrevista (com a resposta que aprova)
Júnior/pleno — "Diferença entre PUT, PATCH e POST"
POST: cria ou executa ação; não idempotente (dois POSTs de "criar" criam dois). PUT: substitui o recurso inteiro (ou cria com id conhecido); idempotente. PATCH: atualização parcial; não necessariamente idempotente. Consequência prática: PUT/DELETE podem ser reexecutados com segurança em retry; POST que cria precisa de chave de idempotência.
Pleno — "Offset ou cursor pagination?"
Cursor (keyset) para qualquer coisa que escale: performance estável em qualquer profundidade (usa índice, não varre-e-descarta) e consistente sob escrita concorrente. Offset só para datasets pequenos ou UI que precisa "ir para a página 7". Sempre com limit máximo e metadados (next_cursor/links).
Pleno — "Como você padroniza erros?"
Um formato único para toda a API — Problem Details (RFC 9457): type (URI da classe de erro, estável), title, status, detail, lista de erros por campo com código legível por máquina + mensagem humana, e um traceId. Status HTTP correto (não 200 com erro no corpo). Retornar todos os erros de validação de uma vez. Nunca stack trace em produção.
Pleno/sénior — "REST, GraphQL ou gRPC para [cenário]?"
REST: API pública/terceiros, CRUD, cacheável. GraphQL: muitos front-ends com necessidades diferentes, telas que agregam várias fontes, evolução rápida do cliente — ao custo de N+1, cache e segurança de query. gRPC: entre serviços internos, alta vazão, streaming, polyglot — não para browser sem proxy. Comum: os três juntos (REST na borda, gRPC interno, GraphQL no BFF).
Sénior — "O que é BOLA e como preveni-lo?"
Broken Object Level Authorization: o endpoint valida que você está autenticado, mas não que este objeto é seu — trocar o id no path vaza dados de outros. É o risco nº 1 do OWASP API Top 10. Prevenção: checar posse/permissão sobre o recurso concreto em toda requisição que recebe um id; testar explicitamente com um usuário tentando acessar o recurso de outro (deve dar 403/404). Ids opacos ajudam mas não substituem a checagem.
Sénior — "Como evoluir uma API sem quebrar clientes?"
Preferir mudanças aditivas (novos campos opcionais, novos endpoints) e o princípio do consumidor tolerante (ignora o que não conhece). Reservar versionamento para rupturas reais. Ao mudar: fase aditiva mantendo o antigo, anúncio de deprecação com headers Deprecation/Sunset, janela longa (6–12+ meses), métricas de quem ainda usa, comunicação ativa, e só então remover — geralmente numa nova versão, mantendo a antiga por um tempo.
Armadilha — "Nossa API REST retorna 200 sempre, o erro vai no corpo"
Anti-padrão: quebra clientes HTTP, proxies, caches e ferramentas que confiam no status; obriga todo consumidor a inspecionar o corpo em toda resposta. O status HTTP existe para isso — 4xx para erro do cliente, 5xx para erro do servidor — e o corpo (Problem Details) dá o detalhe. Idem para GraphQL: entender que 200 com errors é do protocolo, mas o contrato de erros precisa ser explícito.
10.4 Projetos de portfólio que geram entrevista
- API completa design-first (âncora): um domínio real,
openapi.yamlcaprichado (Problem Details, cursor pagination, ETag, auth), implementação que passa em contract test, docs geradas, e um README explicando cada decisão de design e o plano de versionamento. - O mesmo domínio nos três estilos: REST, GraphQL e gRPC, com um texto comparando trade-offs medidos (payload, latência, DX).
- Estudo de segurança: uma API com 5 vulnerabilidades do OWASP API Top 10 plantadas e depois corrigidas, com testes de autorização negativa demonstrando o antes/depois.
- Kit de evolução: demonstrar uma mudança breaking conduzida sem quebrar clientes — fase aditiva, headers de deprecação, detecção de breaking change no CI (oasdiff/Buf).
- Style guide + linter: um guia de estilo de API escrito e um conjunto de regras Spectral que o aplica, rodando em CI sobre a spec.
10.5 Fontes para continuar
- Guidelines: as API design guides públicas de Google (AIP), Microsoft REST, Zalando, Stripe; o "API Stylebook".
- REST/HTTP: as RFCs de HTTP semantics (9110), Problem Details (9457), Sunset (8594); Web API Design (Apigee); Design and Build Great Web APIs (Mike Amundsen).
- GraphQL: a spec oficial, o guia de "Principled GraphQL" (Apollo), o padrão Relay de connections.
- gRPC / Protobuf: a documentação do gRPC, o guia de style e de evolução de Protobuf, e as ferramentas Buf.
- Segurança: OWASP API Security Top 10; OWASP Cheat Sheets (REST, JWT, Authorization).
Quatro ideias sustentam design de API: (1) a API é um contrato de longa duração com pessoas que você não controla — projete para não quebrar; (2) use a semântica do HTTP (métodos, status, cache, idempotência) — a maioria dos erros é ignorá-la; (3) consistência > perfeição — um style guide aplicado por linter vale mais que endpoints individualmente elegantes; (4) a autorização em nível de objeto é a falha nº 1 e mora no design, não num middleware. REST, GraphQL e gRPC são ferramentas — escolha pela força de cada uma e combine-as.