Voltar ao blog
Tutoriais

Tutorial: API REST com Hono e Cloudflare Workers do Zero

Bruno Bracaioli
Tutorial: API REST com Hono e Cloudflare Workers do Zero

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 routes no wrangler.jsonc
  • Rate limiting: use cloudflare/workers-rate-limit ou Workers KV
  • Múltiplos usuários: adicione coluna user_id na tabela e extraia do JWT
  • Migrations versionadas: use wrangler d1 migrations create pra 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.

Compartilhar:

Fique por dentro

Receba novos artigos sobre IA, desenvolvimento e tecnologia direto no seu email.