Nos artigos anteriores, construímos um servidor HTTP e um sistema de roteamento. Agora vamos construir uma API REST completa com operações CRUD, serialização/deserialização JSON e tratamento de erros adequado.
Trabalhando com JSON em Zig
Zig inclui um parser JSON robusto na biblioteca padrão (std.json). Vamos explorar como usá-lo:
Deserializando JSON (Parse)
const std = @import("std");
const Usuario = struct {
nome: []const u8,
email: []const u8,
idade: u32,
ativo: bool = true,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json_str =
\\{"nome": "Ana Silva", "email": "ana@exemplo.com", "idade": 28, "ativo": true}
;
// Deserializar JSON para struct
const parsed = try std.json.parseFromSlice(
Usuario,
allocator,
json_str,
.{},
);
defer parsed.deinit();
const usuario = parsed.value;
std.debug.print("Nome: {s}\n", .{usuario.nome});
std.debug.print("Email: {s}\n", .{usuario.email});
std.debug.print("Idade: {d}\n", .{usuario.idade});
std.debug.print("Ativo: {}\n", .{usuario.ativo});
}
Serializando JSON (Stringify)
const std = @import("std");
const Usuario = struct {
id: u32,
nome: []const u8,
email: []const u8,
idade: u32,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const usuario = Usuario{
.id = 1,
.nome = "Bruno Costa",
.email = "bruno@exemplo.com",
.idade = 32,
};
// Serializar struct para JSON
var buffer = std.ArrayList(u8).init(allocator);
defer buffer.deinit();
try std.json.stringify(usuario, .{}, buffer.writer());
std.debug.print("JSON: {s}\n", .{buffer.items});
}
O Modelo de Dados
Para nossa API, vamos criar um gerenciador de tarefas (To-Do):
const std = @import("std");
const Tarefa = struct {
id: u32,
titulo: []const u8,
descricao: []const u8,
concluida: bool,
criada_em: i64,
};
const TarefaInput = struct {
titulo: []const u8,
descricao: []const u8 = "",
};
const TarefaStore = struct {
tarefas: std.AutoHashMap(u32, Tarefa),
proximo_id: u32,
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator) TarefaStore {
return .{
.tarefas = std.AutoHashMap(u32, Tarefa).init(allocator),
.proximo_id = 1,
.allocator = allocator,
};
}
fn deinit(self: *TarefaStore) void {
var it = self.tarefas.valueIterator();
while (it.next()) |tarefa| {
self.allocator.free(tarefa.titulo);
if (tarefa.descricao.len > 0) {
self.allocator.free(tarefa.descricao);
}
}
self.tarefas.deinit();
}
fn criar(self: *TarefaStore, input: TarefaInput) !Tarefa {
const id = self.proximo_id;
self.proximo_id += 1;
// Copiar strings para memória permanente
const titulo = try self.allocator.alloc(u8, input.titulo.len);
@memcpy(titulo, input.titulo);
var descricao: []const u8 = "";
if (input.descricao.len > 0) {
const desc = try self.allocator.alloc(u8, input.descricao.len);
@memcpy(desc, input.descricao);
descricao = desc;
}
const tarefa = Tarefa{
.id = id,
.titulo = titulo,
.descricao = descricao,
.concluida = false,
.criada_em = std.time.timestamp(),
};
try self.tarefas.put(id, tarefa);
return tarefa;
}
fn obter(self: *TarefaStore, id: u32) ?Tarefa {
return self.tarefas.get(id);
}
fn listar(self: *TarefaStore, allocator: std.mem.Allocator) ![]Tarefa {
var lista = std.ArrayList(Tarefa).init(allocator);
var it = self.tarefas.valueIterator();
while (it.next()) |tarefa| {
try lista.append(tarefa.*);
}
return lista.toOwnedSlice();
}
fn atualizar(self: *TarefaStore, id: u32, concluida: bool) bool {
if (self.tarefas.getPtr(id)) |tarefa| {
tarefa.concluida = concluida;
return true;
}
return false;
}
fn remover(self: *TarefaStore, id: u32) bool {
if (self.tarefas.fetchRemove(id)) |entry| {
self.allocator.free(entry.value.titulo);
if (entry.value.descricao.len > 0) {
self.allocator.free(entry.value.descricao);
}
return true;
}
return false;
}
};
Implementando os Endpoints CRUD
GET /api/tarefas — Listar Todas
fn listarTarefasHandler(ctx: *Context, store: *TarefaStore) !void {
const tarefas = try store.listar(ctx.allocator);
defer ctx.allocator.free(tarefas);
var buffer = std.ArrayList(u8).init(ctx.allocator);
defer buffer.deinit();
const writer = buffer.writer();
try writer.writeAll("{\"tarefas\": [");
for (tarefas, 0..) |tarefa, i| {
if (i > 0) try writer.writeAll(", ");
try std.json.stringify(tarefa, .{}, writer);
}
try writer.writeAll("], \"total\": ");
try std.fmt.formatInt(tarefas.len, 10, .lower, .{}, writer);
try writer.writeAll("}");
try ctx.respondJson(buffer.items);
}
POST /api/tarefas — Criar Nova
fn criarTarefaHandler(ctx: *Context, store: *TarefaStore) !void {
// Ler corpo da requisição
var reader = try ctx.request.reader();
const corpo = try reader.readAllAlloc(ctx.allocator, 64 * 1024);
defer ctx.allocator.free(corpo);
// Deserializar JSON
const parsed = std.json.parseFromSlice(
TarefaInput,
ctx.allocator,
corpo,
.{},
) catch {
try ctx.respond(
"{\"erro\": \"JSON inválido\"}",
.{ .status = .bad_request },
);
return;
};
defer parsed.deinit();
// Validar
if (parsed.value.titulo.len == 0) {
try ctx.respond(
"{\"erro\": \"Título é obrigatório\"}",
.{ .status = .bad_request },
);
return;
}
// Criar tarefa
const tarefa = try store.criar(parsed.value);
// Serializar resposta
var buffer = std.ArrayList(u8).init(ctx.allocator);
defer buffer.deinit();
try std.json.stringify(tarefa, .{}, buffer.writer());
try ctx.respondJson(buffer.items);
}
GET /api/tarefas/:id — Obter Uma
fn obterTarefaHandler(ctx: *Context, store: *TarefaStore) !void {
const id_str = ctx.params.get("id") orelse {
try ctx.respond("{\"erro\": \"ID não fornecido\"}", .{ .status = .bad_request });
return;
};
const id = std.fmt.parseInt(u32, id_str, 10) catch {
try ctx.respond("{\"erro\": \"ID inválido\"}", .{ .status = .bad_request });
return;
};
if (store.obter(id)) |tarefa| {
var buffer = std.ArrayList(u8).init(ctx.allocator);
defer buffer.deinit();
try std.json.stringify(tarefa, .{}, buffer.writer());
try ctx.respondJson(buffer.items);
} else {
try ctx.respond(
"{\"erro\": \"Tarefa não encontrada\"}",
.{ .status = .not_found },
);
}
}
DELETE /api/tarefas/:id — Remover
fn removerTarefaHandler(ctx: *Context, store: *TarefaStore) !void {
const id_str = ctx.params.get("id") orelse {
try ctx.respond("{\"erro\": \"ID não fornecido\"}", .{ .status = .bad_request });
return;
};
const id = std.fmt.parseInt(u32, id_str, 10) catch {
try ctx.respond("{\"erro\": \"ID inválido\"}", .{ .status = .bad_request });
return;
};
if (store.remover(id)) {
try ctx.respondJson("{\"removido\": true}");
} else {
try ctx.respond(
"{\"erro\": \"Tarefa não encontrada\"}",
.{ .status = .not_found },
);
}
}
Tratamento de Erros Padronizado
Uma boa API retorna erros consistentes:
const ApiError = struct {
erro: []const u8,
codigo: u16,
detalhes: ?[]const u8 = null,
fn toJson(self: ApiError, allocator: std.mem.Allocator) ![]u8 {
var buffer = std.ArrayList(u8).init(allocator);
const writer = buffer.writer();
try writer.writeAll("{\"erro\": \"");
try writer.writeAll(self.erro);
try writer.writeAll("\", \"codigo\": ");
try std.fmt.formatInt(self.codigo, 10, .lower, .{}, writer);
if (self.detalhes) |det| {
try writer.writeAll(", \"detalhes\": \"");
try writer.writeAll(det);
try writer.writeAll("\"");
}
try writer.writeAll("}");
return buffer.toOwnedSlice();
}
};
fn responderErro(ctx: *Context, err: ApiError) !void {
const json = try err.toJson(ctx.allocator);
defer ctx.allocator.free(json);
const status: std.http.Status = @enumFromInt(err.codigo);
try ctx.request.respond(json, .{
.status = status,
.extra_headers = &.{
.{ .name = "content-type", .value = "application/json" },
},
});
}
Testando a API
Com a API rodando, teste com curl:
# Listar tarefas (inicialmente vazio)
curl http://localhost:8080/api/tarefas
# Criar tarefa
curl -X POST http://localhost:8080/api/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Aprender Zig", "descricao": "Completar a série de tutoriais"}'
# Obter tarefa específica
curl http://localhost:8080/api/tarefas/1
# Remover tarefa
curl -X DELETE http://localhost:8080/api/tarefas/1
Próximos Passos
No próximo artigo, vamos implementar o padrão Middleware para adicionar logging, autenticação, CORS e rate limiting de forma modular.
Referências
- Roteamento (Artigo 2) — Artigo anterior
- Builtins de Zig — Funções built-in úteis
- Arena Allocator — Padrão de memória por requisição