05 / 05Caso de estudio
Delicaté 4.0
E-commerce y sitio de marca
Jabones artesanales de República Dominicana, en línea con dominio propio: catálogo administrable en Django, carrito sin cuenta que se mantiene al día y pedidos que se cierran por WhatsApp.
- Mi papel
- Levantamiento de necesidades, UX/UI, desarrollo full-stack y despliegue
- Stack
- React 19 · Vite 8 · Django 5.2 · Django REST Framework · PostgreSQL · Docker · Railway
- Estado
- En línea con dominio propio · salida comercial en validación
Alcance
- 18pruebas automatizadas, en CI en cada push
- 0cuentas necesarias para comprar
- 68 KBde JavaScript en producción, con gzip
El reto
El negocio necesitaba presentar su catálogo con una identidad propia y recibir pedidos bien formados sin adoptar pagos en línea ni una operación logística que no existe detrás.
- Lo que construí
- Rediseñé y reconstruí la tienda completa —API de catálogo en Django REST, administración del inventario y una tienda responsive en React— y la llevé a producción en Railway con dominio propio, integración continua y 18 pruebas.
- Decisión técnica clave
- Retiré el registro de clientes, el carrito en servidor y el checkout de la versión anterior, y convertí el carrito en un pedido estructurado por WhatsApp: el mismo canal donde el negocio ya confirma, cobra y entrega.
Decisiones de diseño

- Problema
- El precio de un jabón artesanal se sostiene con la marca, no con un listado.
- Decisión
- La portada presenta la marca antes que la tienda, con una sola acción principal.

- Problema
- Antes de comprar, el cliente pregunta por beneficio, tipo de piel e ingredientes.
- Decisión
- La ficha lo responde en un diálogo nativo, sin salir del catálogo.

- Problema
- Un checkout que no puede cobrar es una promesa rota.
- Decisión
- El carrito arma el pedido, aclara que no cobra y lo cierra en WhatsApp.

- Problema
- Un carrito guardado días antes puede enviar precios viejos o cantidades sin existencias.
- Decisión
- Al volver, el carrito se actualiza con el catálogo real y avisa de lo que cambió.

- Problema
- En una pantalla táctil no hay hover que descubra el botón de compra.
- Decisión
- En táctil «Agregar» queda siempre visible y los filtros se desplazan en una fila.

- Problema
- Si un jabón agotado desaparece del catálogo, quien lo buscaba no sabe si volverá.
- Decisión
- Sigue a la vista con «Agotado» y la compra desactivada, igual en la tarjeta que en la ficha.

- Problema
- Si la API falla, mostrar productos inventados engaña al comprador.
- Decisión
- Producción muestra un error con reintento; el catálogo de demostración sólo existe en desarrollo.

