API REST completa construida con Hono, TypeScript, Cloudflare Workers y D1 Database.
- ✅ Autenticación JWT con scrypt-js para hashing de passwords
- 🔒 Sistema de usuarios con registro y login
- 📝 CRUD de Todos privado por usuario (aislamiento de datos)
- 🖼️ Gestión de imágenes con Cloudflare R2 (upload, download público, delete)
- 🌐 Acceso público a imágenes sin necesidad de autenticación
- 🧹 Limpieza automática de imágenes huérfanas al actualizar/eliminar todos
- 🗄️ Cloudflare D1 como base de datos serverless (SQLite)
- ✨ Validación con Zod en todas las rutas
- 📖 Documentación OpenAPI/Swagger interactiva con autenticación global
- 🎯 TypeScript con ESLint (Standard JS)
- ⚡ Desplegable en Cloudflare Workers
- 🔑 Manejo seguro de secretos con variables de entorno
- 🚀 CI/CD con GitHub Actions para despliegue automático
- 🏗️ Arquitectura MVC con controladores separados
- 📦 Estructura modular con schemas y rutas OpenAPI organizadas
- Framework: Hono con OpenAPIHono
- Documentación: Swagger UI + OpenAPI 3.0
- Runtime: Cloudflare Workers
- Base de datos: Cloudflare D1 (SQLite)
- Almacenamiento: Cloudflare R2 (imágenes)
- Autenticación: JWT (jose) + scrypt-js
- Validación: Zod + @hono/zod-openapi
- Testing: Vitest 3.2.4 con UI y Coverage (v8)
- IDs: nanoid
- Linting: ESLint (Standard JS)
- Package Manager: Yarn
src/
├── controllers/ # Lógica de negocio (MVC)
│ ├── auth/ # Registro y login
│ ├── todo/ # CRUD de todos (6 controladores)
│ └── image/ # Gestión de imágenes R2 (3 controladores)
├── openapi/ # Definiciones OpenAPI separadas
│ ├── schemas/ # Schemas Zod reutilizables
│ │ ├── auth.schemas.ts
│ │ ├── todo.schemas.ts
│ │ └── image.schemas.ts
│ └── routes/ # Definiciones createRoute()
│ ├── auth.routes.openapi.ts
│ ├── todo.routes.openapi.ts
│ └── image.routes.openapi.ts
├── routes/ # Routers Hono (un archivo por endpoint)
│ ├── auth/
│ │ ├── index.ts # Router principal de auth
│ │ ├── register.route.ts # Definición de /register
│ │ └── login.route.ts # Definición de /login
│ ├── todo/
│ │ ├── index.ts # Router principal de todos
│ │ ├── list.route.ts # GET /todos
│ │ ├── get.route.ts # GET /todos/:id
│ │ ├── create.route.ts # POST /todos
│ │ ├── update.route.ts # PUT /todos/:id
│ │ ├── patch.route.ts # PATCH /todos/:id
│ │ └── delete.route.ts # DELETE /todos/:id
│ └── image/
│ ├── index.ts # Router principal de imágenes
│ ├── upload.route.ts # POST /images
│ ├── get.route.ts # GET /images/:userId/:imageId
│ └── delete.route.ts # DELETE /images/:userId/:imageId
├── middleware/ # Middlewares (auth JWT)
├── schemas/ # Schemas de validación runtime
├── types/ # Tipos TypeScript
├── utils/ # Utilidades (JWT, crypto, R2)
└── index.ts # Entry point + configuración OpenAPI global
- Node.js 18+
- Yarn
- Cuenta de Cloudflare (para deploy)
# Instalar dependencias
yarn install
# Configurar variables de entorno (crear .dev.vars)
cp .dev.vars.example .dev.vars # Editar con tus valoresCrear archivo .dev.vars en la raíz:
JWT_SECRET=dev-jwt-secret-change-in-production-min-32-chars
PASSWORD_SALT=dev-password-salt-change-in-production# Crear base de datos D1
npx wrangler d1 create basic-hono-todos-db
# Ejecutar migraciones (en orden)
npx wrangler d1 execute basic-hono-todos-db --local --file=./migrations/001_create_todos_table.sql
npx wrangler d1 execute basic-hono-todos-db --local --file=./migrations/002_create_users_table.sql
npx wrangler d1 execute basic-hono-todos-db --local --file=./migrations/003_add_user_id_to_todos.sql# Desarrollo local
yarn dev
# Testing
yarn test # Ejecutar tests en watch mode
yarn test --run # Ejecutar tests una vez
yarn test:ui # Abrir UI interactivo con coverage
yarn test:coverage # Generar reporte de coverage
# Linting
yarn lint
yarn lint:fix
# Deploy a producción
yarn deployLa API incluye documentación interactiva con Swagger UI y esquema de autenticación global:
- Swagger UI: http://localhost:8787/docs (desarrollo)
- Swagger UI Producción: https://basic-hono-api.borisbelmarm.workers.dev/docs
- OpenAPI JSON:
/openapi.json
Características de la documentación:
- ✨ Explorar todos los endpoints disponibles
- 📝 Esquemas de request/response con Zod
- 🧪 Probar las rutas directamente desde el navegador
- 🔐 Autenticación global: Botón "Authorize" para configurar el token JWT una sola vez
- 🏷️ Endpoints organizados por tags (Auth, Todos, Images)
- 📋 Ejemplos de uso en cada endpoint
- Local:
http://localhost:8787 - Producción:
https://basic-hono-api.borisbelmarm.workers.dev
GET /healthRespuesta:
{
"status": "ok",
"timestamp": "2025-11-24T10:00:00.000Z"
}GET /Respuesta:
{
"message": "Bienvenido a la API con Hono",
"documentation": "/docs",
"openapi": "/openapi.json",
"endpoints": {
"health": "/health",
"auth": {
"register": "/auth/register",
"login": "/auth/login"
},
"todos": "/todos (requiere autenticación)",
"images": "/images (requiere autenticación)"
}
}
---
### 🔐 Autenticación
**Todas las rutas protegidas requieren:**
Authorization: Bearer {token}
**En Swagger UI:** Usa el botón "Authorize" (🔒) en la parte superior para configurar el token una sola vez. Se aplicará automáticamente a todos los endpoints protegidos.
#### Registrar Usuario
```bash
POST /auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
Validaciones:
- Email válido
- Password mínimo 6 caracteres
Respuesta exitosa (201):
{
"success": true,
"data": {
"user": {
"id": "abc123",
"email": "user@example.com",
"createdAt": "2025-11-24T10:00:00.000Z",
"updatedAt": "2025-11-24T10:00:00.000Z"
},
"token": "eyJhbGc..."
}
}POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}Respuesta exitosa (200):
{
"success": true,
"data": {
"user": { /* mismo formato que register */ },
"token": "eyJhbGc..."
}
}Todas las rutas de todos requieren el header:
Authorization: Bearer {token}
GET /todos
Authorization: Bearer {token}Respuesta:
{
"success": true,
"data": [
{
"id": "xyz789",
"userId": "abc123",
"title": "Comprar leche",
"completed": false,
"location": {
"latitude": 40.7128,
"longitude": -74.0060
},
"photoUri": "https://example.com/photo.jpg",
"createdAt": "2025-11-24T10:00:00.000Z",
"updatedAt": "2025-11-24T10:00:00.000Z"
}
],
"count": 1
}GET /todos/:id
Authorization: Bearer {token}POST /todos
Authorization: Bearer {token}
Content-Type: application/json
{
"title": "Comprar leche",
"completed": false,
"location": {
"latitude": 40.7128,
"longitude": -74.0060
},
"photoUri": "https://example.com/photo.jpg"
}Validaciones:
title: string, requerido, mínimo 1 caráctercompleted: boolean, opcional (default: false)location.latitude: number, -90 a 90location.longitude: number, -180 a 180photoUri: string, URL válida, opcional
PUT /todos/:id
Authorization: Bearer {token}
Content-Type: application/json
{
"title": "Comprar pan",
"completed": true,
"location": {
"latitude": 40.7128,
"longitude": -74.0060
},
"photoUri": "https://example.com/new-photo.jpg"
}PATCH /todos/:id
Authorization: Bearer {token}
Content-Type: application/json
{
"completed": true
}Nota: Al menos un campo debe ser proporcionado
DELETE /todos/:id
Authorization: Bearer {token}Rutas protegidas (requieren token):
- POST
/images- Subir imagen - DELETE
/images/:userId/:imageId- Eliminar imagen
Rutas públicas:
- GET
/images/:userId/:imageId- Obtener imagen (sin autenticación)
POST /images
Authorization: Bearer {token}
Content-Type: multipart/form-data
FormData:
image: [archivo de imagen]Validaciones:
- Tamaño máximo: 5MB
- Formatos permitidos: JPEG, PNG, WebP, GIF
Respuesta exitosa (201):
{
"success": true,
"data": {
"url": "/images/abc123/xyz789.jpg",
"key": "abc123/xyz789.jpg",
"size": 245678,
"contentType": "image/jpeg"
}
}GET /images/:userId/:imageIdRespuesta: Archivo de imagen con headers de cache
Headers de respuesta:
Content-Type: Tipo MIME de la imagen (image/jpeg, image/png, etc.)Cache-Control:public, max-age=31536000(1 año)
Ejemplo:
curl http://localhost:8787/images/user123/abc123.jpg -o imagen.jpgDELETE /images/:userId/:imageId
Authorization: Bearer {token}Nota: Solo el dueño de la imagen puede eliminarla.
🧹 Limpieza automática:
- Al actualizar el
photoUride un todo, la imagen anterior se elimina automáticamente de R2 - Al eliminar un todo, su imagen asociada se elimina automáticamente de R2
- Previene acumulación de archivos huérfanos
{
"success": true,
"data": { /* objeto o array */ }
}{
"success": false,
"error": "Mensaje de error descriptivo"
}200- OK201- Creado400- Bad Request (validación fallida)401- No autorizado (token inválido/ausente)404- No encontrado409- Conflicto (ej: email ya registrado)500- Error del servidor
Los secretos deben configurarse una sola vez en Cloudflare Workers:
# JWT Secret (generar uno seguro)
npx wrangler secret put JWT_SECRET
# Password Salt (generar uno seguro - NUNCA cambiar después)
npx wrangler secret put PASSWORD_SALTLinux/Mac:
openssl rand -base64 32Windows (PowerShell):
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))Node.js:
require('crypto').randomBytes(32).toString('base64')npx wrangler d1 execute basic-hono-todos-db --remote --file=./migrations/001_create_todos_table.sql
npx wrangler d1 execute basic-hono-todos-db --remote --file=./migrations/002_create_users_table.sql
npx wrangler d1 execute basic-hono-todos-db --remote --file=./migrations/003_add_user_id_to_todos.sqlyarn deployEl proyecto incluye un workflow de GitHub Actions que despliega automáticamente a Cloudflare Workers en cada push a la rama main.
Configuración requerida (una sola vez):
-
Obtén tu API Token de Cloudflare:
- Ve a Cloudflare Dashboard
- Crea un token con permisos "Edit Cloudflare Workers"
-
Obtén tu Account ID:
npx wrangler whoami
-
Configura los secretos en GitHub:
- Ve a
Settings > Secrets and variables > Actions - Agrega los siguientes secretos:
CLOUDFLARE_API_TOKEN: Tu API token de CloudflareCLOUDFLARE_ACCOUNT_ID: Tu Account ID
- Ve a
-
El workflow se ejecutará automáticamente en cada push a
main
Monitoreo del deployment:
- Ve a la pestaña "Actions" en tu repositorio de GitHub
- Verifica el estado del workflow "Deploy to Cloudflare Workers"
# Listar secretos configurados en Cloudflare
npx wrangler secret list
# Ver logs en tiempo real
npx wrangler tail
# Verificar estado del Worker
curl https://basic-hono-api.borisbelmarm.workers.dev/healthEl proyecto incluye una suite completa de tests con 143 tests y 88.83% de coverage:
# Ejecutar todos los tests
yarn test --run
# Modo watch (desarrollo)
yarn test
# UI interactivo con coverage
yarn test:uisrc/
├── controllers/
│ ├── auth/
│ │ ├── login.controller.test.ts # 6 tests
│ │ └── register.controller.test.ts # 7 tests
│ ├── todo/
│ │ ├── create.controller.test.ts # 6 tests
│ │ ├── list.controller.test.ts # 7 tests
│ │ ├── get.controller.test.ts # 6 tests
│ │ ├── update.controller.test.ts # 4 tests
│ │ ├── patch.controller.test.ts # 7 tests
│ │ └── delete.controller.test.ts # 3 tests
│ └── image/
│ ├── upload.controller.test.ts # 5 tests
│ ├── get.controller.test.ts # 3 tests
│ └── delete.controller.test.ts # 3 tests
├── middleware/
│ └── auth.middleware.test.ts # 7 tests
├── schemas/
│ ├── auth.schema.test.ts # 12 tests
│ ├── todo.schema.test.ts # 22 tests
│ ├── image.schema.test.ts # 10 tests
│ └── common.schema.test.ts # 6 tests
├── utils/
│ ├── crypto.test.ts # 13 tests
│ └── jwt.test.ts # 16 tests
└── test/
├── mocks/
│ ├── d1.mock.ts # Mock D1Database
│ └── r2.mock.ts # Mock R2Bucket
└── helpers/
└── context.helper.ts # Helper para Hono context
| Módulo | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
| Controllers | 88.31% | 81.70% | 100% | 88.31% |
| Middleware | 100% | 100% | 100% | 100% |
| Schemas | 100% | 100% | 100% | 100% |
| Utils | 81.69% | 70.58% | 90% | 81.69% |
| Total | 88.83% | 81.61% | 96.42% | 88.83% |
Mocks de Cloudflare:
D1Database: Mock completo con soporte para CRUD, queries complejas y PATCH parcialR2Bucket: Mock de almacenamiento con put, get, delete, head, list
Context Helper:
createMockContext(): Simula el contexto de Hono con bindings, variables, headersparseJsonResponse(): Helper para parsear respuestas JSON- Soporte automático para extracción de parámetros de rutas
Características:
- ✅ Tests unitarios para todos los controllers
- ✅ Tests de integración para auth middleware
- ✅ Validación de schemas con Zod
- ✅ Tests de utils (crypto, JWT)
- ✅ Mocks realistas de Cloudflare Workers
- ✅ UI interactivo con Vitest
- ✅ Coverage con v8 provider
Los tests se ejecutan automáticamente en cada push a main mediante GitHub Actions:
- Run linter
- Run tests ← Valida que todos los 143 tests pasen
- Deploy (solo si tests pasan)El proyecto incluye una colección completa de Bruno con todos los endpoints documentados:
- Abrir colección: Abre Bruno → "Open Collection" → Selecciona la carpeta
bruno/ - Seleccionar entorno: Elige "Local" o "Production"
- Autenticación automática:
- Ejecuta "Register" o "Login"
- El token se guarda automáticamente en la variable secreta
authToken - Todos los requests siguientes usan el token automáticamente
- Probar endpoints:
- Carpeta "Auth" - Registro y login
- Carpeta "Todos" - CRUD de todos
- Carpeta "Images" - Upload, obtener y eliminar imágenes
🔐 Nota: El token se maneja como secret y no se commitea al repositorio.
# 1. Registrar usuario
curl -X POST http://localhost:8787/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"password123"}'
# Respuesta incluye token JWT
# 2. Subir una imagen
curl -X POST http://localhost:8787/images \
-H "Authorization: Bearer eyJhbGc..." \
-F "image=@/ruta/a/tu/imagen.jpg"
# Respuesta incluye URL de la imagen
# 3. Crear un todo con imagen
curl -X POST http://localhost:8787/todos \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{
"title": "Mi primer todo",
"completed": false,
"location": {
"latitude": 40.7128,
"longitude": -74.0060
},
"photoUri": "/images/abc123/xyz789.jpg"
}'
# 4. Listar todos
curl http://localhost:8787/todos \
-H "Authorization: Bearer eyJhbGc..."
# 5. Actualizar todo (cambia la imagen - la anterior se elimina automáticamente)
curl -X PATCH http://localhost:8787/todos/{id} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{"photoUri": "/images/abc123/nueva-imagen.jpg"}'
# 6. Eliminar todo (la imagen se elimina automáticamente de R2)
curl -X DELETE http://localhost:8787/todos/{id} \
-H "Authorization: Bearer eyJhbGc..."El proyecto sigue una arquitectura modular y escalable:
1. Controladores (Controllers):
- Contienen la lógica de negocio
- Separados por dominio (auth, todo, image)
- Independientes de la capa de presentación
2. Definiciones OpenAPI:
- Schemas Zod reutilizables en
src/openapi/schemas/ - Rutas OpenAPI con
createRoute()ensrc/openapi/routes/ - Documentación centralizada y mantenible
3. Routers (Routes):
- Organizados por dominio en subdirectorios (
auth/,todo/,image/) - Cada endpoint en su propio archivo (ej:
login.route.ts,create.route.ts) - Archivo
index.tsen cada dominio que registra todos los endpoints - Ultra modular: fácil de encontrar y modificar endpoints específicos
4. Middleware:
- Autenticación JWT centralizada
- Aplicado a nivel de router completo
5. Utilidades:
- Funciones reutilizables (JWT, crypto, R2)
- Separación de responsabilidades
✅ Mantenibilidad: Código organizado y fácil de encontrar ✅ Escalabilidad: Agregar nuevos endpoints es simple ✅ Reutilización: Schemas compartidos entre rutas ✅ Documentación: OpenAPI auto-generado desde código ✅ Testing: Controladores testeables independientemente ✅ Legibilidad: Archivos pequeños y enfocados
basic-hono-api/
├── src/
│ ├── controllers/ # Lógica de negocio (MVC)
│ ├── openapi/ # Definiciones OpenAPI separadas
│ │ ├── schemas/ # Schemas Zod reutilizables
│ │ └── routes/ # createRoute() por dominio
│ ├── routes/ # Routers Hono (conectan OpenAPI + Controllers)
│ ├── middleware/ # Middlewares (auth JWT)
│ ├── schemas/ # Schemas de validación runtime
│ ├── types/ # Tipos TypeScript
│ ├── utils/ # Utilidades (JWT, crypto, R2)
│ └── index.ts # Entry point + OpenAPI global config
├── migrations/ # SQL migrations para D1
├── bruno/ # Colección de requests con Bruno
│ ├── Auth/ # Requests de autenticación
│ ├── Todos/ # Requests CRUD de todos
│ ├── Images/ # Requests de imágenes
│ └── environments/ # Entornos (Local, Production)
├── .github/workflows/ # CI/CD con GitHub Actions
├── wrangler.toml # Config Cloudflare Workers + D1 + R2
├── .dev.vars # Variables de entorno local
└── package.json
- ✅ Passwords hasheados con scrypt (N=16384, r=8, p=1)
- ✅ JWT con expiración de 7 días
- ✅ Validación estricta con Zod en todas las entradas
- ✅ Secretos en variables de entorno (nunca en código)
- ✅ Aislamiento de datos por usuario (WHERE user_id)
- ✅ Control de permisos en eliminación de imágenes (solo el dueño)
- ✅ Validación de archivos (tipo y tamaño de imágenes)
- ✅ Limpieza automática de recursos huérfanos en R2
- ✅ HTTPS obligatorio en producción (Cloudflare)
- ✅ Rate limiting automático de Cloudflare Workers
- 🔄 Rotar
JWT_SECRETperiódicamente - 🚫 Nunca cambiar
PASSWORD_SALT(invalidaría todas las contraseñas) - 📊 Monitorear logs con
wrangler tail - 🔐 Usar passwords fuertes (>12 caracteres recomendado)
- 🌐 Las imágenes son públicamente accesibles sin autenticación - considera implementar:
- Signed URLs con expiración para mayor control
- Validación de referrer para prevenir hotlinking
- Rate limiting específico para endpoints de imágenes
- Fork el proyecto
- Crea una rama para tu feature (
git checkout -b feature/amazing-feature) - Commit tus cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abre un Pull Request
MIT
Boris Belmar - borisbelmarm@gmail.com