~/jonasfink.dev
← cd ..

Reciply – Extractor de recetas

2026 · Full-Stack Developer
ReactTypeScriptTailwind v4Node.jsExpress.jsMongoDBGemini APICloudinary
Reciply – Extractor de recetas — screenshotReciply – Extractor de recetas — screenshotReciply – Extractor de recetas — screenshotReciply – Extractor de recetas — screenshotReciply – Extractor de recetas — screenshot

Situación

El contenido de recetas vive en reels de redes sociales, pero la receta en sí queda atrapada en una descripción que hay que desplazar, capturar y volver a teclear a mano. Ninguna herramienta convertía la URL de un vídeo en una receta guardada y estructurada que fuera realmente tuya —el objetivo era: una URL de entrada, una receta estructurada de salida, guardada en tu propia biblioteca.

Tarea

Construir una app full-stack que tome la URL de un reel de Instagram/Facebook y produzca una receta limpia, editable y persistida con cuentas de usuario, imágenes y compartición comunitaria opcional —sin contaminar la base de datos con salida de IA sin confirmar, y lo bastante endurecida para desplegarla. La restricción de diseño central: la extracción no debe persistir. El parseo devuelve un borrador efímero que el usuario edita y solo entonces guarda, de modo que la BD nunca se llena de conjeturas de la IA.

Acción

La pipeline es URL → scraper → parser de IA → vista previa editable → guardar. Un scraper que prioriza la descripción invoca yt-dlp (--dump-json, sin navegador) y cubre ~80 % de los reels; la transcripción con Whisper y los fallbacks con navegador headless están documentados pero deliberadamente aplazados para mantener v1 pequeña. Las descripciones van a Gemini con un responseSchema para salida estructurada, y cada resultado se valida con Zod antes de llegar al cliente. La autenticación es JWT con tokens de acceso de vida corta (15 min) más tokens de refresco rotatorios (7 días, guardados como hash SHA-256 y entregados como cookie httpOnly) con detección de reutilización, bcrypt (coste 12) para las contraseñas y un alcance de propiedad por userId en cada lectura, escritura y borrado (404 en lugar de filtrar la existencia). Las imágenes se suben a Cloudinary con el public_id derivado de forma determinista de la URL guardada, y un feed comunitario permite a los usuarios publicar y «añadir a mi biblioteca» como un snapshot independiente; la identidad de la receta es la sourceUrl, así que los duplicados devuelven un 409 mediante guardas a nivel de aplicación en vez de un índice único arriesgado. Las rutas costosas (extract, community) se cachean en memoria, los rate limiters cubren la API global más extract, subida de imágenes y refresco, el entorno se valida con Zod al arrancar, los errores usan un único envoltorio { message }, y el auto-reenvío async de Express 5 eliminó ~16 bloques try/catch redundantes.

Resultado

La v1 funciona de extremo a extremo: URL → yt-dlp → Gemini → vista previa editable → MongoDB, con autenticación, bibliotecas por usuario, favoritos, subida de imágenes y un feed comunitario público con deduplicación. Está endurecida para desplegar —caché en memoria, cobertura completa de rate limiters, validación del entorno al arrancar y aislamiento estricto de propiedad. El alcance se mantiene deliberadamente en la extracción basada en descripciones con rutas de mejora documentadas, de modo que la base de código siga siendo fácil de razonar.

Conclusiones clave

Acotar a la escalera «primero la descripción» y dejar por escrito —en lugar de construir— los fallbacks de Whisper/headless mantuvo la v1 lista para entregar sin cerrar ninguna puerta. Mantener separados extract y save resultó ser la decisión de arquitectura clave: la base de datos solo contiene recetas confirmadas por una persona. La deduplicación con 409 a nivel de aplicación sobre la sourceUrl evitó un índice único y su riesgo de migración sobre los datos existentes, y el auto-reenvío async de Express 5 eliminó en silencio toda una clase de código repetitivo. El siguiente paso obvio es cambiar las cachés en memoria por Redis, ya marcado en el punto de llamada.

© 2026 Jonas Fink