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
- Servidor HTTP Básico (Artigo 1) — Artigo anterior
- Arena Allocator — Arena por requisição
- Receitas de Strings — Manipulação de strings em Zig