---
title: "API REST em Zig: Rotas, JSON, Erros e Deploy em Produção"
url: "https://ziglang.com.br/artigos/zig-api-rest-completa/"
markdown_url: "https://ziglang.com.br/artigos/zig-api-rest-completa.MD"
description: "Guia prático para criar API REST em Zig em 2026: roteamento explícito, JSON, validação, erros, limites de body, health checks, testes, Docker e observabilidade."
date: "2026-02-21"
author: ""
---

# API REST em Zig: Rotas, JSON, Erros e Deploy em Produção

Guia prático para criar API REST em Zig em 2026: roteamento explícito, JSON, validação, erros, limites de body, health checks, testes, Docker e observabilidade.


Construir uma **API REST em Zig** é diferente de repetir o modelo de um framework web pronto. Zig não tenta esconder o custo de cada alocação, cópia ou bloqueio; por isso, uma API bem escrita tende a ser pequena, previsível e fácil de empacotar. A troca é clara: você escreve mais infraestrutura explícita, mas ganha controle sobre memória, erros, limites e formato do binário.

Este guia mostra um caminho realista para uma API REST em Zig em 2026: estrutura de projeto, roteamento, JSON, validação, tratamento de erros, limites de body, health checks, testes, deploy em container e observabilidade. Ele complementa os guias de [servidor HTTP em Zig](/tutoriais/zig-http-server/), [servidor HTTP em produção](/artigos/zig-http-server-producao/), [integração com bancos de dados](/artigos/zig-banco-dados-integracoes/), [OpenTelemetry](/artigos/zig-opentelemetry-traces-metricas-logs/) e [configuração segura com variáveis de ambiente](/artigos/zig-configuracao-segura-segredos-env/).

## Quando faz sentido usar Zig para uma API REST

Zig faz mais sentido quando a API precisa de uma destas características:

- binário estático pequeno, fácil de distribuir em container mínimo;
- startup rápido para serviços pequenos, workers ou edge-like deployments;
- uso de memória previsível, sem garbage collector;
- integração com bibliotecas C ou código de sistemas;
- controle explícito de parsing, limites e erros;
- baixa dependência de framework.

Ele faz menos sentido quando o produto precisa de um ecossistema web gigantesco agora: ORM maduro, autenticação social pronta, OpenAPI gerado automaticamente, middlewares de terceiros e centenas de integrações. Nesses casos, Go, Kotlin, Node.js ou Python podem entregar mais rápido. Uma boa comparação prática é olhar como <a href="https://golang.com.br/aprenda/api-rest-go/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go estrutura APIs REST idiomáticas</a> e como <a href="https://kotlin.dev.br/guias/guia-kotlin-rest-api/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'kotlin.dev.br' })">Kotlin organiza APIs REST na JVM</a>.

## Estrutura mínima de projeto

Uma API pequena pode começar com poucos arquivos e crescer por módulos. Evite criar camadas abstratas antes de ter rotas reais.

```text
minha-api/
├── build.zig
├── build.zig.zon
└── src/
    ├── main.zig
    ├── config.zig
    ├── router.zig
    ├── http_error.zig
    ├── handlers/
    │   ├── health.zig
    │   └── usuarios.zig
    └── models/
        └── usuario.zig
```

A separação importante é simples: `main.zig` inicializa configuração, allocator e servidor; `router.zig` decide qual handler atende a rota; handlers leem entrada, validam e devolvem resposta; modelos representam dados estáveis. Se o projeto crescer, adicione módulos por domínio antes de inventar um framework interno.

## Modelo de dados e serialização JSON

Zig não obriga você a usar reflexão dinâmica. Para respostas pequenas, um `std.json.stringify` ou writer explícito resolve. Para APIs públicas, prefira um contrato estável e teste a saída.

```zig
const std = @import("std");

pub const Usuario = struct {
    id: u64,
    nome: []const u8,
    email: []const u8,
    ativo: bool,
};

pub fn escreverUsuarioJson(writer: anytype, usuario: Usuario) !void {
    try std.json.stringify(usuario, .{}, writer);
}
```

Para entrada, parseie para uma struct pequena que represente o payload aceito, não diretamente para o modelo final. Isso deixa validação e defaults mais claros.

```zig
const CriarUsuarioInput = struct {
    nome: []const u8,
    email: []const u8,
};

fn validarInput(input: CriarUsuarioInput) !void {
    if (input.nome.len < 2) return error.NomeMuitoCurto;
    if (input.email.len > 320) return error.EmailMuitoLongo;
    if (std.mem.indexOfScalar(u8, input.email, '@') == null) return error.EmailInvalido;
}
```

O detalhe de produção é o allocator: valores parseados por `std.json.parseFromSlice` podem apontar para memória administrada pelo parse result. Use `defer parsed.deinit()` e copie strings se elas precisarem sobreviver depois da requisição.

## Roteamento explícito

