OpenAPI não precisa significar um framework enorme ou um gerador mágico que toma conta do projeto. Em Zig, ele funciona melhor como contrato de fronteira: um arquivo versionado que descreve rotas, métodos, corpos JSON, códigos de resposta e regras mínimas que clientes e servidores devem respeitar. O serviço continua pequeno, explícito e compilado como sempre. O contrato só evita que a API vire uma coleção de decisões escondidas no código.
Isso é especialmente útil quando um binário Zig conversa com front-end, aplicativos móveis, CLIs internas, workers, integrações de parceiros ou serviços escritos em Go, Python, Kotlin e Rust. Sem um contrato claro, cada mudança de campo vira uma aposta: alguém lembra de atualizar o cliente? A resposta 404 tem o mesmo formato da 422? O campo opcional aceita null ou simplesmente não aparece? O endpoint novo está documentado no README ou só no handler?
Este guia mostra uma forma prática de usar OpenAPI em Zig sem perder a filosofia da linguagem: contrato simples, tipos explícitos, validação no limite da aplicação, testes de compatibilidade e geração de clientes apenas onde ela reduz trabalho real. Para aprofundar as peças citadas aqui, leia também API REST em Zig, parsing JSON em Zig, validação de JSON, HTTP client em Zig e testes em Zig.
Quando OpenAPI vale o custo
Se a API tem um único consumidor dentro do mesmo repositório, um teste de integração bem escrito talvez seja suficiente. OpenAPI começa a pagar quando existe mais de uma fronteira humana ou técnica.
Use um contrato quando você tem:
- front-end consumindo endpoints Zig;
- CLI ou SDK interno que chama a API;
- outro time ou agente gerando integrações;
- webhooks recebidos ou enviados;
- versionamento público de rotas;
- testes de regressão para payloads JSON;
- documentação que precisa sobreviver ao autor original;
- mudança frequente em campos de request e response.
Evite usar OpenAPI como desculpa para projetar uma plataforma antes da hora. O arquivo deve cobrir a superfície pública real. Se você ainda está explorando nomes de campos em um protótipo local, comece com testes e exemplos. Quando a rota vira contrato entre processos, promova para OpenAPI.
O papel certo do contrato em Zig
OpenAPI não substitui o código Zig. Ele descreve a promessa externa. O handler ainda precisa fazer parsing, checar limites, validar regras de negócio e mapear erros.
Uma divisão saudável fica assim:
| Camada | Responsabilidade |
|---|---|
openapi.yaml | métodos, caminhos, schemas, exemplos, códigos de resposta |
| tipos Zig | representação interna usada pelo serviço |
| parser JSON | transformar bytes em valores com limite de memória |
| validação | regras que o schema não expressa bem |
| testes | provar que exemplos e handlers continuam compatíveis |
| cliente gerado/manual | reduzir repetição do lado consumidor |
O erro comum é tentar gerar todo o servidor a partir do contrato. Isso costuma produzir abstrações piores que o próprio problema: roteadores pouco idiomáticos, tipos grandes demais, alocação invisível e mensagens de erro genéricas. Em Zig, prefira o inverso: escreva o serviço explicitamente e use OpenAPI como artefato de compatibilidade.
Comece pequeno com um openapi.yaml
Um contrato inicial pode caber em um arquivo curto. Ele não precisa listar todos os detalhes internos, mas deve ser preciso nos nomes e formatos que cruzam a rede.
openapi: 3.1.0
info:
title: Exemplo Zig API
version: 1.0.0
paths:
/v1/jobs:
post:
summary: Cria uma tarefa em background
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateJobRequest'
responses:
'202':
description: Tarefa aceita
content:
application/json:
schema:
$ref: '#/components/schemas/JobAccepted'
'422':
description: Payload inválido
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
CreateJobRequest:
type: object
required: [kind, payload]
additionalProperties: false
properties:
kind:
type: string
enum: [relatorio, webhook, compactacao]
payload:
type: object
JobAccepted:
type: object
required: [id, status]
properties:
id:
type: string
status:
type: string
enum: [queued]
ApiError:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
Esse arquivo já responde perguntas importantes: a rota é POST, aceita JSON, retorna 202 em caso de sucesso e usa um formato padronizado para erro de validação. O additionalProperties: false também força uma decisão: campos desconhecidos são rejeitados ou ignorados?
Mapeando schema para tipos Zig
Não tente representar cada detalhe de JSON Schema no tipo Zig. Use o tipo para o que o serviço realmente consome e deixe validações de contrato nos testes ou na borda.
const std = @import("std");
const JobKind = enum {
relatorio,
webhook,
compactacao,
};
const CreateJobRequest = struct {
kind: JobKind,
payload: std.json.Value,
};
const JobAccepted = struct {
id: []const u8,
status: []const u8 = "queued",
};
Esse desenho deixa duas coisas explícitas. Primeiro, kind virou enum, então valores desconhecidos falham cedo. Segundo, payload continua como std.json.Value porque cada tipo de job pode ter regra própria. Se você tentar modelar todos os payloads em uma única struct, a API pode ficar rígida demais antes de o domínio estabilizar.
Para requests simples, uma struct completa é melhor. Para payloads heterogêneos, valide por etapas: envelope primeiro, regra específica depois.
Parsing com limite de memória
Contrato nenhum protege o processo se você aceitar qualquer corpo de request. Antes de chamar std.json.parseFromSlice, imponha limite de bytes no servidor HTTP. Depois, parseie com allocator controlado e libere tudo no fim da requisição.
fn parseCreateJob(allocator: std.mem.Allocator, body: []const u8) !CreateJobRequest {
if (body.len > 64 * 1024) return error.BodyTooLarge;
var parsed = try std.json.parseFromSlice(
CreateJobRequest,
allocator,
body,
.{ .ignore_unknown_fields = false },
);
defer parsed.deinit();
return .{
.kind = parsed.value.kind,
.payload = try parsed.value.payload.cloneWithAllocator(allocator),
};
}
O detalhe importante é a política para campos desconhecidos. Se o contrato declara additionalProperties: false, o parser deve rejeitar o que não conhece. Se você precisa compatibilidade futura, documente isso no contrato e teste que o handler ignora campos extras de forma intencional.
Erros também fazem parte do contrato
Muita API documenta apenas o caminho feliz. Em produção, o formato de erro é tão importante quanto o payload de sucesso, porque clientes automatizados dependem dele para retry, mensagem ao usuário e observabilidade.
Padronize pelo menos quatro famílias:
| Código HTTP | Uso | Corpo recomendado |
|---|---|---|
400 | JSON malformado | code, message |
401/403 | autenticação/autorização | code, message |
404 | recurso não encontrado | code, message |
422 | JSON válido, regra inválida | code, message, field opcional |
500 | falha interna | code, message sem detalhe sensível |
Em Zig, isso combina com error union. O handler pode mapear erros de domínio para respostas HTTP sem expor stack trace, SQL, path local ou segredo.
const ApiError = struct {
code: []const u8,
message: []const u8,
field: ?[]const u8 = null,
};
fn errorResponse(err: anyerror) ApiError {
return switch (err) {
error.BodyTooLarge => .{ .code = "body_too_large", .message = "Corpo JSON excede o limite." },
error.UnknownJobKind => .{ .code = "invalid_kind", .message = "Tipo de tarefa desconhecido.", .field = "kind" },
else => .{ .code = "internal_error", .message = "Falha interna." },
};
}
Documente esses formatos no OpenAPI. Cliente que só sabe tratar 200 e 500 vai quebrar no primeiro problema real.
Testando exemplos do contrato
O melhor uso de OpenAPI em um projeto Zig pequeno é transformar exemplos em testes. Se o contrato mostra um payload aceito, o parser deve aceitar. Se mostra um erro esperado, o handler deve devolver aquele código.
Uma abordagem simples:
- mantenha exemplos JSON em
testdata/openapi/; - referencie os mesmos exemplos no
openapi.yaml; - escreva testes Zig que leem esses arquivos;
- rode os testes no CI junto com
zig fmt --checkezig build test.
test "CreateJobRequest aceita exemplo oficial" {
const allocator = std.testing.allocator;
const body = @embedFile("../testdata/openapi/create-job-request.json");
const req = try parseCreateJob(allocator, body);
try std.testing.expectEqual(JobKind.relatorio, req.kind);
}
Esse padrão reduz drift porque o exemplo que aparece para o consumidor é o mesmo que o serviço compila nos testes. Se alguém renomeia kind para type, o teste quebra antes do deploy.
Validando o documento no CI
Mesmo sem gerador, valide o openapi.yaml. Use uma ferramenta externa no CI e mantenha o Zig livre de dependência pesada. Duas opções comuns são redocly lint e swagger-cli validate. A escolha importa menos que rodar sempre.
Um pipeline mínimo:
name: api-contract
on: [push, pull_request]
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx --yes @redocly/cli lint openapi.yaml
- run: zig fmt --check src test
- run: zig build test
Se o projeto evita Node no CI, use um container ou binário dedicado. O ponto é que o contrato deve falhar rápido quando alguém remove schema usado por cliente, duplica operação ou escreve YAML inválido.
Clientes: gerar tudo ou escrever fino?
Gerar um cliente completo pode ser útil para TypeScript, Kotlin ou Python, mas nem sempre vale a pena para Zig. Muitas APIs internas precisam de meia dúzia de funções explícitas, não de centenas de tipos gerados.
Uma camada manual em Zig costuma ser suficiente:
pub fn createJob(client: *std.http.Client, allocator: std.mem.Allocator, base_url: []const u8, req: CreateJobRequest) !JobAccepted {
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
try std.json.stringify(req, .{}, body.writer());
const url = try std.fmt.allocPrint(allocator, "{s}/v1/jobs", .{base_url});
defer allocator.free(url);
// Monte request, envie body, cheque status 202 e parseie resposta.
// Em código real, centralize headers, timeout e mapeamento de ApiError.
_ = client;
return error.NotImplemented;
}
O contrato ainda ajuda: ele define status esperado, schema de erro, campos obrigatórios e exemplos. A implementação Zig fica pequena e legível.
Para consumidores TypeScript, Java ou Go, pode fazer sentido gerar cliente a partir do OpenAPI. Nesse caso, trate o código gerado como artefato de fronteira, não como modelo de domínio. Ele existe para atravessar HTTP, não para mandar no desenho interno do serviço. Se a equipe mantém serviços em Go também, compare esse limite com o guia de APIs REST em Go antes de padronizar geração em todos os projetos.
Versionamento sem quebrar consumidores
OpenAPI torna quebra de contrato visível, mas não decide sua política. Defina regras antes de a API crescer.
Mudanças geralmente seguras:
- adicionar campo opcional em resposta;
- adicionar novo endpoint;
- adicionar novo código de erro documentado;
- aceitar enum novo quando clientes ignoram valores desconhecidos.
Mudanças quebradoras:
- renomear campo obrigatório;
- remover campo de resposta usado por cliente;
- mudar tipo de
stringparanumber; - trocar
202por200sem aviso; - transformar campo ausente em
nullou o inverso; - mudar semântica de paginação.
Para APIs internas, versionamento pode ser simples: contrato no mesmo monorepo, PR revisado por donos dos consumidores e testes de compatibilidade. Para APIs externas, prefira /v1, changelog e janela de migração.
Segurança e documentação honesta
Contrato não deve vazar segredo. Não coloque token real em exemplo, URL interna sensível, nome de bucket privado ou payload de cliente. Use exemplos plausíveis e sanitizados.
Também não prometa mais estabilidade do que existe. Se uma rota é experimental, marque com x-beta: true, tag experimental ou descrição clara. Se o endpoint só funciona para uso interno, diga isso. Documentação honesta evita integração errada.
Para endpoints autenticados, documente o mecanismo sem expor implementação desnecessária:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
Depois alinhe com a prática do serviço: logs sem token, mensagens de erro sem segredo e testes que garantem 401/403 previsíveis.
Checklist de adoção
Antes de dizer que a API tem contrato, confirme:
- existe
openapi.yamlversionado no repositório; - rotas públicas têm request, response e erro documentados;
- exemplos JSON são usados por testes Zig;
- CI valida o documento;
- handlers impõem limite de corpo antes de parsear;
- parser e schema concordam sobre campos desconhecidos;
- erros seguem formato comum;
- mudanças quebradoras exigem revisão dos consumidores;
- documentação não contém segredo real;
- pelo menos um cliente ou teste consome o contrato.
OpenAPI em Zig deve deixar o serviço mais previsível, não mais burocrático. Use o contrato para estabilizar fronteiras, impedir drift e ajudar clientes. Mantenha o core em Zig claro, pequeno e explícito.
Perguntas frequentes
Preciso gerar código Zig a partir do OpenAPI?
Na maioria dos projetos pequenos, não. O ganho maior vem de validar o contrato, compartilhar exemplos e testar handlers. Gere código quando houver muitos endpoints repetitivos ou quando consumidores externos exigirem SDK.
OpenAPI substitui testes de integração?
Não. Ele descreve a promessa. Testes provam que o servidor cumpre a promessa. O melhor arranjo é usar exemplos do contrato dentro dos testes.
Posso usar JSON Schema completo no runtime Zig?
Pode, mas avalie custo. Muitas vezes é melhor validar campos críticos com código Zig explícito e deixar a validação completa do documento para CI e testes de contrato.
Como lidar com streaming, SSE ou WebSocket?
OpenAPI cobre bem HTTP request/response tradicional. Para Server-Sent Events e WebSockets, documente handshake, eventos, mensagens e erros com texto e exemplos; não force tudo em schemas quando o formato é contínuo.