Zig Server HTTP: API REST com std.http.Server | Zig Brasil

Criar servidores HTTP é essencial para desenvolvimento backend moderno. Com Zig, você pode construir APIs pequenas, previsíveis e fáceis de distribuir como binários únicos, sem carregar um runtime pesado para cada deploy.

Este guia é a rota prática para quem pesquisa por zig server, zig HTTP server ou std.http.Server e quer sair com um projeto executável. Vamos criar desde um servidor simples até uma API REST com roteamento, JSON, build, Docker e checklist de produção para VPS, containers e ambientes de empresas brasileiras.

Resposta rápida: como criar uma API REST em Zig

ObjetivoCaminho recomendadoLink interno
Criar um servidor HTTP localstd.net.Address.listen, server.accept() e uma resposta HTTP mínima para entender o fluxoUse este guia como ponto de partida
Usar a biblioteca padrão para APIsstd.http.Server, buffer de headers, handler por rota e respostas com status explícitoCombine com Parsing JSON em Zig
Consumir APIs externas no mesmo projetostd.http.Client para GET, POST, JSON, autenticação e limites de respostaZig HTTP Client: GET, POST, JSON e APIs REST em Zig
Enviar payload JSON para outra APIstd.http.Client.open(.POST, ...), Content-Type: application/json e content_length definidoComo Fazer Requisições HTTP POST em Zig
Colocar o servidor no arReverse proxy, limite de body, logs, health check, shutdown e deploy reversívelZig HTTP Server em Produção

Se você quer apenas aprender a API, comece com std.http.Server. Se a meta é um produto público, trate o servidor como parte de uma pilha maior: proxy na frente, binário Zig atrás, observabilidade mínima, versão do Zig fixada e rollback claro. Para projetos que precisam de roteamento mais ergonômico, veja também o panorama de clientes e bibliotecas HTTP no ecossistema Zig antes de adicionar dependências.

Introdução ao std.http.Server

Por Zig para Backend?

Zig oferece vantagens únicas para desenvolvimento web:

  • Performance nativa: Sem garbage collection, com controle preciso de alocações
  • Footprint pequeno: Binários pequenos, ideal para containers
  • Segurança de memória: Eliminação de vulnerabilidades comuns (buffer overflows)
  • Cross-compilation: Compile para qualquer plataforma facilmente
  • Sem dependências: std.http.Server está na biblioteca padrão

Quando usar std.http.Server, httpz ou zap?

A demanda por servidores HTTP em Zig aparece em três cenários diferentes. Escolher a ferramenta certa evita transformar um estudo simples em arquitetura grande demais.

CenárioMelhor ponto de partidaPor quê
Aprender HTTP, requests e respostasstd.http.ServerMostra o fluxo real da conexão sem esconder alocação, buffers e erros
API interna pequena ou ferramenta de automaçãostd.http.Server ou httpzA biblioteca padrão basta para poucos endpoints; httpz reduz boilerplate quando o projeto cresce
Serviço público com alta concorrência no Linuxzap, httpz ou arquitetura própriaFrameworks ajudam com roteamento, middleware e performance, mas adicionam dependências

Para portfólio, entrevista e aprendizado, comece com std.http.Server. Para produto em produção, valide requisitos como TLS, logs, timeouts, limites de body, shutdown gracioso e observabilidade antes de escolher a pilha final.

Arquitetura Básica

┌─────────────────────────────────────┐
│      std.http.Server                │  ← Escuta conexões
├─────────────────────────────────────┤
│   Accept Connection                 │  ← Aceita novas conexões
├─────────────────────────────────────┤
│   Handle Request → Send Response    │  ← Processa e responde
├─────────────────────────────────────┤
│   std.http.Server.Request/Response  │  ← Tipos de requisição/resposta
└─────────────────────────────────────┘

Servidor HTTP Básico

Olá, Mundo Web

Vamos começar com o servidor mais simples possível:

