Construindo um Servidor HTTP Básico em Zig

Neste primeiro artigo da série Desenvolvimento Web com Zig, vamos construir um servidor HTTP funcional do zero. Sem frameworks, sem dependências externas — apenas Zig puro e sua biblioteca padrão.

Entendendo o Modelo de Rede do Zig

Zig oferece acesso direto a sockets POSIX através de std.posix e uma abstração de nível mais alto com std.net e std.http. Vamos começar pelo nível mais baixo e subir progressivamente.

Passo 1: Um Servidor TCP Simples

Antes de HTTP, precisamos de TCP. Vamos criar um servidor que aceita conexões:

const std = @import("std");
const net = std.net;

pub fn main() !void {
    // Criar um socket do servidor
    const endereco = net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
    var servidor = try endereco.listen(.{
        .reuse_address = true,
    });
    defer servidor.deinit();

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

    // Loop de aceitação de conexões
    while (true) {
        var conexao = try servidor.accept();
        defer conexao.stream.close();

        std.debug.print("Nova conexão de {}\n", .{conexao.address});

        // Ler dados do cliente
        var buffer: [1024]u8 = undefined;
        const bytes_lidos = try conexao.stream.read(&buffer);

        if (bytes_lidos > 0) {
            std.debug.print("Recebido: {s}\n", .{buffer[0..bytes_lidos]});

            // Enviar resposta
            const resposta = "Olá do servidor Zig!\n";
            _ = try conexao.stream.write(resposta);
        }
    }
}

Teste com: echo "Olá" | nc localhost 8080

Passo 2: Servidor HTTP com std.http.Server

Agora vamos usar a abstração HTTP de Zig para criar um servidor HTTP real:

const std = @import("std");

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

    // Configurar endereço
    const endereco = std.net.Address.parseIp("127.0.0.1", 8080) catch unreachable;

    // Criar servidor HTTP
    var servidor = try endereco.listen(.{
        .reuse_address = true,
    });
    defer servidor.deinit();

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

    // Loop principal
    while (true) {
        // Aceitar conexão
        var conexao = try servidor.accept();
        defer conexao.stream.close();

        // Criar servidor HTTP para esta conexão
        var buf: [8192]u8 = undefined;
        var http_server = std.http.Server.init(conexao, &buf);

        // Ler a requisição
        var request = http_server.receiveHead() catch |err| {
            std.debug.print("Erro ao ler requisição: {}\n", .{err});
            continue;
        };

        // Processar a requisição
        tratarRequisicao(allocator, &request) catch |err| {
            std.debug.print("Erro ao processar requisição: {}\n", .{err});
        };
    }
}

fn tratarRequisicao(allocator: std.mem.Allocator, request: *std.http.Server.Request) !void {
    _ = allocator;

    const metodo = request.head.method;
    const caminho = request.head.target;

    std.debug.print("{s} {s}\n", .{ @tagName(metodo), caminho });

    // Corpo da resposta
    const corpo =
        \\<!DOCTYPE html>
        \\<html lang="pt-BR">
        \\<head>
        \\    <meta charset="UTF-8">
        \\    <title>Servidor Zig</title>
        \\</head>
        \\<body>
        \\    <h1>Olá do Zig!</h1>
        \\    <p>Este servidor foi construído com Zig puro.</p>
        \\</body>
        \\</html>
    ;

    // Enviar resposta
    try request.respond(corpo, .{
        .extra_headers = &.{
            .{ .name = "content-type", .value = "text/html; charset=utf-8" },
        },
    });
}

Passo 3: Servindo Diferentes Tipos de Conteúdo

Vamos expandir o servidor para servir diferentes formatos:

const std = @import("std");

const ContentType = enum {
    html,
    json,
    text,
    not_found,

    fn header(self: ContentType) []const u8 {
        return switch (self) {
            .html => "text/html; charset=utf-8",
            .json => "application/json",
            .text => "text/plain; charset=utf-8",
            .not_found => "text/plain; charset=utf-8",
        };
    }
};