- Problema
- El negocio tenía que cambiar su catálogo sin tocar código ni recompilar.
- Decisión
- Precio, existencias, destacado y visibilidad se editan en la propia lista del administrador.
Sistema
Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.
La marca antes que la tienda: una sola acción principal lleva a la colección.
React 19 · CSS propio
Se lee entero de la API, siguiendo todas sus páginas, y se filtra en el navegador; los filtros declaran su estado con aria-pressed.
fetch con AbortController · React 19
Un diálogo nativo responde beneficio, piel, ingredientes y existencias sin salir del catálogo.
<dialog> nativo · React 19
Sin cuenta. Al volver se cruza con el catálogo: toma el precio actual, limita cada cantidad a las existencias y avisa de lo que cambió.
useReducer · Hook propio useCart
Pensada para el teléfono: en táctil «Agregar» siempre está visible y el carrito ocupa la pantalla entera.
@media (hover: none) · CSS propio
El formulario no guarda nada en el servidor: convierte la consulta en un mensaje de WhatsApp.
React 19
Pública y de sólo lectura: listado paginado con filtros y ficha por slug. El catálogo sólo se escribe desde el administrador.
Django REST Framework 3.17 · Paginación hasta 100
Precio, existencias, destacado y visibilidad se editan en la propia lista; las fotos se validan (JPG, PNG o WebP, hasta 5 MB).
Django Admin · Pillow 12.3
CSP y demás cabeceras en toda respuesta, HSTS de un año y límites de frecuencia que cuentan la IP real detrás del proxy.
Middleware propio · Throttling de DRF
El carrito vive en el navegador; el servidor nunca guarda pedidos ni datos de compradores.
localStorage
Cuatro modelos, frente a ocho en 2024: productos, mensajes, suscripciones y el usuario del equipo.
PostgreSQL · psycopg 3.3 · dj-database-url 3.1
Las fotos subidas viven en un volumen persistente; Django no arranca si su ruta es relativa.
Volumen de Railway · Caché de 7 días
En cada push: check de Django, migraciones pendientes, las 18 pruebas, lint y build.
GitHub Actions
Imagen multietapa: Node construye la tienda y Python la sirve, con DEBUG apagado por defecto.
node:24-alpine · python:3.13-slim
Migra antes de publicar y sólo da tráfico a la versión nueva si el healthcheck, que consulta la base, responde 200.
Railway · railway.json
Un solo proceso sirve tienda, API, administración y fotos desde el mismo origen: sin CORS.
Gunicorn 23 · WhiteNoise 6.12
El pedido sale como mensaje prearmado por wa.me, en el canal donde el negocio ya confirma, cobra y entrega.
Enlace wa.me
Al compartir el enlace, una imagen de 1200×630 y la URL canónica salen de la configuración del build.
Open Graph · Twitter Card
Cliente
Portada
La marca antes que la tienda: una sola acción principal lleva a la colección.
- Tecnologías
- React 19 · CSS propio
- Entrega a
- Catálogo · Vista previa
Tecnologías
43 herramientas en 10 áreas. En el sistema, cada módulo dice las suyas.
Lenguajes
- Python 3.13
- JavaScript (JSX)
- CSS
- HTML
Frontend
- React 19.2
- Vite 8.2
- Rolldown 1.2
- @vitejs/plugin-react 6.0
Interfaz
- CSS propio sin framework
- Lightning CSS 1.33
- Tipografía del sistema
- 8 iconos SVG en línea
- <dialog> nativo
Estado en el navegador
- useReducer y un hook propio
- localStorage
- fetch con AbortController
Backend
- Django 5.2 LTS
- Django REST Framework 3.17
- Django Admin
- django-cors-headers 4.9
- Pillow 12.3
- python-dotenv 1.2
Datos
- PostgreSQL
- SQLite
- psycopg 3.3
- dj-database-url 3.1
Servidor y despliegue
- Docker multietapa
- Gunicorn 23
- WhiteNoise 6.12
- Railway
- Volumen persistente
- Dominio propio con TLS
Integraciones
- Enlace wa.me de WhatsApp
- Open Graph y Twitter Card
Calidad y CI
- Test runner de Django
- APITestCase de DRF
- ESLint 10.8
- eslint-plugin-react-hooks 7.1
- GitHub Actions
Herramientas
- Node.js 24
- npm
- Git
- Railway CLI
Resultados verificables
- En línea en delicate.jonasjavier.dev como un solo contenedor Docker en Railway, con PostgreSQL y un volumen para las fotos
- Catálogo administrable desde Django, sin tocar código ni recompilar la tienda
- Carrito sin cuenta que se guarda en el navegador y, al volver, se actualiza con precios y existencias reales
- Pedido completo, con cantidades y total estimado, prearmado para WhatsApp; el sitio no cobra
- Migraciones antes de publicar, healthcheck con base de datos, CSP y límites de frecuencia
- 18 pruebas automatizadas y una CI que comprueba migraciones, lint y build en cada push
Recorrido por módulos
30 pantallas en 7 módulos.
Portada y marca3 pantallas
Catálogo y ficha6 pantallas

El catálogo se carga desde Django y se filtra al instante.

Cada tarjeta resume precio, tipo de piel y peso antes de abrir la ficha.

En escritorio, «Agregar» aparece al pasar el ratón sobre la foto.

La ficha responde beneficio, piel, ingredientes y existencias sin salir del catálogo.

Un producto sin existencias sigue a la vista, pero no se puede agregar.

La ficha avisa que está agotado y desactiva la compra.
Carrito y pedido3 pantallas
Cómo se pide3 pantallas
Estados del catálogo2 pantallas
Administración2 pantallas
En el teléfono11 pantallas

El pedido sale por WhatsApp desde el mismo teléfono.

El aviso explica el cambio antes de enviar el pedido.

En táctil, «Agregar» siempre está visible y los filtros se deslizan.

En el teléfono la ficha se apila y se desplaza dentro del diálogo.

La acción de compra cierra la ficha a todo el ancho.

El titular y la acción principal quedan al alcance del pulgar.

El menú se cierra con Escape o al elegir un destino.

La historia se lee de arriba abajo.

Los tres pasos para pedir, apilados.

El error se entiende y se resuelve igual en el teléfono.