const std = @import("std");

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

    // Criar o servidor na porta 8080
    const address = std.net.Address.parseIp4("0.0.0.0", 8080) catch |err| {
        std.debug.print("Erro ao parsear endereço: {}\n", .{err});
        return err;
    };

    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Servidor rodando em http://localhost:8080\n", .{});

    // Loop principal — aceita conexões indefinidamente
    while (true) {
        const conn = try server.accept();
        
        // Processar conexão
        try handleConnection(allocator, conn);
    }
}

fn handleConnection(allocator: std.mem.Allocator, conn: std.net.Server.Connection) !void {
    defer conn.stream.close();

    // Criar reader/writer HTTP
    var read_buffer: [1024]u8 = undefined;
    const reader = conn.stream.reader();
    const writer = conn.stream.writer();

    // Parse da requisição HTTP
    const request_line = try reader.readUntilDelimiterOrEof(
        &read_buffer,
        '\n'
    ) orelse return;

    // Parse simples: "GET / HTTP/1.1"
    var parts = std.mem.split(u8, request_line, " ");
    const method = parts.next() orelse return;
    const path = parts.next() orelse return;

    std.debug.print("{s} {s}\n", .{ method, path });

    // Responder
    const response = "HTTP/1.1 200 OK\r\n" ++
        "Content-Type: text/plain\r\n" ++
        "Content-Length: 13\r\n" ++
        "\r\n" ++
        "Ola, Mundo!";

    try writer.writeAll(response);
}

Testando o Servidor

# Compilar
zig build-exe server_simples.zig

# Executar
./server_simples

# Em outro terminal, teste:
curl http://localhost:8080
# Output: Ola, Mundo!

Usando std.http.Server

Servidor HTTP Completo

O código anterior usava TCP diretamente. Vamos usar std.http.Server que é mais robusto:

const std = @import("std");

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

    // Criar servidor
    const address = try std.net.Address.parseIp4("127.0.0.1", 8080);
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Servidor em http://127.0.0.1:8080\n", .{});

    // Buffer para headers
    var header_buffer: [8192]u8 = undefined;

    while (true) {
        const conn = try server.accept();
        
        // Criar conexão HTTP
        var http_conn = std.http.Server.init(conn, &header_buffer);
        
        // Receber requisição
        var request = try http_conn.receiveHead();

        // Responder
        try sendResponse(conn.stream.writer(), "Hello from std.http.Server!");
    }
}

fn sendResponse(writer: anytype, body: []const u8) !void {
    const response = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "HTTP/1.1 200 OK\r\n" ++
            "Content-Type: text/plain\r\n" ++
            "Content-Length: {d}\r\n" ++
            "\r\n" ++
            "{s}",
        .{ body.len, body }
    );
    defer std.heap.page_allocator.free(response);

    try writer.writeAll(response);
}

Roteamento Básico

Implementando Rotas Simples

Um servidor real precisa rotear requisições para diferentes handlers:

const std = @import("std");

const Route = struct {
    method: []const u8,
    path: []const u8,
    handler: *const fn ([]const u8, std.net.Stream.Writer) anyerror!void,
};

fn homeHandler(_: []const u8, writer: std.net.Stream.Writer) !void {
    const body = "Bem-vindo a API Zig!";
    try sendResponse(writer, 200, "OK", "text/plain", body);
}

fn usersHandler(_: []const u8, writer: std.net.Stream.Writer) !void {
    const body = "[{\"id\": 1, \"name\": \"Alice\"}, {\"id\": 2, \"name\": \"Bob\"}]";
    try sendResponse(writer, 200, "OK", "application/json", body);
}

fn aboutHandler(_: []const u8, writer: std.net.Stream.Writer) !void {
    const body = "API desenvolvida em Zig";
    try sendResponse(writer, 200, "OK", "text/plain", body);
}

fn notFoundHandler(_: []const u8, writer: std.net.Stream.Writer) !void {
    const body = "404 - Rota nao encontrada";
    try sendResponse(writer, 404, "Not Found", "text/plain", body);
}

