
O que vamos construir
Uma API REST de gerenciamento de tarefas (todo list) com:
- 5 endpoints (
GET /tasks,GET /tasks/:id,POST /tasks,PATCH /tasks/:id,DELETE /tasks/:id) - Validação de input com Zod
- Persistência em Cloudflare D1 (SQLite serverless)
- Autenticação com bearer token
- Deploy global em ~5 segundos
Tempo total: 30 minutos. Pré-requisitos: Node.js 20+, conta Cloudflare (free), wrangler CLI.
Por que Hono?
Hono é um framework web inspirado em Express/Express, mas escrito do zero pra ser ultra-rápido em runtimes edge. Funciona em Workers, Bun, Deno, Node, Vercel Edge — tudo com a mesma API. O bundle é minúsculo (< 30 KB) e a DX é familiar pra quem já usou Express.
Setup
npm create hono@latest todo-api
# Escolha "cloudflare-workers" como template
# Escolha "npm" como package manager
cd todo-api
npm install
npm install zod
Você ganha um projeto pronto:
todo-api/
├── src/
│ └── index.ts
├── wrangler.jsonc
├── package.json
└── tsconfig.json
Criando o banco D1
npx wrangler d1 create todo-db
A saída vai mostrar algo como:
[[d1_databases]]
binding = "DB"
database_name = "todo-db"
database_id = "abc123-..."
Cole esse bloco no seu wrangler.jsonc:
{
"name": "todo-api",
"main": "src/index.ts",
"compatibility_date": "2026-04-01",
"d1_databases": [
{
"binding": "DB",
"database_name": "todo-db",
"database_id": "abc123-..."
}
]
}
Agora crie a tabela. Crie um arquivo migrations/0001_initial.sql:
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_tasks_created_at ON tasks(created_at);
E aplique no D1 local e remoto:
npx wrangler d1 execute todo-db --local --file=migrations/0001_initial.sql
npx wrangler d1 execute todo-db --remote --file=migrations/0001_initial.sql
Escrevendo a API
Substitua o conteúdo de src/index.ts:
import { Hono } from "hono";
import { cors } from "hono/cors";
import { bearerAuth } from "hono/bearer-auth";
import { z } from "zod";
type Bindings = {
DB: D1Database;
API_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
// Middleware global
app.use("*", cors());
app.use("/tasks/*", async (c, next) => {
const auth = bearerAuth({ token: c.env.API_TOKEN });
return auth(c, next);
});
// Schemas
const createSchema = z.object({
title: z.string().min(1).max(200),
});
const updateSchema = z.object({
title: z.string().min(1).max(200).optional(),
done: z.boolean().optional(),
});
// GET /tasks
app.get("/tasks", async (c) => {
const { results } = await c.env.DB.prepare(
"SELECT id, title, done, created_at FROM tasks ORDER BY created_at DESC"
).all();
return c.json({ tasks: results });
});
// GET /tasks/:id
app.get("/tasks/:id", async (c) => {
const id = c.req.param("id");
const task = await c.env.DB.prepare(
"SELECT id, title, done, created_at FROM tasks WHERE id = ?"
).bind(id).first();
if (!task) return c.json({ error: "not_found" }, 404);
return c.json(task);
});
// POST /tasks
app.post("/tasks", async (c) => {
const body = await c.req.json();
const parsed = createSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: "invalid_input", details: parsed.error.flatten() }, 400);
}
const id = crypto.randomUUID();
await c.env.DB.prepare(
"INSERT INTO tasks (id, title, done) VALUES (?, ?, 0)"
).bind(id, parsed.data.title).run();
return c.json({ id, title: parsed.data.title, done: false }, 201);
});
// PATCH /tasks/:id
app.patch("/tasks/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
const parsed = updateSchema.safeParse(body);
if (!parsed.success) {
return c.json({ error: "invalid_input" }, 400);
}
const fields: string[] = [];
const values: unknown[] = [];
if (parsed.data.title !== undefined) {
fields.push("title = ?");
values.push(parsed.data.title);
}
if (parsed.data.done !== undefined) {
fields.push("done = ?");
values.push(parsed.data.done ? 1 : 0);
}
if (fields.length === 0) {
return c.json({ error: "no_fields" }, 400);
}
values.push(id);
const result = await c.env.DB.prepare(
`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`
).bind(...values).run();
if (result.meta.changes === 0) {
return c.json({ error: "not_found" }, 404);
}
return c.json({ id, ...parsed.data });
});
// DELETE /tasks/:id
app.delete("/tasks/:id", async (c) => {
const id = c.req.param("id");
const result = await c.env.DB.prepare(
"DELETE FROM tasks WHERE id = ?"
).bind(id).run();
if (result.meta.changes === 0) {
return c.json({ error: "not_found" }, 404);
}
return c.body(null, 204);
});
export default app;
Configurando o token de autenticação
Crie o secret:
npx wrangler secret put API_TOKEN
# Cole um token forte gerado com: openssl rand -hex 32
Pra desenvolvimento local, crie .dev.vars:
API_TOKEN=dev-token-not-for-production
Rodando localmente
npx wrangler dev
Saída:
⛅️ wrangler 4.x.x
─────────────────
⎔ Starting local server...
[wrangler:info] Ready on http://localhost:8787
Testando
Abra outro terminal:
TOKEN="dev-token-not-for-production"
URL="http://localhost:8787"
# Criar uma tarefa
curl -X POST $URL/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Aprender Hono"}'
# Listar tarefas
curl $URL/tasks -H "Authorization: Bearer $TOKEN"
# Marcar como concluída (use o id da resposta anterior)
curl -X PATCH $URL/tasks/SEU_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"done": true}'
# Deletar
curl -X DELETE $URL/tasks/SEU_ID \
-H "Authorization: Bearer $TOKEN"
Tudo verde? Hora de subir.
Deploy
npx wrangler deploy
Saída:
Deployed todo-api triggers
https://todo-api.SEU_USUARIO.workers.dev
Em 5 segundos sua API está no ar globalmente, em 300+ data centers da Cloudflare. Latência abaixo de 50ms em qualquer lugar do mundo.
Próximos passos
- Logs em tempo real:
npx wrangler tail - Custom domain: configure
routesnowrangler.jsonc - Rate limiting: use
cloudflare/workers-rate-limitou Workers KV - Múltiplos usuários: adicione coluna
user_idna tabela e extraia do JWT - Migrations versionadas: use
wrangler d1 migrations createpra automatizar
Conclusão
Você acabou de subir uma API REST production-ready, com banco de dados global, validação, auth, e deploy em segundos — sem provisionar infra, sem cold start, e gastando $0 (free tier). Esse é o nível de DX que define 2026.


