Zig HTTP Client: GET, POST, JSON e APIs REST em Zig

Fazer requisições HTTP é uma necessidade fundamental em quase todo software moderno. Seja para consumir APIs REST, fazer webhooks, baixar artefatos ou integrar com serviços externos, você precisa saber como usar o std.http.Client de Zig com controle de memória, limites de resposta e tratamento explícito de erros.

Este tutorial é o guia prático para quem pesquisa por zig HTTP client, std.http.Client, GET e POST em Zig ou consumir API REST com Zig. Vamos sair de uma requisição GET simples, passar por JSON e autenticação, e chegar em padrões reutilizáveis para CLIs, workers, integrações internas e serviços backend.

Resposta rápida: como fazer HTTP em Zig

ObjetivoCaminho recomendadoLink interno
Fazer um GET simplesstd.http.Client, std.Uri.parse, client.request(.GET, ...) e limite explícito no readAllAllocVeja também a receita de HTTP GET
Enviar JSON com POSTSerializar com std.json.stringify, definir Content-Type: application/json e escrever no body antes de finish()Combine com Parsing JSON em Zig
Criar uma API REST em ZigUse este client para consumir APIs; use std.http.Server para recebê-lasZig Server HTTP: API REST com std.http.Server
Rodar em produçãoDefina User-Agent, valide status, limite corpo, trate retries e observe logsZig HTTP Server em Produção

Se você está montando uma aplicação completa, pense no par: std.http.Client para chamar APIs externas e std.http.Server para expor endpoints internos. Essa ligação também aparece em guias de networking com sockets TCP/UDP e em projetos de filas e workers em background, onde chamadas HTTP precisam ser previsíveis e fáceis de depurar.

Quando usar este guia, uma receita ou a referência

A busca por zig http client costuma misturar três intenções: copiar um exemplo curto, entender a API da stdlib e montar uma integração REST mais robusta. Use esta página como trilha principal e pule para o material certo conforme o seu estágio:

Intenção de buscaMelhor próximo passoPor que ajuda
“Quero só fazer um GET agora”Como fazer requisições HTTP GET em ZigReceita curta, direta e fácil de adaptar para scripts e CLIs
“Preciso enviar JSON com POST”Como fazer requisições HTTP POST em ZigMostra Content-Type, content_length, escrita do body e validação de status
“Quero entender a API oficial”std.http.Client em ZigReferência focada em fetch, opções, headers, TLS e armazenamento da resposta
“Estou escolhendo biblioteca HTTP”Clientes HTTP em ZigCompara stdlib e alternativas quando o projeto exige ergonomia ou recursos extras
“Meu serviço também recebe requests”Zig Server HTTP: API REST com std.http.ServerFecha o ciclo entre consumir APIs externas e expor endpoints internos

Para produção, não trate o cliente HTTP como detalhe de infraestrutura invisível. Defina um User-Agent, limite o tamanho da resposta, valide status antes de fazer parse, registre contexto suficiente para depuração e diferencie erro de rede, erro HTTP e JSON inválido. Se o client roda dentro de um backend, combine este checklist com o guia de HTTP server em produção para alinhar timeouts, retries, logs e health checks.

Introdução ao std.http.Client

Por Que Não Usar uma Biblioteca Externa?

Zig vem com um cliente HTTP completo na biblioteca padrão (std.http.Client). Isso significa:

  • Zero dependências externas para a maioria dos casos de uso
  • Performance nativa — código Zig puro, sem bindings
  • Cross-compilation funciona perfeitamente
  • Integração natural com o ecossistema Zig (allocators, errors, etc.)

Arquitetura do Cliente HTTP

O std.http.Client é construído em camadas:

┌─────────────────────────────────────┐
│          std.http.Client            │  ← Cliente principal, gerencia conexões
├─────────────────────────────────────┤
│      std.http.Client.Connection     │  ← Conexão persistente com servidor
├─────────────────────────────────────┤
│       std.http.Request/Response     │  ← Requisição e resposta HTTP
├─────────────────────────────────────┤
│         std.crypto.tls              │  ← TLS/HTTPS (quando necessário)
└─────────────────────────────────────┘

Requisição GET Simples

O Básico: Fetch de uma URL

Vamos começar com o exemplo mais simples — fazer um GET e imprimir a resposta:

const std = @import("std");