const routes = [_]Route{
    .{ .method = "GET", .path = "/", .handler = homeHandler },
    .{ .method = "GET", .path = "/users", .handler = usersHandler },
    .{ .method = "GET", .path = "/about", .handler = aboutHandler },
};

fn sendResponse(
    writer: std.net.Stream.Writer,
    status_code: u16,
    status_text: []const u8,
    content_type: []const u8,
    body: []const u8,
) !void {
    const response = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "HTTP/1.1 {d} {s}\r\n" ++
            "Content-Type: {s}\r\n" ++
            "Content-Length: {d}\r\n" ++
            "\r\n" ++
            "{s}",
        .{ status_code, status_text, content_type, body.len, body }
    );
    defer std.heap.page_allocator.free(response);

    try writer.writeAll(response);
}

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

    const address = try std.net.Address.parseIp4("127.0.0.1", 8080);
    var server = try address.listen(.{
        .reuse_address = true,
        .kernel_backlog = 128,
    });
    defer server.deinit();

    std.debug.print("API em http://127.0.0.1:8080\n", .{});
    std.debug.print("Rotas:\n", .{});
    for (routes) |route| {
        std.debug.print("  {s} {s}\n", .{ route.method, route.path });
    }

    var read_buffer: [1024]u8 = undefined;

    while (true) {
        const conn = try server.accept();
        defer conn.stream.close();

        const reader = conn.stream.reader();
        const writer = conn.stream.writer();

        // Ler requisição
        const request_line = try reader.readUntilDelimiterOrEof(
            &read_buffer,
            '\n'
        ) orelse continue;

        // Parse
        var parts = std.mem.split(u8, request_line, " ");
        const method = parts.next() orelse continue;
        const path = parts.next() orelse continue;

        // Ignorar headers (para este exemplo simples)
        while (true) {
            const line = try reader.readUntilDelimiterOrEof(
                &read_buffer,
                '\n'
            ) orelse break;
            if (line.len == 1 or (line.len == 2 and line[0] == '\r')) break;
        }

        // Roteamento
        var found = false;
        for (routes) |route| {
            if (std.mem.eql(u8, route.method, method) and
                std.mem.eql(u8, route.path, path))
            {
                try route.handler(path, writer);
                found = true;
                break;
            }
        }

        if (!found) {
            try notFoundHandler(path, writer);
        }
    }
}

API REST com JSON

Servidor CRUD Completo

Vamos criar uma API REST para gerenciar tarefas (todo list):

const std = @import("std");

// Modelo
const Task = struct {
    id: u32,
    title: []const u8,
    completed: bool,
};

// Estado global (simplificado — em produção use banco de dados)
var tasks = std.ArrayList(Task).init(std.heap.page_allocator);
var next_id: u32 = 1;

const Context = struct {
    allocator: std.mem.Allocator,
    reader: std.net.Stream.Reader,
    writer: std.net.Stream.Writer,
    method: []const u8,
    path: []const u8,
    body: ?[]const u8,
};

// Handler para GET /tasks
fn getTasks(ctx: Context) !void {
    // Serializar tasks para JSON
    var json = std.ArrayList(u8).init(ctx.allocator);
    defer json.deinit();

    try json.append('[');
    for (tasks.items, 0..) |task, i| {
        if (i > 0) try json.appendSlice(",");
        
        const task_json = try std.fmt.allocPrint(
            ctx.allocator,
            "{{\"id\":{d},\"title\":\"{s}\",\"completed\":{}}}",
            .{ task.id, task.title, task.completed }
        );
        defer ctx.allocator.free(task_json);
        
        try json.appendSlice(task_json);
    }
    try json.append(']');

    try sendJson(ctx.writer, 200, json.items);
}