fn rotear(caminho: []const u8) struct { corpo: []const u8, tipo: ContentType, status: std.http.Status } {
    if (std.mem.eql(u8, caminho, "/")) {
        return .{
            .corpo =
            \\<!DOCTYPE html>
            \\<html><body>
            \\<h1>Bem-vindo ao Zig Web Server</h1>
            \\<ul>
            \\  <li><a href="/api/status">Status da API</a></li>
            \\  <li><a href="/sobre">Sobre</a></li>
            \\</ul>
            \\</body></html>
            ,
            .tipo = .html,
            .status = .ok,
        };
    }

    if (std.mem.eql(u8, caminho, "/api/status")) {
        return .{
            .corpo =
            \\{"status": "online", "versao": "1.0.0", "linguagem": "Zig"}
            ,
            .tipo = .json,
            .status = .ok,
        };
    }

    if (std.mem.eql(u8, caminho, "/sobre")) {
        return .{
            .corpo =
            \\<!DOCTYPE html>
            \\<html><body>
            \\<h1>Sobre</h1>
            \\<p>Servidor HTTP construído com Zig - Sem dependências externas!</p>
            \\<p><a href="/">Voltar</a></p>
            \\</body></html>
            ,
            .tipo = .html,
            .status = .ok,
        };
    }

    return .{
        .corpo = "404 - Página não encontrada",
        .tipo = .not_found,
        .status = .not_found,
    };
}

fn tratarRequisicao(request: *std.http.Server.Request) !void {
    const metodo = request.head.method;
    const caminho = request.head.target;

    std.debug.print("[{s}] {s}\n", .{ @tagName(metodo), caminho });

    const rota = rotear(caminho);

    try request.respond(rota.corpo, .{
        .status = rota.status,
        .extra_headers = &.{
            .{ .name = "content-type", .value = rota.tipo.header() },
            .{ .name = "server", .value = "Zig/1.0" },
        },
    });
}

pub fn main() !void {
    const endereco = std.net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
    var servidor = try endereco.listen(.{ .reuse_address = true });
    defer servidor.deinit();

    std.debug.print("Servidor rodando em http://127.0.0.1:8080\n", .{});
    std.debug.print("Rotas disponíveis:\n", .{});
    std.debug.print("  GET /           - Página inicial\n", .{});
    std.debug.print("  GET /api/status - Status da API (JSON)\n", .{});
    std.debug.print("  GET /sobre      - Página sobre\n", .{});

    while (true) {
        var conexao = try servidor.accept();
        defer conexao.stream.close();

        var buf: [8192]u8 = undefined;
        var http = std.http.Server.init(conexao, &buf);

        var request = http.receiveHead() catch continue;
        tratarRequisicao(&request) catch |err| {
            std.debug.print("Erro: {}\n", .{err});
        };
    }
}

Passo 4: Lendo o Corpo da Requisição

Para POST e PUT, precisamos ler o corpo da requisição:

const std = @import("std");

fn lerCorpo(allocator: std.mem.Allocator, request: *std.http.Server.Request) ![]u8 {
    var reader = try request.reader();
    const corpo = try reader.readAllAlloc(allocator, 1024 * 1024); // Máx 1MB
    return corpo;
}

fn tratarPost(allocator: std.mem.Allocator, request: *std.http.Server.Request) !void {
    const corpo = try lerCorpo(allocator, request);
    defer allocator.free(corpo);

    std.debug.print("Corpo recebido ({d} bytes): {s}\n", .{ corpo.len, corpo });

    const resposta = try std.fmt.allocPrint(
        allocator,
        "{{\"recebido\": true, \"tamanho\": {d}}}",
        .{corpo.len},
    );
    defer allocator.free(resposta);

    try request.respond(resposta, .{
        .extra_headers = &.{
            .{ .name = "content-type", .value = "application/json" },
        },
    });
}

Teste com: curl -X POST -d '{"nome": "Zig"}' http://localhost:8080/api/dados

Entendendo o Modelo de Concorrência

Nosso servidor atual é single-threaded — processa uma requisição por vez. Nos próximos artigos, vamos explorar como torná-lo concorrente. Por enquanto, este modelo é suficiente para entender os fundamentos.

Exercício Prático

Tente expandir o servidor com:

  1. Uma rota /api/hora que retorna a hora atual em JSON
  2. Uma rota /api/echo que retorna o corpo da requisição POST
  3. Headers customizados como X-Powered-By: Zig

Próximos Passos

No próximo artigo, vamos implementar um sistema de roteamento robusto com handlers modulares, parâmetros de URL e separação de responsabilidades.

Referências

Continue aprendendo Zig

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