pub fn main() !void {
    // Allocator necessário para o cliente HTTP
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Criar o cliente HTTP
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    // Fazer a requisição GET
    const uri = try std.Uri.parse("https://api.github.com/users/ziglang");
    
    var req = try client.request(.GET, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    // Adicionar headers
    try req.headers.append("User-Agent", "ZigHTTPClient/1.0");

    // Enviar e finalizar a requisição
    try req.start();
    try req.finish();
    try req.wait();

    // Ler a resposta
    const body = try req.reader().readAllAlloc(allocator, 10 * 1024 * 1024); // Max 10MB
    defer allocator.free(body);

    // Imprimir
    std.debug.print("Status: {d}\n", .{req.response.status});
    std.debug.print("Body: {s}\n", .{body});
}

Explicação Passo a Passo

  1. Criar o cliente: std.http.Client precisa de um allocator para gerenciar buffers
  2. Parsear a URI: Convertemos a string URL para o tipo std.Uri
  3. Criar a requisição: .request() configura o método, URI e opções
  4. Adicionar headers: Headers HTTP como User-Agent são importantes
  5. start/finish/wait: Sequência necessária para enviar e receber
  6. Ler resposta: O body pode ser lido via req.reader()

Trabalhando com JSON

Parsing JSON da Resposta

Consumir APIs geralmente significa trabalhar com JSON. Vamos integrar com parsing:

const std = @import("std");

// Struct que representa a resposta da API
const User = struct {
    login: []const u8,
    id: u64,
    avatar_url: []const u8,
    html_url: []const u8,
    public_repos: u32,
    followers: u32,
    following: u32,
    created_at: []const u8,
};

pub fn fetchUser(allocator: std.mem.Allocator, username: []const u8) !User {
    // Construir URL
    const base_url = "https://api.github.com/users/";
    const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ base_url, username });
    defer allocator.free(url);

    // Criar cliente
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    // Configurar requisição
    const uri = try std.Uri.parse(url);
    var req = try client.request(.GET, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    // Headers necessários para GitHub API
    try req.headers.append("User-Agent", "ZigTutorial");
    try req.headers.append("Accept", "application/vnd.github.v3+json");

    // Executar
    try req.start();
    try req.finish();
    try req.wait();

    // Verificar status
    if (req.response.status != .ok) {
        return error.RequestFailed;
    }

    // Ler body
    const body = try req.reader().readAllAlloc(allocator, 1024 * 1024);
    defer allocator.free(body);

    // Parse JSON
    const parsed = try std.json.parseFromSlice(User, allocator, body, .{
        .ignore_unknown_fields = true,
    });
    defer parsed.deinit();

    return parsed.value;
}

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

    const user = try fetchUser(allocator, "ziglang");

    std.debug.print("Usuário: {s}\n", .{user.login});
    std.debug.print("ID: {d}\n", .{user.id});
    std.debug.print("Repositórios públicos: {d}\n", .{user.public_repos});
    std.debug.print("Seguidores: {d}\n", .{user.followers});
    std.debug.print("URL: {s}\n", .{user.html_url});
}

Requisições POST com JSON

Enviando Dados para uma API

POST é usado para criar recursos. Vamos ver como enviar JSON:

const std = @import("std");

const Post = struct {
    title: []const u8,
    body: []const u8,
    userId: u32,
};

const CreatedPost = struct {
    title: []const u8,
    body: []const u8,
    userId: u32,
    id: u32,
};

pub fn createPost(allocator: std.mem.Allocator, post: Post) !CreatedPost {
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("https://jsonplaceholder.typicode.com/posts");

    // Serializar JSON
    var json_buffer = std.ArrayList(u8).init(allocator);
    defer json_buffer.deinit();
    try std.json.stringify(post, .{}, json_buffer.writer());

    var req = try client.request(.POST, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    // Headers importantes para POST JSON
    try req.headers.append("User-Agent", "ZigHTTPClient");
    try req.headers.append("Content-Type", "application/json");
    try req.headers.append("Accept", "application/json");

    // Enviar body
    try req.start();
    try req.writer().writeAll(json_buffer.items);
    try req.finish();
    try req.wait();

    // Ler resposta
    const body = try req.reader().readAllAlloc(allocator, 1024 * 1024);
    defer allocator.free(body);

    // Parse resposta
    const parsed = try std.json.parseFromSlice(CreatedPost, allocator, body, .{});
    defer parsed.deinit();

    return parsed.value;
}

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

    const new_post = Post{
        .title = "Aprendendo Zig HTTP",
        .body = "Este é um post criado via API usando Zig!",
        .userId = 1,
    };

    const created = try createPost(allocator, new_post);

    std.debug.print("Post criado!\n", .{});
    std.debug.print("ID: {d}\n", .{created.id});
    std.debug.print("Título: {s}\n", .{created.title});
}