// Handler para GET /tasks/:id
fn getTask(ctx: Context, id: u32) !void {
    for (tasks.items) |task| {
        if (task.id == id) {
            const json = try std.fmt.allocPrint(
                ctx.allocator,
                "{{\"id\":{d},\"title\":\"{s}\",\"completed\":{}}}",
                .{ task.id, task.title, task.completed }
            );
            defer ctx.allocator.free(json);
            
            try sendJson(ctx.writer, 200, json);
            return;
        }
    }
    
    try sendError(ctx.writer, 404, "Task not found");
}

// Handler para POST /tasks
fn createTask(ctx: Context) !void {
    const body = ctx.body orelse {
        try sendError(ctx.writer, 400, "Body required");
        return;
    };

    // Parse JSON simples (em produção use std.json.parse)
    const title = extractJsonString(body, "title") orelse {
        try sendError(ctx.writer, 400, "Title required");
        return;
    };

    const task = Task{
        .id = next_id,
        .title = try ctx.allocator.dupe(u8, title),
        .completed = false,
    };
    next_id += 1;

    try tasks.append(task);

    const json = try std.fmt.allocPrint(
        ctx.allocator,
        "{{\"id\":{d},\"title\":\"{s}\",\"completed\":false}}",
        .{ task.id, task.title }
    );
    defer ctx.allocator.free(json);

    try sendJson(ctx.writer, 201, json);
}

// Handler para PUT /tasks/:id
fn updateTask(ctx: Context, id: u32) !void {
    const body = ctx.body orelse {
        try sendError(ctx.writer, 400, "Body required");
        return;
    };

    for (tasks.items) |*task| {
        if (task.id == id) {
            if (extractJsonString(body, "title")) |new_title| {
                task.title = new_title;
            }
            if (extractJsonBool(body, "completed")) |new_completed| {
                task.completed = new_completed;
            }

            const json = try std.fmt.allocPrint(
                ctx.allocator,
                "{{\"id\":{d},\"title\":\"{s}\",\"completed\":{}}}",
                .{ task.id, task.title, task.completed }
            );
            defer ctx.allocator.free(json);

            try sendJson(ctx.writer, 200, json);
            return;
        }
    }

    try sendError(ctx.writer, 404, "Task not found");
}

// Handler para DELETE /tasks/:id
fn deleteTask(ctx: Context, id: u32) !void {
    for (tasks.items, 0..) |task, i| {
        if (task.id == id) {
            _ = tasks.orderedRemove(i);
            try sendJson(ctx.writer, 204, "");
            return;
        }
    }

    try sendError(ctx.writer, 404, "Task not found");
}

// Helpers
fn sendJson(writer: std.net.Stream.Writer, status: u16, body: []const u8) !void {
    const response = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "HTTP/1.1 {d} {s}\r\n" ++
            "Content-Type: application/json\r\n" ++
            "Content-Length: {d}\r\n" ++
            "\r\n" ++
            "{s}",
        .{ status, if (status == 200) "OK" else if (status == 201) "Created" else "No Content", body.len, body }
    );
    defer std.heap.page_allocator.free(response);
    
    try writer.writeAll(response);
}

fn sendError(writer: std.net.Stream.Writer, status: u16, message: []const u8) !void {
    const json = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "{{\"error\":\"{s}\"}}",
        .{message}
    );
    defer std.heap.page_allocator.free(json);

    const response = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "HTTP/1.1 {d} Error\r\n" ++
            "Content-Type: application/json\r\n" ++
            "Content-Length: {d}\r\n" ++
            "\r\n" ++
            "{s}",
        .{ status, json.len, json }
    );
    defer std.heap.page_allocator.free(response);

    try writer.writeAll(response);
}

fn extractJsonString(json: []const u8, key: []const u8) ?[]const u8 {
    const pattern = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "\"{s}\":\"",
        .{key}
    );
    defer std.heap.page_allocator.free(pattern);

    if (std.mem.indexOf(u8, json, pattern)) |start| {
        const value_start = start + pattern.len;
        if (std.mem.indexOfScalar(u8, json[value_start..], '"')) |end| {
            return json[value_start .. value_start + end];
        }
    }
    return null;
}

