Construindo APIs com Hono no Cloudflare Workers: do local ao deploy

1. Introdução ao Hono e Cloudflare Workers

Hono é um framework web ultraleve e extremamente rápido, projetado especificamente para ambientes de edge computing. Com menos de 14KB de tamanho, ele oferece performance comparável a frameworks nativos, suporte nativo a TypeScript e uma API intuitiva baseada em middleware. O Cloudflare Workers, por sua vez, é a plataforma serverless da Cloudflare que executa código JavaScript/TypeScript na borda global, em mais de 330 data centers ao redor do mundo.

A combinação Hono + Cloudflare Workers é ideal para construir APIs REST, microsserviços, gateways de API e webhooks que exigem latência mínima e alta disponibilidade. Diferente de soluções tradicionais baseadas em Node.js, essa stack elimina a necessidade de gerenciar servidores, oferece escalabilidade automática e reduz custos operacionais.

2. Configuração do ambiente de desenvolvimento

Antes de começar, certifique-se de ter os seguintes pré-requisitos instalados:
- Node.js (versão 18 ou superior)
- npm ou pnpm
- Conta gratuita na Cloudflare
- Wrangler CLI (instalado globalmente com npm install -g wrangler)

Para inicializar um novo projeto com Hono, execute:

npm create cloudflare@latest minha-api-hono -- --template hono
cd minha-api-hono
npm install

A estrutura de pastas recomendada para o projeto é:

minha-api-hono/
├── src/
│   ├── routes/
│   │   ├── usuarios.ts
│   │   └── produtos.ts
│   ├── middlewares/
│   │   ├── auth.ts
│   │   └── cors.ts
│   ├── schemas/
│   │   └── validacao.ts
│   ├── utils/
│   │   └── respostas.ts
│   └── index.ts
├── tests/
│   └── api.test.ts
├── wrangler.toml
└── package.json

3. Construção da API com Hono — rotas e middlewares

Vamos criar uma API simples de gerenciamento de usuários. No arquivo src/index.ts, configure o servidor básico:

import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { usuariosRouter } from './routes/usuarios'

const app = new Hono()

// Middlewares globais
app.use('*', cors())
app.use('*', logger())

// Rotas
app.route('/api/usuarios', usuariosRouter)

// Health check
app.get('/health', (c) => c.json({ status: 'ok', timestamp: Date.now() }))

export default app

Agora, crie o arquivo src/routes/usuarios.ts com as rotas CRUD:

import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const usuariosRouter = new Hono()

// Schema de validação com Zod
const usuarioSchema = z.object({
  nome: z.string().min(3).max(100),
  email: z.string().email(),
  idade: z.number().int().positive().optional()
})

// Banco simulado em memória (substituir por D1 em produção)
let usuarios = [
  { id: 1, nome: 'João Silva', email: 'joao@exemplo.com', idade: 30 }
]
let proximoId = 2

// GET - Listar todos os usuários
usuariosRouter.get('/', (c) => {
  return c.json({ dados: usuarios, total: usuarios.length })
})

// GET - Buscar usuário por ID
usuariosRouter.get('/:id', (c) => {
  const id = Number(c.req.param('id'))
  const usuario = usuarios.find(u => u.id === id)
  if (!usuario) {
    return c.json({ erro: 'Usuário não encontrado' }, 404)
  }
  return c.json({ dados: usuario })
})

// POST - Criar novo usuário
usuariosRouter.post('/', zValidator('json', usuarioSchema), async (c) => {
  const body = await c.req.json()
  const novoUsuario = { id: proximoId++, ...body }
  usuarios.push(novoUsuario)
  return c.json({ dados: novoUsuario }, 201)
})

// PUT - Atualizar usuário
usuariosRouter.put('/:id', zValidator('json', usuarioSchema), async (c) => {
  const id = Number(c.req.param('id'))
  const body = await c.req.json()
  const index = usuarios.findIndex(u => u.id === id)
  if (index === -1) {
    return c.json({ erro: 'Usuário não encontrado' }, 404)
  }
  usuarios[index] = { ...usuarios[index], ...body }
  return c.json({ dados: usuarios[index] })
})