Autenticação

Bearer Token (JWT/OAuth)

APIs modernas geralmente usam tokens Bearer:

const AuthenticatedClient = struct {
    client: std.http.Client,
    base_url: []const u8,
    token: []const u8,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator, base_url: []const u8, token: []const u8) AuthenticatedClient {
        return .{
            .client = std.http.Client{ .allocator = allocator },
            .base_url = base_url,
            .token = token,
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *AuthenticatedClient) void {
        self.client.deinit();
    }

    pub fn get(self: *AuthenticatedClient, endpoint: []const u8) ![]u8 {
        const url = try std.fmt.allocPrint(
            self.allocator,
            "{s}{s}",
            .{ self.base_url, endpoint }
        );
        defer self.allocator.free(url);

        const uri = try std.Uri.parse(url);
        var req = try self.client.request(.GET, uri, .{
            .allocator = self.allocator,
        }, .{});
        defer req.deinit();

        // Adicionar token de autenticação
        const auth_header = try std.fmt.allocPrint(
            self.allocator,
            "Bearer {s}",
            .{self.token}
        );
        defer self.allocator.free(auth_header);

        try req.headers.append("Authorization", auth_header);
        try req.headers.append("User-Agent", "ZigApp/1.0");
        try req.headers.append("Accept", "application/json");

        try req.start();
        try req.finish();
        try req.wait();

        if (req.response.status != .ok) {
            return error.RequestFailed;
        }

        return try req.reader().readAllAlloc(self.allocator, 10 * 1024 * 1024);
    }
};

API Key

Para APIs que usam chaves de API:

pub fn requestWithApiKey(
    allocator: std.mem.Allocator,
    url: []const u8,
    api_key: []const u8,
) ![]u8 {
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse(url);
    var req = try client.request(.GET, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    // Método 1: Header X-API-Key
    try req.headers.append("X-API-Key", api_key);

    // Método 2: Query parameter (para algumas APIs)
    // const url_with_key = try std.fmt.allocPrint(allocator, "{s}?api_key={s}", .{url, api_key});

    try req.start();
    try req.finish();
    try req.wait();

    return try req.reader().readAllAlloc(allocator, 10 * 1024 * 1024);
}

Tratamento de Erros em HTTP

Erros Comuns e Como Tratá-los

const HttpError = error{
    ConnectionFailed,
    Timeout,
    InvalidResponse,
    ServerError,
    NotFound,
    Unauthorized,
    TooManyRequests,
};

pub fn robustRequest(
    allocator: std.mem.Allocator,
    method: std.http.Method,
    url: []const u8,
) ![]u8 {
    var client = std.http.Client{
        .allocator = allocator,
    };
    defer client.deinit();

    const uri = std.Uri.parse(url) catch {
        return error.InvalidResponse;
    };

    var req = client.request(method, uri, .{
        .allocator = allocator,
    }, .{}) catch {
        return error.ConnectionFailed;
    };
    defer req.deinit();

    try req.headers.append("User-Agent", "ZigRobustClient/1.0");

    req.start() catch {
        return error.ConnectionFailed;
    };
    req.finish() catch {
        return error.ConnectionFailed;
    };
    req.wait() catch {
        return error.Timeout;
    };

    // Tratamento específico por status code
    switch (req.response.status) {
        .ok => {},
        .not_found => return error.NotFound,
        .unauthorized => return error.Unauthorized,
        .too_many_requests => return error.TooManyRequests,
        .internal_server_error => return error.ServerError,
        .bad_gateway => return error.ServerError,
        .service_unavailable => return error.ServerError,
        else => {
            if (@intFromEnum(req.response.status) >= 500) {
                return error.ServerError;
            }
            return error.InvalidResponse;
        },
    }

    return req.reader().readAllAlloc(allocator, 10 * 1024 * 1024) catch {
        return error.InvalidResponse;
    };
}

Retry com Backoff

const RetryConfig = struct {
    max_retries: u32 = 3,
    base_delay_ms: u32 = 1000,
    max_delay_ms: u32 = 30000,
};

pub fn requestWithRetry(
    allocator: std.mem.Allocator,
    url: []const u8,
    config: RetryConfig,
) ![]u8 {
    var delay_ms = config.base_delay_ms;

    var last_error: anyerror = error.Unknown;

    var i: u32 = 0;
    while (i < config.max_retries) : (i += 1) {
        return robustRequest(allocator, .GET, url) catch |err| {
            last_error = err;

            // Não retry em erros cliente (4xx)
            if (err == error.NotFound or err == error.Unauthorized) {
                return err;
            }

            // Esperar antes de retry (exceto na última tentativa)
            if (i < config.max_retries - 1) {
                std.time.sleep(delay_ms * std.time.ns_per_ms);
                delay_ms = @min(delay_ms * 2, config.max_delay_ms); // Exponential backoff
            }

            continue;
        };
    }

    return last_error;
}

Streaming e Grandes Respostas

Lendo Respostas Sem Carregar Tudo na Memória

Para arquivos grandes ou streams:

pub fn downloadFile(
    allocator: std.mem.Allocator,
    url: []const u8,
    output_path: []const u8,
) !void {
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse(url);
    var req = try client.request(.GET, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    try req.headers.append("User-Agent", "ZigDownloader/1.0");

    try req.start();
    try req.finish();
    try req.wait();

    if (req.response.status != .ok) {
        return error.DownloadFailed;
    }

    // Criar arquivo de saída
    const file = try std.fs.cwd().createFile(output_path, .{});
    defer file.close();

    // Ler em chunks para não carregar tudo na memória
    var buf: [8192]u8 = undefined;
    var reader = req.reader();
    var writer = file.writer();

    while (true) {
        const bytes_read = try reader.read(&buf);
        if (bytes_read == 0) break;
        try writer.writeAll(buf[0..bytes_read]);
    }

    std.debug.print("Download completo: {s}\n", .{output_path});
}

Cliente HTTP Reutilizável

Padrão: Wrapper Idiomático

Para aplicações reais, crie um wrapper reutilizável:

const ApiClient = struct {
    client: std.http.Client,
    base_url: []const u8,
    default_headers: std.http.Headers,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator, base_url: []const u8) !ApiClient {
        var headers = std.http.Headers.init(allocator);

        try headers.append("User-Agent", "MyZigApp/1.0");
        try headers.append("Accept", "application/json");

        return ApiClient{
            .client = std.http.Client{ .allocator = allocator },
            .base_url = try allocator.dupe(u8, base_url),
            .default_headers = headers,
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *ApiClient) void {
        self.client.deinit();
        self.default_headers.deinit();
        self.allocator.free(self.base_url);
    }

    pub fn get(self: *ApiClient, endpoint: []const u8) !Response {
        return self.request(.GET, endpoint, null);
    }

    pub fn post(
        self: *ApiClient,
        endpoint: []const u8,
        body: ?[]const u8,
    ) !Response {
        return self.request(.POST, endpoint, body);
    }

    fn request(
        self: *ApiClient,
        method: std.http.Method,
        endpoint: []const u8,
        body: ?[]const u8,
    ) !Response {
        const url = try std.fmt.allocPrint(
            self.allocator,
            "{s}{s}",
            .{ self.base_url, endpoint }
        );
        defer self.allocator.free(url);

        const uri = try std.Uri.parse(url);
        var req = try self.client.request(method, uri, .{
            .allocator = self.allocator,
            .headers = self.default_headers,
        }, .{});
        defer req.deinit();

        if (body) |b| {
            try req.headers.append("Content-Type", "application/json");
        }

        try req.start();
        if (body) |b| {
            try req.writer().writeAll(b);
        }
        try req.finish();
        try req.wait();

        const response_body = try req.reader().readAllAlloc(
            self.allocator,
            10 * 1024 * 1024
        );

        return Response{
            .status = req.response.status,
            .body = response_body,
            .allocator = self.allocator,
        };
    }
};

const Response = struct {
    status: std.http.Status,
    body: []u8,
    allocator: std.mem.Allocator,

    pub fn deinit(self: Response) void {
        self.allocator.free(self.body);
    }

    pub fn json(self: Response, comptime T: type) !T {
        const parsed = try std.json.parseFromSlice(T, self.allocator, self.body, .{
            .ignore_unknown_fields = true,
        });
        defer parsed.deinit();
        return parsed.value;
    }
};

Exemplo Completo: CLI de Previsão do Tempo

Vamos criar uma aplicação CLI completa que consome uma API de previsão do tempo:

const std = @import("std");

const WeatherResponse = struct {
    current: CurrentWeather,
    location: Location,
};

const CurrentWeather = struct {
    temp_c: f32,
    condition: Condition,
    humidity: u32,
    wind_kph: f32,
};

const Condition = struct {
    text: []const u8,
};

const Location = struct {
    name: []const u8,
    country: []const u8,
};

pub fn fetchWeather(
    allocator: std.mem.Allocator,
    city: []const u8,
    api_key: []const u8,
) !WeatherResponse {
    // URL encode da cidade
    var encoded_city = std.ArrayList(u8).init(allocator);
    defer encoded_city.deinit();

    for (city) |c| {
        if (c == ' ') {
            try encoded_city.append('%');
            try encoded_city.append('2');
            try encoded_city.append('0');
        } else {
            try encoded_city.append(c);
        }
    }

    const url = try std.fmt.allocPrint(
        allocator,
        "https://api.weatherapi.com/v1/current.json?key={s}&q={s}&aqi=no",
        .{ api_key, encoded_city.items }
    );
    defer allocator.free(url);

    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse(url);
    var req = try client.request(.GET, uri, .{
        .allocator = allocator,
    }, .{});
    defer req.deinit();

    try req.start();
    try req.finish();
    try req.wait();

    if (req.response.status != .ok) {
        return error.WeatherApiError;
    }

    const body = try req.reader().readAllAlloc(allocator, 1024 * 1024);
    defer allocator.free(body);

    const parsed = try std.json.parseFromSlice(WeatherResponse, allocator, body, .{
        .ignore_unknown_fields = true,
    });
    defer parsed.deinit();

    return parsed.value;
}

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

    // Argumentos da linha de comando
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len < 3) {
        std.debug.print("Uso: {s} <cidade> <api_key>\n", .{args[0]});
        std.process.exit(1);
    }

    const city = args[1];
    const api_key = args[2];

    std.debug.print("Buscando previsão para: {s}...\n", .{city});

    const weather = fetchWeather(allocator, city, api_key) catch |err| {
        std.debug.print("Erro ao buscar previsão: {}\n", .{err});
        std.process.exit(1);
    };

    std.debug.print("\n📍 {s}, {s}\n", .{ weather.location.name, weather.location.country });
    std.debug.print("🌡️  Temperatura: {d:.1}°C\n", .{weather.current.temp_c});
    std.debug.print("☁️  Condição: {s}\n", .{weather.current.condition.text});
    std.debug.print("💧 Umidade: {d}%\n", .{weather.current.humidity});
    std.debug.print("💨 Vento: {d:.1} km/h\n", .{weather.current.wind_kph});
}

