Angela Sofíá Osorio
Tiempo de lectura 5 minutes
Fecha de publicación
He creado mi propio sistema de Stories efímeras: Arquitectura Limpia en el Edge con Cloudflare y Hono.js
¿Qué tal, amigos? El día de hoy quiero hablarles de un emocionante viaje de ingeniería: el desarrollo de mi propio sistema de publicación de historias efímeras en video diseñado especialmente para integrarse en mi plataforma web de enlaces (estilo Linktree).
El propósito central de este proyecto es tener un canal directo y dinámico para mantener actualizada a la comunidad respecto a mis LIVES, proyectos y actualizaciones en general, además de compartir un poco de mi día a día de una forma interactiva. Aunque por ahora nos hemos concentrado en dejar el backend 100% operativo y blindado, en los próximos streams y videos en vivo estaremos construyendo la interfaz de usuario e integrándolo por completo con Astro.
A continuación, les comparto las entrañas del proyecto, las decisiones arquitectónicas que tomé y cómo logré exprimir la infraestructura moderna para mantenerlo completamente gratis y escalable.
El Desafío: Videos efímeros sin costo de infraestructura
El diseño de una aplicación que maneja streaming de video suele levantar alertas rojas en términos de costos de almacenamiento y transferencia de datos (bandwidth). Si dejamos que los archivos se acumulen, cualquier base de datos o bucket de almacenamiento tradicional colapsaría o generaría una factura costosa.
Para solucionar esto, ideé una estrategia basada en dos pilares:
- Desacoplamiento total: Un backend independiente serverless optimizado para el Edge computing.
- Autodestrucción garantizada (Regla de las 24 horas): Tanto los registros de la base de datos como los archivos de video físicos se limpian automáticamente al cumplir un día de antigüedad, permitiendo que la aplicación viva eternamente dentro de las capas gratuitas más generosas del mercado cloud.
El Stack Tecnológico
Elegí herramientas modernas del ecosistema JavaScript/TypeScript enfocadas en el rendimiento extremo y latencia ultra baja:
- Hono.js (^4.12.14): Un framework web diseñado específicamente para runtimes de nueva generación como Cloudflare Workers. Es increíblemente rápido, ligero y cuenta con un tipado estricto excelente.
- Cloudflare Workers: Infraestructura serverless que ejecuta el código directamente en los nodos perimetrales (Edge) de Cloudflare, reduciendo los tiempos de respuesta a milisegundos sin importar en qué parte del mundo esté el usuario.
- Cloudflare D1: Una base de datos relacional nativa (SQLite) en la nube con soporte estricto de integridad referencial.
- Cloudflare R2: Almacenamiento de objetos compatible con la API S3 de AWS. Lo mejor de R2 es que no cobra costos de egreso (egress fees) por la descarga de datos, lo que elimina el principal gasto asociado a los videos.
- Wrangler (^4.4.0): La CLI oficial para compilar, probar localmente y desplegar la infraestructura en Cloudflare.
- TypeScript (ESNext): Lenguaje principal configurado en modo estricto para garantizar la estabilidad del software.
Arquitectura de Software: Desacoplando las Reglas de Negocio
Para asegurar que el backend sea mantenible, escalable y resistente al cambio, decidí implementar Arquitectura Limpia (Clean Architecture) orientada por Diseño Guiado por el Dominio (DDD).
┌─────────────────────────────────────────────────────────────────────┐
│ API Layer │ ← HTTP, Rutas, Controladores, Middlewares ├─────────────────────────────────────────────────────────────────────┤
│ Application Layer │ ← Casos de Uso, DTOs (Data Transfer Objects) ├─────────────────────────────────────────────────────────────────────┤
│ Domain Layer │ ← Entidades puras, Interfaces de Repositorios ├─────────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │ ← Implementaciones concretas (D1 SQL, R2) └─────────────────────────────────────────────────────────────────────┘TeXLa aplicación se divide en cuatro capas concéntricas aisladas. La regla de oro es simple: las dependencias apuntan hacia adentro. La lógica de negocio no sabe si los datos se guardan en D1, Postgres o un archivo de texto, ni tampoco le importa si las peticiones llegan por HTTP (Hono), GraphQL o una consola de comandos.
Estructura del Proyecto
El árbol de directorios refleja fielmente esta separación de responsabilidades, facilitando la navegación en el código fuente:
stories-manager/
├── scripts/
│ └── create-admin.js # Script para inicializar administradores
├── src/
│ ├── index.ts # Punto de entrada (Hono application + Cron Handler)
│ ├── api/
│ │ ├── controllers/
│ │ │ ├── admin.controller.ts # Gestión de moderación y bloqueo de IPs
│ │ │ ├── auth.controller.ts # Autenticación y emisión de JWT
│ │ │ └── stories.controller.ts # Endpoints para interactuar con historias
│ │ ├── middlewares/
│ │ │ ├── auth.middleware.ts # Guardián de rutas administrativas mediante JWT
│ │ │ └── ipBan.middleware.ts # Firewall perimetral contra atacantes bloqueados
│ │ └── routes/ # Enrutadores declarativos de la API
│ ├── application/
│ │ ├── dtos/
│ │ │ └── CommentResponseDTO.ts # Estructuras de datos seguras para el cliente
│ │ └── use-cases/ # Casos de uso concretos (Reglas de negocio)
│ ├── domain/
│ │ ├── entities/ # Modelos matemáticos / estructurales puros
│ │ └── repositories/ # Contratos de abstracción de datos
│ └── infrastructure/
│ ├── db/
│ │ └── schema.sql # Definición de tablas y cascadas en SQLite
│ ├── repositories/ # Sentencias SQL preparadas sobre Cloudflare D1
│ └── storage/
│ └── R2StorageService.ts # Integración con AWS SDK v3 para URLs Pre-firmadas
├── wrangler.jsonc # Orquestación de recursos e infraestructura Cloud
└── worker-configuration.d.ts # Tipados estáticos autogenerados de CloudflareTeXFlujo de Funcionamiento y Ejemplos de Código
Para entender el valor de este diseño, analicemos dos flujos esenciales de la aplicación desde la perspectiva del código fuente.
1. Privacidad del Usuario mediante DTOs (Data Transfer Objects)
Cuando un usuario solicita ver los comentarios de un video, la base de datos registra información interna confidencial, como la dirección IP del autor (utilizada para moderación). Devolver la entidad de dominio directamente al cliente expondría datos sensibles en la red.
Para solucionar esto de manera elegante, la Capa de Aplicación intercepta los datos y los procesa mediante un DTO.
Primero definimos la estructura pública que verá nuestro frontend de Astro:
// src/application/dtos/CommentResponseDTO.ts
export interface CommentResponseDTO {
id: string;
content: string;
createdAt: string;
// Omitimos intencionalmente 'ipAddress' y 'storyId' por privacidad y optimización
}TypeScriptPosteriormente, el caso de uso se encarga de realizar la transformación limpia, aislando por completo la infraestructura de la respuesta HTTP:
// src/application/use-cases/GetStoryCommentsUseCase.ts
import type { ICommentRepository } from '../../domain/repositories/ICommentRepository';
import type { CommentResponseDTO } from '../dtos/CommentResponseDTO';
export class GetStoryCommentsUseCase {
constructor(private commentRepo: ICommentRepository) {}
async execute(storyId: string): Promise<CommentResponseDTO[]> {
// 1. Recuperamos las entidades crudas del repositorio
const comments = await this.commentRepo.getByStoryId(storyId);
// 2. Mapeamos hacia el DTO omitiendo las direcciones IP
return comments.map(comment => ({
id: comment.id,
content: comment.content,
createdAt: comment.createdAt
}));
}
}TypeScript2. El Controlador HTTP en Hono
El controlador actúa exclusivamente como un traductor del protocolo HTTP. Recibe la petición del cliente, delega la ejecución al caso de uso correspondiente e inyecta la respuesta JSON con su código de estado adecuado:
// src/api/controllers/stories.controller.ts
import type { Context } from 'hono';
import { D1CommentRepository } from '../../infrastructure/repositories/D1CommentRepository';
import { GetStoryCommentsUseCase } from '../../application/use-cases/GetStoryCommentsUseCase';
export class StoriesController {
static async getComments(c: Context) {
try {
const storyId = c.req.param('id');
// Inyección manual de infraestructura en el caso de uso
const commentRepo = new D1CommentRepository(c.env.stories_manager);
const useCase = new GetStoryCommentsUseCase(commentRepo);
const comments = await useCase.execute(storyId);
return c.json({ success: true, data: comments }, 200);
} catch (error: any) {
return c.json({ success: false, error: error.message }, 500);
}
}
}
TypeScriptAutomatización en la Nube: La Magia Detrás de la Capa Gratuita
Para asegurar que los videos no saturen el almacenamiento, configuramos dos procesos paralelos de limpieza:
- Mecanismo Cron en el Worker: Cada hora, Cloudflare dispara un evento nativo (Cron Trigger) que ejecuta nuestro caso de uso
CleanExpiredDataUseCase, barriendo de la base de datos D1 todos los registros que superen las 24 horas de antigüedad. - Políticas de Ciclo de Vida en R2: En lugar de gastar valioso tiempo de cómputo de CPU eliminando archivos de video mediante código, configuramos una regla nativa de ciclo de vida en el panel de Cloudflare R2:
deleteStories --Delete objects after 1 day. Con esto, los videos físicos pesados se auto-eliminan solos a nivel perimetral de forma gratuita y ultra-eficiente.
Próximos pasos y Estado del Repositorio
El backend ha quedado sólidamente estructurado bajo estándares profesionales de diseño de software. El siguiente paso en nuestros próximos streams será abrir el proyecto de Astro y desarrollar los componentes visuales interactivos: el carrusel de historias, los reproductores de video inmersivos y la caja de comentarios dinámicos consumiendo de extremo a extremo esta API en tiempo real.
El repositorio de este gestor de historias actualmente se encuentra en modo privado. Sin embargo, si estás interesado en inspeccionar el código fuente completo, aplicar esta arquitectura en tus propios proyectos o colaborar en las siguientes etapas de desarrollo, más adelante estaré dando actualizaciones sobre como puedes acceder al repositorio.
Contents

