Roteamento e Request Handlers em Zig: Sistema Modular

No artigo anterior, construímos um servidor HTTP básico. Agora vamos criar um sistema de roteamento modular que nos permita organizar o código de forma escalável.

O Problema com Roteamento Ad-Hoc

No artigo anterior, usamos uma cadeia de if/else para rotear. Isso rapidamente se torna incontrolável em aplicações reais. Precisamos de uma abstração melhor.

Definindo a Interface do Router

Vamos criar um sistema onde cada handler é uma função independente:

const std = @import("std");

// Contexto da requisição — passado para cada handler
const Context = struct {
    request: *std.http.Server.Request,
    allocator: std.mem.Allocator,
    params: std.StringHashMap([]const u8),

    fn respond(self: *Context, corpo: []const u8, options: anytype) !void {
        try self.request.respond(corpo, options);
    }

    fn respondJson(self: *Context, corpo: []const u8) !void {
        try self.request.respond(corpo, .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = "application/json" },
            },
        });
    }

    fn respondHtml(self: *Context, corpo: []const u8) !void {
        try self.request.respond(corpo, .{
            .extra_headers = &.{
                .{ .name = "content-type", .value = "text/html; charset=utf-8" },
            },
        });
    }
};

// Tipo de um handler
const Handler = *const fn (*Context) anyerror!void;

// Uma rota mapeia método + path para um handler
const Route = struct {
    metodo: std.http.Method,
    caminho: []const u8,
    handler: Handler,
};

Implementando o Router

const Router = struct {
    rotas: std.ArrayList(Route),
    not_found_handler: Handler,

    fn init(allocator: std.mem.Allocator) Router {
        return .{
            .rotas = std.ArrayList(Route).init(allocator),
            .not_found_handler = defaultNotFound,
        };
    }

    fn deinit(self: *Router) void {
        self.rotas.deinit();
    }

    fn get(self: *Router, caminho: []const u8, handler: Handler) !void {
        try self.rotas.append(.{
            .metodo = .GET,
            .caminho = caminho,
            .handler = handler,
        });
    }

    fn post(self: *Router, caminho: []const u8, handler: Handler) !void {
        try self.rotas.append(.{
            .metodo = .POST,
            .caminho = caminho,
            .handler = handler,
        });
    }

    fn put(self: *Router, caminho: []const u8, handler: Handler) !void {
        try self.rotas.append(.{
            .metodo = .PUT,
            .caminho = caminho,
            .handler = handler,
        });
    }

    fn delete(self: *Router, caminho: []const u8, handler: Handler) !void {
        try self.rotas.append(.{
            .metodo = .DELETE,
            .caminho = caminho,
            .handler = handler,
        });
    }

    fn encontrarHandler(self: *Router, metodo: std.http.Method, caminho: []const u8) Handler {
        for (self.rotas.items) |rota| {
            if (rota.metodo == metodo and matchPath(rota.caminho, caminho)) {
                return rota.handler;
            }
        }
        return self.not_found_handler;
    }

    fn matchPath(pattern: []const u8, caminho: []const u8) bool {
        // Match exato por enquanto
        return std.mem.eql(u8, pattern, caminho);
    }

    fn defaultNotFound(ctx: *Context) anyerror!void {
        try ctx.respondJson("{\"erro\": \"Rota não encontrada\", \"status\": 404}");
    }
};

Definindo Handlers Separados

Cada handler é uma função independente, fácil de testar e manter:

// handlers.zig - módulo de handlers

fn homeHandler(ctx: *Context) anyerror!void {
    const html =
        \\<!DOCTYPE html>
        \\<html lang="pt-BR">
        \\<head><meta charset="UTF-8"><title>Zig Web App</title></head>
        \\<body>
        \\  <h1>Bem-vindo à aplicação Zig</h1>
        \\  <nav>
        \\    <a href="/api/status">Status</a> |
        \\    <a href="/api/usuarios">Usuários</a>
        \\  </nav>
        \\</body>
        \\</html>
    ;
    try ctx.respondHtml(html);
}

fn statusHandler(ctx: *Context) anyerror!void {
    const json = try std.fmt.allocPrint(
        ctx.allocator,
        \\{{"status": "online", "uptime_s": {d}, "versao": "1.0.0"}}
    ,
        .{std.time.timestamp()},
    );
    defer ctx.allocator.free(json);
    try ctx.respondJson(json);
}