// DELETE - Remover usuário
usuariosRouter.delete('/:id', (c) => {
  const id = Number(c.req.param('id'))
  const index = usuarios.findIndex(u => u.id === id)
  if (index === -1) {
    return c.json({ erro: 'Usuário não encontrado' }, 404)
  }
  usuarios.splice(index, 1)
  return c.json({ mensagem: 'Usuário removido com sucesso' })
})

export { usuariosRouter }

4. Integração com o ecossistema Cloudflare Workers

Para persistência real, substitua o banco em memória pelo D1 (SQLite serverless). Primeiro, configure o binding no wrangler.toml:

name = "minha-api-hono"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "meu-banco"
database_id = "seu-id-aqui"

Agora, modifique a rota para usar D1:

import { D1Database } from '@cloudflare/workers-types'

// No handler da rota
usuariosRouter.get('/', async (c) => {
  const db = c.env.DB as D1Database
  const { results } = await db.prepare('SELECT * FROM usuarios').all()
  return c.json({ dados: results })
})

Para variáveis de ambiente e secrets, adicione no wrangler.toml:

[vars]
API_VERSION = "v1"
MAX_REQUESTS = "100"

# Secrets (definir via CLI)
# wrangler secret put JWT_SECRET

5. Testes locais e simulação de produção

Execute o servidor localmente com hot reload:

wrangler dev --port 8787

Para testes automatizados com Vitest e Miniflare, instale as dependências:

npm install -D vitest @cloudflare/vitest-pool-workers

Crie o arquivo tests/api.test.ts:

import { describe, it, expect, beforeAll } from 'vitest'
import { createExecutionContext } from 'cloudflare:test'
import app from '../src/index'

describe('API de Usuários', () => {
  it('GET /health deve retornar status ok', async () => {
    const req = new Request('http://localhost/health')
    const env = { DB: null }
    const ctx = createExecutionContext()
    const res = await app.fetch(req, env, ctx)
    const data = await res.json()
    expect(res.status).toBe(200)
    expect(data.status).toBe('ok')
  })

  it('POST /api/usuarios deve criar novo usuário', async () => {
    const novoUsuario = { nome: 'Maria', email: 'maria@teste.com', idade: 25 }
    const req = new Request('http://localhost/api/usuarios', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(novoUsuario)
    })
    const env = { DB: null }
    const ctx = createExecutionContext()
    const res = await app.fetch(req, env, ctx)
    const data = await res.json()
    expect(res.status).toBe(201)
    expect(data.dados.nome).toBe('Maria')
  })
})

Execute os testes com:

npx vitest run

6. Deploy contínuo e monitoramento

Para deploy manual:

wrangler deploy

Configure um pipeline CI/CD com GitHub Actions. Crie .github/workflows/deploy.yml:

name: Deploy para Cloudflare Workers

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test
      - name: Deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}

No Cloudflare Dashboard, monitore métricas como requisições por segundo, latência média, erros HTTP e uso de CPU. Ative alertas para picos de erro ou lentidão.

7. Boas práticas e otimizações para produção

Implemente rate limiting com Hono:

import { rateLimiter } from 'hono-rate-limiter'

app.use('/api/*', rateLimiter({
  windowMs: 60 * 1000, // 1 minuto
  max: 100, // máximo de 100 requisições por minuto
  message: { erro: 'Muitas requisições. Tente novamente mais tarde.' }
}))

Utilize Workers KV para cache de respostas frequentes:

import { KVNamespace } from '@cloudflare/workers-types'

app.get('/api/produtos/populares', async (c) => {
  const cache = c.env.CACHE as KVNamespace
  const cacheKey = 'produtos_populares'

  const cached = await cache.get(cacheKey)
  if (cached) {
    return c.json(JSON.parse(cached))
  }

  const dados = await buscarProdutosPopulares()
  await cache.put(cacheKey, JSON.stringify(dados), { expirationTtl: 300 })

  return c.json(dados)
})

Para segurança, sempre valide entradas com Zod, sanitize dados antes de persistir e utilize prepared statements para consultas SQL a fim de prevenir injeção.

Referências