fn extractJsonBool(json: []const u8, key: []const u8) ?bool {
    const pattern = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "\"{s}\":",
        .{key}
    );
    defer std.heap.page_allocator.free(pattern);

    if (std.mem.indexOf(u8, json, pattern)) |start| {
        const value_start = start + pattern.len;
        if (std.mem.startsWith(u8, json[value_start..], "true")) {
            return true;
        } else if (std.mem.startsWith(u8, json[value_start..], "false")) {
            return false;
        }
    }
    return null;
}

// Roteamento
fn handleRequest(ctx: Context) !void {
    if (std.mem.eql(u8, ctx.path, "/tasks")) {
        if (std.mem.eql(u8, ctx.method, "GET")) {
            try getTasks(ctx);
        } else if (std.mem.eql(u8, ctx.method, "POST")) {
            try createTask(ctx);
        } else {
            try sendError(ctx.writer, 405, "Method not allowed");
        }
    } else if (std.mem.startsWith(u8, ctx.path, "/tasks/")) {
        const id_str = ctx.path[7..];
        const id = std.fmt.parseInt(u32, id_str, 10) catch {
            try sendError(ctx.writer, 400, "Invalid ID");
            return;
        };

        if (std.mem.eql(u8, ctx.method, "GET")) {
            try getTask(ctx, id);
        } else if (std.mem.eql(u8, ctx.method, "PUT")) {
            try updateTask(ctx, id);
        } else if (std.mem.eql(u8, ctx.method, "DELETE")) {
            try deleteTask(ctx, id);
        } else {
            try sendError(ctx.writer, 405, "Method not allowed");
        }
    } else {
        try sendError(ctx.writer, 404, "Not found");
    }
}

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

    const address = try std.net.Address.parseIp4("127.0.0.1", 8080);
    var server = try address.listen(.{
        .reuse_address = true,
        .kernel_backlog = 128,
    });
    defer server.deinit();

    std.debug.print("API REST em http://127.0.0.1:8080\n", .{});
    std.debug.print("\nEndpoints:\n", .{});
    std.debug.print("  GET    /tasks       - Listar tarefas\n", .{});
    std.debug.print("  GET    /tasks/:id   - Obter tarefa\n", .{});
    std.debug.print("  POST   /tasks       - Criar tarefa\n", .{});
    std.debug.print("  PUT    /tasks/:id   - Atualizar tarefa\n", .{});
    std.debug.print("  DELETE /tasks/:id   - Remover tarefa\n", .{});

    var read_buffer: [4096]u8 = undefined;

    while (true) {
        const conn = try server.accept();

        // Ler requisição
        const reader = conn.stream.reader();
        const writer = conn.stream.writer();

        const request_line = try reader.readUntilDelimiterOrEof(
            &read_buffer,
            '\n'
        ) orelse {
            conn.stream.close();
            continue;
        };

        var parts = std.mem.split(u8, request_line, " ");
        const method = parts.next() orelse {
            conn.stream.close();
            continue;
        };
        const path = parts.next() orelse {
            conn.stream.close();
            continue;
        };

        // Ler headers e body
        var content_length: usize = 0;
        while (true) {
            const line = try reader.readUntilDelimiterOrEof(
                &read_buffer,
                '\n'
            ) orelse break;
            
            if (line.len == 1 or (line.len == 2 and line[0] == '\r')) break;

            if (std.mem.startsWith(u8, line, "Content-Length:")) {
                const val = std.mem.trim(u8, line[15..], " \r\n");
                content_length = std.fmt.parseInt(usize, val, 10) catch 0;
            }
        }

        // Ler body se houver
        var body: ?[]const u8 = null;
        if (content_length > 0) {
            const body_buf = try allocator.alloc(u8, content_length);
            _ = try reader.readAll(body_buf);
            body = body_buf;
        }

        const ctx = Context{
            .allocator = allocator,
            .reader = reader,
            .writer = writer,
            .method = method,
            .path = path,
            .body = body,
        };

        handleRequest(ctx) catch |err| {
            std.debug.print("Erro: {}\n", .{err});
        };

        if (body) |b| allocator.free(b);
        conn.stream.close();
    }
}

