URL Shortener em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um serviço de encurtamento de URLs com servidor HTTP embutido em Zig. O serviço aceita URLs longas, gera códigos curtos e faz o redirecionamento quando o código é acessado.
O Que Vamos Construir
Nosso URL shortener vai:
- Servidor HTTP que aceita requisições POST para criar URLs curtas
- Gerador de códigos alfanuméricos curtos (6 caracteres)
- Redirecionamento HTTP 301 ao acessar URL curta
- Armazenamento em memória com contagem de acessos
- API JSON para criação e consulta de URLs
- Página de estatísticas
Por Que Este Projeto?
URL shorteners são um dos projetos web mais instrutivos. Eles combinam servidor HTTP, geração de identificadores, armazenamento de dados e redirecionamento — tudo em um projeto compacto. Em Zig, construímos o servidor HTTP do zero, o que nos dá entendimento profundo do protocolo.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Conhecimento básico de HTTP (servidor de arquivos)
- Familiaridade com redes TCP em Zig
Passo 1: Estrutura do Projeto
mkdir url-shortener
cd url-shortener
zig init
Passo 2: Armazenamento de URLs
const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
const time = std.time;
/// Representa uma URL armazenada no sistema.
const UrlEntry = struct {
url_original: []const u8,
codigo: []const u8,
acessos: u64,
criado_em: i64, // timestamp em ms
};
/// Armazena e gerencia as URLs encurtadas.
const UrlStore = struct {
/// Mapeia código curto -> entrada de URL
por_codigo: std.StringHashMap(UrlEntry),
/// Mapeia URL original -> código (para evitar duplicatas)
por_url: std.StringHashMap([]const u8),
allocator: mem.Allocator,
contador: u64,
const Self = @This();
const CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const TAMANHO_CODIGO = 6;
pub fn init(allocator: mem.Allocator) Self {
return .{
.por_codigo = std.StringHashMap(UrlEntry).init(allocator),
.por_url = std.StringHashMap([]const u8).init(allocator),
.allocator = allocator,
.contador = 0,
};
}
pub fn deinit(self: *Self) void {
var it = self.por_codigo.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.value_ptr.url_original);
self.allocator.free(entry.value_ptr.codigo);
}
self.por_codigo.deinit();
self.por_url.deinit();
}
/// Gera um código curto baseado no contador interno.
fn gerarCodigo(self: *Self, buf: *[TAMANHO_CODIGO]u8) void {
self.contador += 1;
var n = self.contador;
for (0..TAMANHO_CODIGO) |i| {
const idx = n % CHARSET.len;
buf[TAMANHO_CODIGO - 1 - i] = CHARSET[idx];
n /= CHARSET.len;
}
}
/// Encurta uma URL. Se já foi encurtada antes, retorna o código existente.
pub fn encurtar(self: *Self, url_original: []const u8) ![]const u8 {
// Verificar se já existe
if (self.por_url.get(url_original)) |codigo_existente| {
return codigo_existente;
}
// Gerar novo código
var codigo_buf: [TAMANHO_CODIGO]u8 = undefined;
self.gerarCodigo(&codigo_buf);
const url_copia = try self.allocator.dupe(u8, url_original);
errdefer self.allocator.free(url_copia);
const codigo_copia = try self.allocator.dupe(u8, &codigo_buf);
errdefer self.allocator.free(codigo_copia);
const entrada = UrlEntry{
.url_original = url_copia,
.codigo = codigo_copia,
.acessos = 0,
.criado_em = time.milliTimestamp(),
};
try self.por_codigo.put(codigo_copia, entrada);
try self.por_url.put(url_copia, codigo_copia);
return codigo_copia;
}
/// Busca a URL original pelo código curto e incrementa o contador de acessos.
pub fn resolver(self: *Self, codigo: []const u8) ?[]const u8 {
if (self.por_codigo.getPtr(codigo)) |entrada| {
entrada.acessos += 1;
return entrada.url_original;
}
return null;
}
/// Retorna estatísticas de uma URL pelo código.
pub fn stats(self: *const Self, codigo: []const u8) ?UrlEntry {
return self.por_codigo.get(codigo);
}
/// Retorna o total de URLs armazenadas.
pub fn total(self: *const Self) usize {
return self.por_codigo.count();
}
};
Passo 3: Parser HTTP Simples
/// Representa uma requisição HTTP parseada.
const HttpRequest = struct {
metodo: []const u8,
caminho: []const u8,
corpo: []const u8,
content_length: usize,
};
/// Faz o parsing básico de uma requisição HTTP.
fn parsearRequest(dados: []const u8) ?HttpRequest {
// Encontrar a primeira linha (Request Line)
const fim_linha = mem.indexOf(u8, dados, "\r\n") orelse return null;
const request_line = dados[0..fim_linha];
var it = mem.splitScalar(u8, request_line, ' ');
const metodo = it.next() orelse return null;
const caminho = it.next() orelse return null;
// Encontrar Content-Length
var content_length: usize = 0;
if (mem.indexOf(u8, dados, "Content-Length: ")) |pos| {
const inicio = pos + "Content-Length: ".len;
const fim = mem.indexOfPos(u8, dados, inicio, "\r\n") orelse dados.len;
content_length = fmt.parseInt(usize, dados[inicio..fim], 10) catch 0;
}
// Encontrar o corpo (depois de \r\n\r\n)
const corpo = if (mem.indexOf(u8, dados, "\r\n\r\n")) |pos|
dados[pos + 4 ..]
else
"";
return .{
.metodo = metodo,
.caminho = caminho,
.corpo = corpo,
.content_length = content_length,
};
}
/// Envia uma resposta HTTP.
fn enviarResposta(
socket: posix.socket_t,
status: []const u8,
content_type: []const u8,
corpo: []const u8,
) void {
var buf: [8192]u8 = undefined;
const resposta = fmt.bufPrint(&buf,
"HTTP/1.1 {s}\r\n" ++
"Content-Type: {s}\r\n" ++
"Content-Length: {d}\r\n" ++
"Connection: close\r\n" ++
"\r\n" ++
"{s}",
.{ status, content_type, corpo.len, corpo },
) catch return;
_ = posix.write(socket, resposta) catch {};
}
/// Envia um redirecionamento HTTP 301.
fn enviarRedirecionamento(socket: posix.socket_t, url: []const u8) void {
var buf: [4096]u8 = undefined;
const resposta = fmt.bufPrint(&buf,
"HTTP/1.1 301 Moved Permanently\r\n" ++
"Location: {s}\r\n" ++
"Content-Length: 0\r\n" ++
"Connection: close\r\n" ++
"\r\n",
.{url},
) catch return;
_ = posix.write(socket, resposta) catch {};
}
Passo 4: Roteamento e Handlers
/// Processa uma requisição e envia a resposta apropriada.
fn processarRequest(
store: *UrlStore,
request: HttpRequest,
socket: posix.socket_t,
base_url: []const u8,
) void {
// POST /encurtar — cria uma URL curta
if (mem.eql(u8, request.metodo, "POST") and mem.eql(u8, request.caminho, "/encurtar")) {
const url = mem.trim(u8, request.corpo, " \t\r\n");
if (url.len == 0) {
enviarResposta(socket, "400 Bad Request", "text/plain", "URL nao fornecida");
return;
}
const codigo = store.encurtar(url) catch {
enviarResposta(socket, "500 Internal Server Error", "text/plain", "Erro interno");
return;
};
var buf_resposta: [512]u8 = undefined;
const json = fmt.bufPrint(&buf_resposta,
\\{{"codigo":"{s}","url_curta":"{s}/{s}","url_original":"{s}"}}
, .{ codigo, base_url, codigo, url }) catch return;
enviarResposta(socket, "201 Created", "application/json", json);
return;
}
// GET /stats/<codigo> — estatísticas de uma URL
if (mem.eql(u8, request.metodo, "GET") and mem.startsWith(u8, request.caminho, "/stats/")) {
const codigo = request.caminho[7..];
if (store.stats(codigo)) |entrada| {
var buf_resposta: [1024]u8 = undefined;
const json = fmt.bufPrint(&buf_resposta,
\\{{"codigo":"{s}","url_original":"{s}","acessos":{d}}}
, .{ entrada.codigo, entrada.url_original, entrada.acessos }) catch return;
enviarResposta(socket, "200 OK", "application/json", json);
} else {
enviarResposta(socket, "404 Not Found", "application/json", "{\"erro\":\"URL nao encontrada\"}");
}
return;
}
// GET / — página principal
if (mem.eql(u8, request.metodo, "GET") and mem.eql(u8, request.caminho, "/")) {
const html =
\\<!DOCTYPE html><html><head><title>URL Shortener Zig</title></head>
\\<body><h1>URL Shortener em Zig</h1>
\\<p>API: POST /encurtar com a URL no corpo</p>
\\<p>Exemplo: curl -X POST -d "https://ziglang.org" http://localhost:8080/encurtar</p>
\\</body></html>
;
enviarResposta(socket, "200 OK", "text/html", html);
return;
}
// GET /<codigo> — redirecionamento
if (mem.eql(u8, request.metodo, "GET") and request.caminho.len > 1) {
const codigo = request.caminho[1..]; // remove o '/' inicial
if (store.resolver(codigo)) |url_original| {
enviarRedirecionamento(socket, url_original);
return;
}
}
enviarResposta(socket, "404 Not Found", "text/plain", "Pagina nao encontrada");
}
Passo 5: Servidor HTTP Principal
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = io.getStdOut().writer();
const porta: u16 = 8080;
const base_url = "http://localhost:8080";
var store = UrlStore.init(allocator);
defer store.deinit();
// Criar socket do servidor
const endereco = net.Address.initIp4(.{ 0, 0, 0, 0 }, porta);
const socket = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
defer posix.close(socket);
const optval: i32 = 1;
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, mem.asBytes(&optval));
try posix.bind(socket, &endereco.any, endereco.getOsSockLen());
try posix.listen(socket, 10);
try stdout.print(
\\
\\ ==========================================
\\ URL SHORTENER - Zig
\\ ==========================================
\\ Rodando em {s}
\\
\\ Endpoints:
\\ POST /encurtar - Encurtar URL
\\ GET /<codigo> - Redirecionar
\\ GET /stats/<codigo> - Estatisticas
\\
\\ Teste:
\\ curl -X POST -d "https://ziglang.org" {s}/encurtar
\\ ==========================================
\\
, .{ base_url, base_url });
// Loop principal — aceita conexões
while (true) {
const cliente = posix.accept(socket, null, null) catch continue;
// Ler request
var buf: [8192]u8 = undefined;
const n = posix.read(cliente, &buf) catch {
posix.close(cliente);
continue;
};
if (n == 0) {
posix.close(cliente);
continue;
}
if (parsearRequest(buf[0..n])) |request| {
try stdout.print(" {s} {s}\n", .{ request.metodo, request.caminho });
processarRequest(&store, request, cliente, base_url);
} else {
enviarResposta(cliente, "400 Bad Request", "text/plain", "Request invalida");
}
posix.close(cliente);
}
}
Testes
test "url store - encurtar e resolver" {
const allocator = std.testing.allocator;
var store = UrlStore.init(allocator);
defer store.deinit();
const codigo = try store.encurtar("https://ziglang.org");
try std.testing.expectEqual(@as(usize, 6), codigo.len);
const url = store.resolver(codigo);
try std.testing.expect(url != null);
try std.testing.expectEqualStrings("https://ziglang.org", url.?);
}
test "url store - deduplicacao" {
const allocator = std.testing.allocator;
var store = UrlStore.init(allocator);
defer store.deinit();
const c1 = try store.encurtar("https://example.com");
const c2 = try store.encurtar("https://example.com");
try std.testing.expectEqualStrings(c1, c2);
}
test "url store - contagem de acessos" {
const allocator = std.testing.allocator;
var store = UrlStore.init(allocator);
defer store.deinit();
const codigo = try store.encurtar("https://zig.news");
_ = store.resolver(codigo);
_ = store.resolver(codigo);
_ = store.resolver(codigo);
const entry = store.stats(codigo).?;
try std.testing.expectEqual(@as(u64, 3), entry.acessos);
}
test "parser http - GET simples" {
const raw = "GET /abc123 HTTP/1.1\r\nHost: localhost\r\n\r\n";
const req = parsearRequest(raw).?;
try std.testing.expectEqualStrings("GET", req.metodo);
try std.testing.expectEqualStrings("/abc123", req.caminho);
}
Compilando e Executando
# Compilar e executar
zig build run
# Em outro terminal, testar:
# Criar URL curta
curl -X POST -d "https://ziglang.org" http://localhost:8080/encurtar
# Acessar URL curta (redirecionamento)
curl -L http://localhost:8080/aaaaab
# Ver estatísticas
curl http://localhost:8080/stats/aaaaab
# Rodar testes
zig build test
Conceitos Aprendidos
- Servidor HTTP com parsing manual do protocolo
- Roteamento de requisições por método e caminho
- Geração de códigos com base alfanumérica
- Deduplicação com mapa reverso
- Respostas JSON e redirecionamento HTTP 301
Próximos Passos
- Explore o Servidor HTTP de Arquivos para mais HTTP
- Aprenda sobre Rate Limiter para proteger sua API
- Veja o Proxy HTTP para roteamento avançado
- Consulte a stdlib de redes para mais detalhes