fn listarUsuariosHandler(ctx: *Context) anyerror!void {
    const json =
        \\{
        \\  "usuarios": [
        \\    {"id": 1, "nome": "Ana", "email": "ana@exemplo.com"},
        \\    {"id": 2, "nome": "Bruno", "email": "bruno@exemplo.com"},
        \\    {"id": 3, "nome": "Carla", "email": "carla@exemplo.com"}
        \\  ]
        \\}
    ;
    try ctx.respondJson(json);
}

fn criarUsuarioHandler(ctx: *Context) anyerror!void {
    // Ler corpo da requisição
    var reader = try ctx.request.reader();
    const corpo = try reader.readAllAlloc(ctx.allocator, 1024 * 64);
    defer ctx.allocator.free(corpo);

    std.debug.print("Novo usuário: {s}\n", .{corpo});

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

Juntando Tudo: Servidor com Router

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

    // Configurar rotas
    var router = Router.init(allocator);
    defer router.deinit();

    try router.get("/", homeHandler);
    try router.get("/api/status", statusHandler);
    try router.get("/api/usuarios", listarUsuariosHandler);
    try router.post("/api/usuarios", criarUsuarioHandler);

    // Iniciar servidor
    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", .{});
    imprimirRotas(&router);

    // Loop principal
    while (true) {
        var arena = std.heap.ArenaAllocator.init(allocator);
        defer arena.deinit();

        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;

        // Criar contexto
        var params = std.StringHashMap([]const u8).init(arena.allocator());
        var ctx = Context{
            .request = &request,
            .allocator = arena.allocator(),
            .params = params,
        };

        // Encontrar e executar handler
        const handler = router.encontrarHandler(request.head.method, request.head.target);
        handler(&ctx) catch |err| {
            std.debug.print("Erro no handler: {}\n", .{err});
        };

        _ = params;
    }
}

fn imprimirRotas(router: *Router) void {
    std.debug.print("\nRotas registradas:\n", .{});
    for (router.rotas.items) |rota| {
        std.debug.print("  {s} {s}\n", .{ @tagName(rota.metodo), rota.caminho });
    }
    std.debug.print("\n", .{});
}

Note o uso de ArenaAllocator para cada requisição — padrão que discutimos na Masterclass de Memória. Toda memória temporária da requisição é liberada automaticamente.

Implementando Matching de Parâmetros

Para rotas como /api/usuarios/:id, precisamos de matching com parâmetros:

fn matchPath(pattern: []const u8, caminho: []const u8) bool {
    var pat_it = std.mem.splitScalar(u8, pattern, '/');
    var cam_it = std.mem.splitScalar(u8, caminho, '/');

    while (true) {
        const pat_seg = pat_it.next();
        const cam_seg = cam_it.next();

        if (pat_seg == null and cam_seg == null) return true;
        if (pat_seg == null or cam_seg == null) return false;

        const p = pat_seg.?;
        const c = cam_seg.?;

        // Segmentos que começam com ':' são parâmetros (match qualquer coisa)
        if (p.len > 0 and p[0] == ':') continue;

        // Segmentos normais devem ser iguais
        if (!std.mem.eql(u8, p, c)) return false;
    }
}

fn extrairParams(
    allocator: std.mem.Allocator,
    pattern: []const u8,
    caminho: []const u8,
) !std.StringHashMap([]const u8) {
    var params = std.StringHashMap([]const u8).init(allocator);

    var pat_it = std.mem.splitScalar(u8, pattern, '/');
    var cam_it = std.mem.splitScalar(u8, caminho, '/');

    while (pat_it.next()) |pat_seg| {
        const cam_seg = cam_it.next() orelse break;

        if (pat_seg.len > 0 and pat_seg[0] == ':') {
            const nome_param = pat_seg[1..]; // Remove ':'
            try params.put(nome_param, cam_seg);
        }
    }

    return params;
}

Agora podemos definir rotas como:

try router.get("/api/usuarios/:id", buscarUsuarioHandler);
try router.delete("/api/usuarios/:id", deletarUsuarioHandler);

E no handler, acessar o parâmetro:

fn buscarUsuarioHandler(ctx: *Context) anyerror!void {
    const id = ctx.params.get("id") orelse "desconhecido";
    const json = try std.fmt.allocPrint(
        ctx.allocator,
        \\{{"id": "{s}", "nome": "Usuário {s}"}}
    ,
        .{ id, id },
    );
    defer ctx.allocator.free(json);
    try ctx.respondJson(json);
}

Próximos Passos

No próximo artigo, vamos expandir este sistema para construir uma API REST completa com JSON, incluindo serialização/deserialização, validação e CRUD completo.

Referências

Continue aprendendo Zig

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