Testando a API

# Iniciar servidor
zig run api.zig

# Listar tarefas (vazio inicialmente)
curl http://localhost:8080/tasks
# Output: []

# Criar tarefa
curl -X POST http://localhost:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Aprender Zig"}'
# Output: {"id":1,"title":"Aprender Zig","completed":false}

# Criar outra
curl -X POST http://localhost:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Criar API"}'

# Listar todas
curl http://localhost:8080/tasks
# Output: [{"id":1,...},{"id":2,...}]

# Atualizar
curl -X PUT http://localhost:8080/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed":true}'

# Deletar
curl -X DELETE http://localhost:8080/tasks/2

Servindo Arquivos Estáticos

Static File Server

const std = @import("std");

const MIME_TYPES = std.StaticStringMap([]const u8).initComptime(.{
    .{ ".html", "text/html" },
    .{ ".css", "text/css" },
    .{ ".js", "application/javascript" },
    .{ ".json", "application/json" },
    .{ ".png", "image/png" },
    .{ ".jpg", "image/jpeg" },
    .{ ".gif", "image/gif" },
    .{ ".svg", "image/svg+xml" },
    .{ ".ico", "image/x-icon" },
});

fn serveFile(writer: std.net.Stream.Writer, path: []const u8) !void {
    // Sanitizar path
    if (std.mem.indexOf(u8, path, "..") != null) {
        try sendError(writer, 403, "Forbidden");
        return;
    }

    // Default para index.html
    const file_path = if (std.mem.eql(u8, path, "/")) 
        "public/index.html" 
    else 
        try std.fmt.allocPrint(
            std.heap.page_allocator,
            "public{s}",
            .{path}
        );

    const file = std.fs.cwd().openFile(file_path, .{}) catch {
        try sendError(writer, 404, "Not found");
        return;
    };
    defer file.close();

    const content = file.readToEndAlloc(std.heap.page_allocator, 10 * 1024 * 1024) catch {
        try sendError(writer, 500, "Internal error");
        return;
    };
    defer std.heap.page_allocator.free(content);

    // Detectar MIME type
    const ext = std.fs.path.extension(file_path);
    const mime = MIME_TYPES.get(ext) orelse "application/octet-stream";

    try sendResponse(writer, 200, "OK", mime, content);
}

fn sendResponse(
    writer: std.net.Stream.Writer,
    status: u16,
    status_text: []const u8,
    content_type: []const u8,
    body: []const u8,
) !void {
    const response = try std.fmt.allocPrint(
        std.heap.page_allocator,
        "HTTP/1.1 {d} {s}\r\n" ++
            "Content-Type: {s}\r\n" ++
            "Content-Length: {d}\r\n" ++
            "\r\n",
        .{ status, status_text, content_type, body.len }
    );
    defer std.heap.page_allocator.free(response);

    try writer.writeAll(response);
    try writer.writeAll(body);
}

fn sendError(writer: std.net.Stream.Writer, status: u16, message: []const u8) !void {
    try sendResponse(writer, status, message, "text/plain", message);
}

pub fn main() !void {
    const address = try std.net.Address.parseIp4("127.0.0.1", 8080);
    var server = try address.listen(.{ .reuse_address = true });
    defer server.deinit();

    std.debug.print("Static server em http://127.0.0.1:8080\n", .{});
    std.debug.print("Coloque arquivos em ./public/\n", .{});

    var read_buffer: [1024]u8 = undefined;

    while (true) {
        const conn = try server.accept();
        defer conn.stream.close();

        const reader = conn.stream.reader();
        const writer = conn.stream.writer();

        const request_line = try reader.readUntilDelimiterOrEof(
            &read_buffer,
            '\n'
        ) orelse continue;

        var parts = std.mem.split(u8, request_line, " ");
        const method = parts.next() orelse continue;
        const path = parts.next() orelse continue;

        // Ignorar headers
        while (true) {
            const line = try reader.readUntilDelimiterOrEof(
                &read_buffer,
                '\n'
            ) orelse break;
            if (line.len <= 2) break;
        }

        if (std.mem.eql(u8, method, "GET")) {
            try serveFile(writer, path);
        } else {
            try sendError(writer, 405, "Method not allowed");
        }
    }
}