El estado vacío también está resuelto en el teléfono.
9 min de lectura
El caso completo
Contexto
Delicaté es una marca de jabones artesanales hechos en pequeñas tandas en República Dominicana, y el sitio lo hice para ella. Vende por conversación: el cliente pregunta, elige y coordina la entrega por WhatsApp. El sitio tenía que presentar la marca y el catálogo con una experiencia propia y llevar al cliente a esa conversación con el pedido ya armado.
El repositorio conserva dos generaciones. La de 2024 era una tienda convencional: registro e inicio de sesión con Google, carrito guardado en el servidor, reseñas, historial de pedidos y blog. La 4.0, reconstruida en agosto de 2026 y en producción desde septiembre, parte de preguntar qué necesitaba de verdad el negocio.
Problema
Una tienda con cuentas, checkout, pagos e inventario promete una operación que este negocio no tiene. Pedir registro para comprar un jabón añade fricción sin aportar nada, y un checkout que no puede cobrar es una promesa rota. Lo que sí hacía falta era mostrar bien los productos, ayudar a elegir y recibir un pedido claro, porque la confirmación, el cobro y la entrega seguirían ocurriendo por WhatsApp.
Mi función
Levanté las necesidades con el negocio, definí la arquitectura de información y el recorrido, diseñé la interfaz y desarrollé la tienda, la API y la administración. También la desplegué con su dominio y escribí la documentación técnica, la lista de salida a producción y la especificación de una futura línea de jabones personalizados.
Usuarios
El comprador explora la colección, filtra por categoría, abre la ficha de cada jabón y arma su pedido sin crear una cuenta. El equipo del negocio mantiene productos, precios, existencias, fotografías y destacados desde el administrador de Django, y recibe en WhatsApp un mensaje legible con cada producto, su cantidad y el total.
Restricciones
- Sin pagos, sin logística y sin prometer existencias en tiempo real. Por eso el total del carrito es «estimado».
- Un catálogo que cambia sin recompilar nada. La tienda lo lee de la API en cada visita.
- Primero el teléfono. Es donde compra el público de la marca y donde vive WhatsApp.
- Ningún dato de compradores. La tienda no guarda perfiles, direcciones ni pedidos.
Cómo lo construí
Partí del recorrido comercial real —descubrir la marca, encontrar un producto, entender sus beneficios, armar el pedido y abrir la conversación— y retiré todo lo que no servía a ese recorrido. La reconstrucción eliminó los modelos de la versión anterior con migraciones explícitas (carrito, líneas de carrito, reseñas y perfiles de usuario) y añadió una migración de datos para dar a cada producto un slug único. Pasó de ocho modelos a cuatro y de treinta dependencias de ejecución en el frontend a dos: React y React DOM.
El backend son tres aplicaciones de Django: shop (catálogo, API y
administración), contact (mensajes y suscripciones) y accounts (el usuario
del equipo, que entra con su correo). El frontend son seis componentes de
React, un hook para el carrito y CSS propio, sin librerías de estado ni de
componentes. En septiembre de 2026 la preparé para producción: endurecí Django,
hice que el carrito se sincronizara con el catálogo y la desplegué con su
dominio.
Decisiones de arquitectura
- Sin cuentas de cliente. El usuario personalizado de Django existe sólo para el equipo; el comprador nunca se registra.
- API pública de sólo lectura. El catálogo se consulta por la API y se escribe únicamente desde el administrador. La tienda pide todas las páginas del listado, así que un catálogo más grande no deja productos fuera.
- Un carrito que no miente. Vive en
localStoragey sobrevive a la recarga. Al llegar el catálogo, cada línea toma el precio actual, su cantidad se limita a las existencias y los productos retirados salen del carrito; si algo cambió, el cajón lo dice. - Un solo contenedor en producción. Django sirve la API, el administrador, las fotos y el build de React desde el mismo dominio, con WhiteNoise: no hace falta CORS. En desarrollo, Vite hace de proxy hacia Django.
- La demostración nunca sustituye a los datos reales. El catálogo de respaldo sólo aparece en desarrollo o si se activa explícitamente, con un aviso visible. En producción, si la API falla, el cliente ve un error con reintento, no productos inventados.
El pedido por WhatsApp
«Finalizar por WhatsApp» abre un enlace wa.me con el pedido ya escrito. No
hay integración con la API de WhatsApp ni nada que pagar por mensaje: el
servidor no registra el pedido y la conversación sigue en el teléfono del
negocio. Éste es el mensaje que genera el carrito de las capturas:
¡Hola, Delicaté!
Quiero realizar este pedido:
• 2 × Avena Calma — RD$700
• 1 × Corazón de Lavanda — RD$425
• 1 × Café Despierto — RD$450
Total estimado: RD$1,575
Mi nombre es:
Prefiero: entrega / recoger
¿Me confirman disponibilidad y forma de entrega? Gracias.
El saludo llevaba un emoji que la página web de WhatsApp convertía en «�» al redirigir el enlace; ahora va sin él. El formulario de contacto hace lo mismo con el nombre y la consulta: tampoco guarda nada en el servidor.
Producción y seguridad
- Publicar sólo lo que funciona. Cada push a
mainconstruye la imagen, aplica las migraciones antes de publicar y sólo da tráfico a la versión nueva si/api/health/, que consulta la base de datos, responde 200. Si algo falla, sigue la anterior. - Cabeceras en todas las respuestas, también en las que sirve WhiteNoise: política de seguridad de contenidos, Permissions-Policy y X-Frame-Options; HTTPS obligatorio, HSTS de un año y cookies seguras.
- Límites de frecuencia que cuentan la IP real detrás del proxy: mil peticiones por hora y visitante, y topes propios para los endpoints de escritura.
- Arranque seguro. Con
DEBUGapagado, Django se niega a arrancar con la clave de desarrollo o con una ruta de fotos relativa. - Fotos validadas por formato (JPG, PNG, WebP) y peso (5 MB), y servidas con caché de siete días.
Desafíos
El principal fue construir una experiencia de compra convincente sin fingir una infraestructura de pagos o logística. El segundo, mantener coherentes el catálogo administrado, los filtros, la ficha y un carrito que sobrevive entre visitas cuando cambian los precios o las existencias. Y el tercero fue reducir: pasar de una tienda sobredimensionada a un producto de unas 2.600 líneas que se puede mantener.
Lo que enseñó el despliegue
- Railway no aplicó
railway.jsonal servicio creado desde la línea de comandos, y la primera versión salió sin migraciones. La configuración quedó fijada en el propio servicio. - Git Bash convirtió la ruta
/data/mediaen una ruta de Windows y las fotos se perdieron en un redespliegue. Lo corregí y añadí la validación que hoy impide arrancar con una ruta relativa. - Al añadir el dominio propio, Railway dejó de exponer la dirección
*.up.railway.app; ahora los dos hosts están autorizados de forma explícita.
Diseño y UX
La dirección visual usa una paleta cálida de crema, verde bosque y terracota, una serif editorial para los titulares y mucho aire para comunicar artesanía, sin descargar fuentes web. La página cuenta una historia en orden: portada, valores, colección, historia de la marca, cómo pedir en tres pasos, preguntas frecuentes y contacto. Al agregar un producto, el carrito se abre para confirmar el gesto.
Los estados están resueltos: carga, error con reintento, categoría vacía y
producto agotado. En accesibilidad, la ficha es un <dialog> nativo, el
carrito atrapa y devuelve el foco, el menú móvil se cierra con Escape, hay
enlace para saltar al contenido, los filtros declaran su estado con
aria-pressed y se respeta prefers-reduced-motion.
Lo que no tiene
Para que nadie lo dé por supuesto: no hay pagos, ni búsqueda en la tienda
(sólo la API acepta ?search=), ni variantes de producto, ni más de una foto
por producto, ni una página propia con URL para cada jabón: la ficha es un
diálogo. Tampoco hay panel de pedidos, porque los pedidos no pasan por el
servidor. La API conserva dos endpoints de contacto y boletín, con límite de
frecuencia, que ninguna pantalla de la tienda usa hoy.
Sobre las capturas
Las capturas se hicieron el 25 de septiembre de 2026 sobre el build de producción de la tienda, servido en local con una base de datos desechable y los diez productos del catálogo. Algunos estados se provocaron a propósito para poder enseñarlos: el producto agotado, el catálogo que tarda o falla, la categoría vacía y el carrito guardado con precios viejos. Lo que se ve en cada uno es el código de producción. Las fotografías de producto son las del proyecto original; las de ambiente de la portada y la historia se generaron para esta versión. Las pantallas con algún defecto visible se dejaron fuera.
Aprendizajes
Un buen producto no necesita fingir más infraestructura de la que el negocio puede operar. Quitar cuentas, carrito en servidor y checkout hizo el sistema más pequeño, más seguro y más fácil de adoptar, y convirtió una limitación real en un flujo coherente. La versión que más aporta no fue la que más hacía, sino la que mejor se ajustaba a cómo vende el negocio.
Estado actual
En línea en delicate.jonasjavier.dev desde el 25 de septiembre de 2026: un servicio Docker en Railway con PostgreSQL, un volumen para las fotos y dominio propio con HTTPS. El repositorio es público para evaluación, bajo licencia propietaria, con la integración continua en verde y 18 pruebas automatizadas que pasan. Antes de anunciarlo, quedan las validaciones que dependen del negocio y están en su lista de salida: políticas de entrega y privacidad, un pedido completo probado en Android e iPhone, copias de seguridad y monitoreo. No publico ventas ni visitas porque no las mido.
La siguiente línea del producto ya está especificada: un configurador guiado de jabón personalizado cuyo resultado es una solicitud por WhatsApp, no una compra automática. Todavía no forma parte del sistema.