Melhores Práticas

1. Sempre Use defer para Cleanup

// ✅ Bom: cleanup automático
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();

var req = try client.request(.GET, uri, .{ .allocator = allocator }, .{});
defer req.deinit();

// ❌ Ruim: esquecer de deinit pode vazar memória

2. Limite o Tamanho das Respostas

// ✅ Bom: limite para evitar OOM
const body = try req.reader().readAllAlloc(allocator, 10 * 1024 * 1024);

// ❌ Ruim: sem limite pode causar problemas
// const body = try req.reader().readAllAlloc(allocator, std.math.maxInt(usize));

3. Reuse Conexões Quando Possível

// ✅ Bom: cliente reutilizado
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();

// Múltiplas requisições com o mesmo cliente
for (urls) |url| {
    // req = client.request(...) reusa conexões
}

4. Trate Timeouts Adequadamente

// Configurar timeout na criação do cliente
var client = std.http.Client{
    .allocator = allocator,
};
// HTTP client Zig atual não expõe timeout diretamente
// Considere usar threads async para timeout manual

5. Valide Status Codes

// ✅ Bom: tratar diferentes status
switch (req.response.status) {
    .ok => {},
    .not_found => return error.NotFound,
    .unauthorized => return error.Unauthorized,
    else => return error.RequestFailed,
}

Limitações Atuais

O Que Ainda Está Evoluindo

  1. HTTP/2: Suporte ainda parcial em algumas versões
  2. WebSockets: Não há suporte nativo, use bibliotecas externas
  3. Client-side certificates: Suporte limitado
  4. Proxy configuration: Configuração manual necessária

Para casos avançados, considere bibliotecas como zig-network ou integrações com libcurl via C interop.

Próximos Passos

Agora que você domina requisições HTTP em Zig, explore:

  1. Criando um Servidor HTTP em Zig — Aprenda a receber requisições, não apenas fazer
  2. Parsing JSON em Zig — Aprofunde o trabalho com APIs REST
  3. Tratamento de Erros em Zig — Padrões avançados de error handling
  4. Async/Await em Zig — Para requests HTTP concorrentes

Consumindo APIs com Zig é simples e eficiente. Compartilhe o que você construiu com a comunidade!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.