Middleware e Logging

Adicionando Logging

fn logRequest(method: []const u8, path: []const u8, status: u16, duration_ms: u64) void {
    const timestamp = std.time.timestamp();
    const datetime = timestampToDateTime(timestamp);
    
    std.debug.print(
        "[{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}] ",
        .{ datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute, datetime.second }
    );
    std.debug.print("{s} {s} - {d} ({d}ms)\n", .{ method, path, status, duration_ms });
}

fn timestampToDateTime(timestamp: i64) struct { year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8 } {
    // Simplificação — use biblioteca de data real em produção
    const seconds_per_day = 86400;
    const days = @divFloor(timestamp, seconds_per_day);
    const seconds_of_day = @mod(timestamp, seconds_per_day);
    
    const hour = @intCast(u8, @divFloor(seconds_of_day, 3600));
    const minute = @intCast(u8, @mod(@divFloor(seconds_of_day, 60), 60));
    const second = @intCast(u8, @mod(seconds_of_day, 60));
    
    // Aproximação simples (não considera anos bissextos)
    const year = 1970 + @intCast(u16, @divFloor(days, 365));
    const day_of_year = @mod(days, 365);
    const month = @intCast(u8, @divFloor(day_of_year, 30) + 1);
    const day = @intCast(u8, @mod(day_of_year, 30) + 1);
    
    return .{
        .year = year,
        .month = month,
        .day = day,
        .hour = hour,
        .minute = minute,
        .second = second,
    };
}

Melhores Práticas

1. Sempre Use defer para Cleanup

const conn = try server.accept();
defer conn.stream.close(); // Sempre fecha a conexão

2. Limite o Tamanho dos Buffers

// ✅ Bom: buffer limitado
var body_buf = try allocator.alloc(u8, content_length);
if (content_length > 10 * 1024 * 1024) {
    return error.BodyTooLarge;
}

// ❌ Ruim: aceitar qualquer tamanho

3. Sanitize Inputs

// ✅ Verificar path traversal
if (std.mem.indexOf(u8, path, "..") != null) {
    return error.Forbidden;
}

4. Use Allocators Apropriados

// ✅ GPA para debug, PageAllocator/FixedBuffer para produção
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// Em produção: considere FixedBufferAllocator ou Arena

5. Trate Erros Gracefully

handleRequest(ctx) catch |err| {
    std.log.err("Erro na requisição: {}", .{err});
    sendError(writer, 500, "Internal Server Error") catch {};
};

Build e Deploy

build.zig para Servidor Web

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "zig-server",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the server");
    run_step.dependOn(&run_cmd.step);
}

Docker para Deploy

# Dockerfile
FROM alpine:latest AS builder
RUN apk add --no-cache curl
WORKDIR /app
RUN curl -L https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar xJ
ENV PATH="/app/zig-linux-x86_64-0.13.0:${PATH}"
COPY . .
RUN zig build -Doptimize=ReleaseSafe

FROM scratch
COPY --from=builder /app/zig-out/bin/zig-server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Cross-Compilation

# Linux x86_64
zig build -Dtarget=x86_64-linux-gnu -Doptimize=ReleaseSafe

# Linux ARM64 (Raspberry Pi, AWS Graviton)
zig build -Dtarget=aarch64-linux-gnu -Doptimize=ReleaseSafe

# Windows
zig build -Dtarget=x86_64-windows-gnu -Doptimize=ReleaseSafe