Um roteador simples pode ser uma lista de rotas estáticas. Em muitos serviços internos, isso é suficiente e mais previsível do que uma árvore dinâmica.

```zig
const std = @import("std");

const Method = enum { GET, POST, PUT, DELETE };

const Request = struct {
    method: Method,
    path: []const u8,
    body: []const u8,
};

const Response = struct {
    status: u16,
    body: []const u8,
    content_type: []const u8 = "application/json; charset=utf-8",
};

const Handler = *const fn (std.mem.Allocator, Request) anyerror!Response;

const Route = struct {
    method: Method,
    path: []const u8,
    handler: Handler,
};

pub const Router = struct {
    routes: []const Route,

    pub fn dispatch(self: Router, allocator: std.mem.Allocator, req: Request) !Response {
        for (self.routes) |route| {
            if (route.method == req.method and std.mem.eql(u8, route.path, req.path)) {
                return route.handler(allocator, req);
            }
        }

        return Response{ .status = 404, .body = "{\"erro\":\"rota nao encontrada\"}" };
    }
};
```

Esse exemplo separa `Request` e `Response` do tipo concreto de `std.http.Server`. Na prática, isso facilita testes: você consegue chamar handlers sem abrir socket. Depois, um adaptador fino traduz a requisição HTTP real para essa estrutura.

## Handler de criação com limite de body

APIs em produção precisam de limite explícito de payload. Sem isso, uma rota simples pode virar consumo excessivo de memória.

```zig
const std = @import("std");
const Usuario = @import("models/usuario.zig").Usuario;

const MAX_BODY = 16 * 1024;

pub fn criarUsuario(allocator: std.mem.Allocator, req: Request) !Response {
    if (req.body.len == 0) return jsonErro(400, "body obrigatório");
    if (req.body.len > MAX_BODY) return jsonErro(413, "body muito grande");

    const parsed = std.json.parseFromSlice(
        CriarUsuarioInput,
        allocator,
        req.body,
        .{ .ignore_unknown_fields = true },
    ) catch return jsonErro(400, "JSON inválido");
    defer parsed.deinit();

    validarInput(parsed.value) catch |err| switch (err) {
        error.NomeMuitoCurto => return jsonErro(422, "nome muito curto"),
        error.EmailMuitoLongo => return jsonErro(422, "email muito longo"),
        error.EmailInvalido => return jsonErro(422, "email inválido"),
        else => return err,
    };

    const usuario = Usuario{
        .id = 1,
        .nome = parsed.value.nome,
        .email = parsed.value.email,
        .ativo = true,
    };

    var out = std.ArrayList(u8).init(allocator);
    try std.json.stringify(usuario, .{}, out.writer());
    return Response{ .status = 201, .body = try out.toOwnedSlice() };
}

fn jsonErro(status: u16, msg: []const u8) Response {
    _ = msg; // Em produção, escape a mensagem ou use stringify.
    return Response{ .status = status, .body = "{\"erro\":\"requisição inválida\"}" };
}
```

O exemplo acima é propositalmente compacto. Em uma API real, `jsonErro` deve serializar a mensagem com escaping correto, incluir um código estável (`VALIDATION_ERROR`, `NOT_FOUND`, `CONFLICT`) e talvez um `trace_id` para suporte.

## Adaptador para `std.http.Server`

A API de `std.http.Server` muda entre versões do Zig, então trate o adaptador HTTP como borda do sistema. O domínio da aplicação deve continuar testável mesmo se você ajustar detalhes do servidor.

```zig
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const routes = [_]Route{
        .{ .method = .GET, .path = "/health", .handler = health },
        .{ .method = .POST, .path = "/api/v1/usuarios", .handler = criarUsuario },
    };
    const router = Router{ .routes = &routes };

    // Pseudocódigo de borda: adapte conforme a versão do Zig usada no projeto.
    // 1. escute 0.0.0.0:8080;
    // 2. leia método, path e body com limite;
    // 3. chame router.dispatch;
    // 4. escreva status, Content-Type e body.
    _ = router;
    _ = allocator;
}
```

A regra operacional é: fixe a versão do Zig no CI e documente a API do servidor usada. Zig ainda caminha para 1.0; exemplos copiados de versões diferentes podem quebrar. Veja também o guia de [Zig em 2026](/artigos/zig-em-2026-estado-atual/) para entender estabilidade e roadmap.

## Erros HTTP e contrato de resposta

Uma API REST confiável não deve vazar stack trace, `@errorName` interno ou detalhes de banco. Mapeie erros do domínio para respostas estáveis.

| Caso | Status | Corpo sugerido |
|---|---:|---|
| JSON malformado | 400 | `{"error":"BAD_JSON"}` |
| Validação falhou | 422 | `{"error":"VALIDATION_ERROR","field":"email"}` |
| Recurso não encontrado | 404 | `{"error":"NOT_FOUND"}` |
| Conflito de unicidade | 409 | `{"error":"CONFLICT"}` |
| Limite de body | 413 | `{"error":"PAYLOAD_TOO_LARGE"}` |
| Falha interna | 500 | `{"error":"INTERNAL_ERROR","trace_id":"..."}` |

O usuário da API precisa de previsibilidade, não de uma frase nova a cada deploy. Logs internos podem carregar o erro detalhado; a resposta pública deve ser estável.

## Configuração, segredos e health checks

Carregue configuração uma vez no boot. Porta, ambiente, string de conexão, nível de log e limites devem vir de variáveis de ambiente ou arquivo de config controlado. Segredos não devem entrar em `build.zig`, logs ou HTML de erro.

Health checks merecem duas rotas diferentes quando o serviço roda em orquestrador:

- `/health/live`: o processo está vivo e consegue responder;
- `/health/ready`: dependências mínimas estão prontas, como banco ou fila.

Isso evita matar um processo saudável apenas porque uma dependência oscilou por segundos. Para mais detalhes, veja [configuração segura em Zig](/artigos/zig-configuracao-segura-segredos-env/) e [observabilidade em Zig](/artigos/zig-observabilidade/).

## Testes de handler sem abrir socket

A vantagem de separar roteador e adaptador HTTP aparece nos testes. Você pode validar status e corpo chamando a função diretamente.

```zig
test "POST /usuarios rejeita email inválido" {
    const allocator = std.testing.allocator;
    const req = Request{
        .method = .POST,
        .path = "/api/v1/usuarios",
        .body = "{\"nome\":\"Ana\",\"email\":\"sem-arroba\"}",
    };

    const resp = try criarUsuario(allocator, req);
    try std.testing.expectEqual(@as(u16, 422), resp.status);
}
```

Além de testes unitários, mantenha um smoke test de processo: sobe o binário em porta temporária, chama `/health`, faz um POST válido e um inválido, depois encerra. Esse teste pega erro de adaptador, cabeçalhos, parsing real e empacotamento.

## Deploy: container pequeno, usuário não-root e limites

A API REST em Zig combina bem com container mínimo, mas não transforme “binário pequeno” em desculpa para pular segurança operacional.

Checklist de deploy:

- compilar com versão fixa de Zig;
- rodar `zig fmt --check` e `zig build test` no CI;
- usar `ReleaseSafe` para produção padrão;
- rodar como usuário não-root;
- definir `readiness` e `liveness` checks;
- limitar body, timeout e conexões;
- registrar logs em stdout com contexto;
- expor métricas ou pelo menos contadores básicos;
- documentar variáveis de ambiente obrigatórias.

Se o projeto usa Docker, leia também [Zig e Docker em 2026](/artigos/zig-docker-containers/) e [GitHub Actions para releases multiplataforma](/artigos/zig-github-actions-release-multiplataforma/).

## Armadilhas comuns

1. **Copiar exemplo antigo de `std.http.Server`**: fixe a versão do Zig e ajuste a API de servidor ao release usado.
2. **Parsear JSON sem limite de body**: sempre imponha tamanho máximo antes do parse.
3. **Guardar slices do parser depois do `deinit`**: copie dados persistentes.
4. **Responder erro interno ao cliente**: mapeie erros para códigos públicos estáveis.
5. **Criar framework antes da segunda rota**: comece explícito; abstraia depois.
6. **Ignorar observabilidade**: logs, health e métricas básicas entram no primeiro deploy, não no incidente.

## Conclusão

Uma API REST em Zig pode ser simples, rápida e operacionalmente limpa quando você aceita o estilo da linguagem: controle explícito, contratos pequenos e poucas mágicas. O caminho recomendado é separar domínio e adaptador HTTP, impor limites de payload, tratar JSON com structs claras, mapear erros para respostas estáveis, testar handlers sem socket e empacotar o serviço com health checks e logs úteis.

Zig ainda não oferece o mesmo conforto de frameworks web maduros, mas isso é parte do ponto. Para APIs pequenas, serviços internos, gateways de performance, ferramentas locais e sistemas que precisam de binário previsível, ele é uma escolha séria. Para produtos que dependem de ecossistema web amplo, use Zig nos pontos onde o controle realmente paga — e deixe o restante em uma stack mais produtiva.

## Conteúdo relacionado

- [Servidor HTTP em Zig](/tutoriais/zig-http-server/) — base do servidor e request/response.
- [Servidor HTTP em produção](/artigos/zig-http-server-producao/) — proxy, logs, limites e health checks.
- [Integrando com bancos de dados](/artigos/zig-banco-dados-integracoes/) — SQLite, PostgreSQL, Redis e pooling.
- [Microserviços com Zig](/artigos/zig-microservicos/) — quando usar Zig em arquitetura distribuída.
- [gRPC e Protocol Buffers com Zig](/artigos/zig-grpc-protobuf/) — alternativa binária para comunicação interna.