# macOS
zig build -Dtarget=x86_64-macos-none -Doptimize=ReleaseSafe
zig build -Dtarget=aarch64-macos-none -Doptimize=ReleaseSafe

Checklist para colocar no ar no Brasil

Um servidor HTTP em Zig normalmente nasce como ferramenta interna, webhook, API para dashboard ou serviço de alta performance. Antes de expor em produção, feche esta lista mínima:

  • Versão do Zig fixada: use a mesma versão no ambiente local, CI e Dockerfile. Zig ainda evolui rápido, então builds reproduzíveis importam.
  • Timeouts e limites: defina limite de tamanho para body, cabeçalhos e tempo de leitura para evitar conexões presas.
  • Logs estruturados: registre método, path, status, duração e erro sem gravar tokens ou dados pessoais.
  • Health check: mantenha uma rota como /healthz para Kubernetes, systemd, Fly.io, Render ou VPS com monitoramento.
  • TLS fora do binário: para a maioria dos projetos brasileiros, coloque Caddy, Nginx, Cloudflare Tunnel ou load balancer na frente e deixe Zig falar HTTP interno.
  • Deploy reversível: publique binário versionado, mantenha a versão anterior e valide rollback antes de trocar tráfego.

Essa disciplina é o que separa um tutorial de um serviço útil para uma empresa. Zig facilita o binário pequeno; a confiabilidade vem do processo ao redor.

std.http.Server, httpz ou zap: decisão rápida

Quem chega pesquisando por zig server nem sempre quer a mesma coisa. Algumas pessoas querem aprender a biblioteca padrão, outras querem publicar uma API REST pequena e outras estão comparando Zig com Go, Rust ou Node para tráfego real. Use este mapa antes de escolher a stack:

SituaçãoUse primeiroO que validar antes de crescer
Tutorial, entrevista ou protótipo de APIstd.http.ServerRoteamento, parsing JSON, status HTTP e testes locais com curl
Ferramenta interna com poucos endpointsstd.http.Server com helpers pequenosLimite de body, logs, health check e deploy reversível
API REST pública com rotas e middlewarehttpz ou framework similarMaturidade da versão, exemplos recentes, tratamento de erro e compatibilidade com sua versão do Zig
Serviço de alta concorrência no Linuxzap, arquitetura própria ou proxy agressivo na frenteBenchmarks no hardware real, timeouts, observabilidade e rollback

Não escolha framework apenas porque ele parece mais completo. Em Zig, dependência também significa acompanhar mudanças de compilador, alocadores, API de rede e build system. Para a maioria dos leitores brasileiros começando em backend com Zig, o melhor caminho é: criar um servidor mínimo aqui, consumir uma API externa com Zig HTTP Client, enviar JSON com a receita de HTTP POST em Zig e só então avaliar bibliotecas HTTP do ecossistema.

Se o projeto virar produto, trate a escolha como decisão operacional. Escreva um health check, rode atrás de Caddy ou Nginx, defina versão fixa do Zig, mantenha um binário anterior para rollback e documente como medir erro 5xx, latência e uso de memória. Esses pontos são mais importantes para crescimento real do que trocar std.http.Server por um framework antes de existir tráfego.

Comparação com Outras Linguagens

AspectoZigNode.jsGoRust
Deps HTTP0 (stdlib)0 (built-in)0 (stdlib)1 (hyper/axum)
Binary size~100KB50MB+ (com runtime)~2MB~1MB
MemoryManualGCGCOwnership
Performance⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
ComplexidadeMédiaBaixaBaixaAlta

Próximos Passos

Agora que você sabe criar servidores HTTP em Zig:

  1. HTTP Client em Zig — Consuma outras APIs do seu servidor
  2. Parsing JSON em Zig — APIs REST geralmente usam JSON
  3. Zig e C: Interoperabilidade — Use bibliotecas C avançadas
  4. Async/Await em Zig — Servidores concorrentes

Construa APIs rápidas e seguras com Zig. Compartilhe seus projetos!

Continue aprendendo Zig

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