04 / 05Caso de estudio
Network 3.0
Red social full-stack
Una API REST en Django y una aplicación en React y TypeScript: feed por cursor, reposts y citas, hashtags, menciones y notificaciones, desplegadas en Railway con demo pública.
- Mi papel
- Único desarrollador; diseño de producto, API, frontend, pruebas y despliegue
- Stack
- React · TypeScript · Vite · Tailwind CSS · TanStack Query · Django · Django REST Framework · PostgreSQL · Redis · Docker
- Estado
- Desplegado en Railway · demo pública
Alcance
- 132pruebas automatizadas, backend y frontend
- 37operaciones de API documentadas en OpenAPI
- 8tipos de aviso, cada uno con su destino
El reto
En una red social todo está conectado: un like cambia un contador, un perfil, una lista de personas y el aviso de otra persona, mientras el feed se mueve bajo quien lo lee.
- Lo que construí
- Reescribí desde cero mi proyecto de CS50W como API REST en Django y aplicación en React y TypeScript, y lo llevé a producción con Docker, integración continua y 132 pruebas.
- Decisión técnica clave
- Paginación por cursor para que el feed no se repita, contadores con subconsultas en vez de JOIN múltiples y las notificaciones como dominio propio.
Decisiones de diseño

- Problema
- Lo que ves en una red social depende de a quién sigues, y a quién seguir no se descubre solo.
- Decisión
- Publicar, conversar y descubrir personas conviven en una vista de tres columnas, sin salir del feed.

- Problema
- En el teléfono no caben tres columnas ni seis destinos de navegación.
- Decisión
- Una sola columna y una barra inferior de cuatro destinos; marcadores y ajustes pasan al menú de la cuenta.

- Problema
- Un spinner centrado no dice qué viene, y al llegar el contenido la página salta.
- Decisión
- Esqueletos con la geometría exacta de la tarjeta real; el hueco ya tiene su tamaño y nada se mueve.

- Problema
- Una pantalla que sólo dice «no hay nada» parece una función a medio terminar.
- Decisión
- Cada vacío explica qué irá ahí y ofrece la acción que lo llena, como buscar a quién seguir.

- Problema
- Ocho tipos de actividad en una sola lista se leen como un muro indiferenciado.
- Decisión
- Cada tipo lleva su icono, su color y su frase; las no leídas se marcan y cada fila lleva a su origen.

- Problema
- Un aviso de comentario que sólo abre la publicación obliga a buscar el comentario en el hilo.
- Decisión
- El enlace lleva al comentario exacto; la página baja hasta él y lo resalta con un anillo.

- Problema
- «¿Estás seguro?» no dice nada; se confirma sin saber qué se pierde.
- Decisión
- El diálogo nombra lo que se borra y, cuando no hay vuelta atrás, pide la contraseña.

- Problema
- Un modo oscuro que destella en blanco al cargar, o que ignora el del sistema, se siente roto.
- Decisión
- Claro, oscuro o el del sistema; el tema se aplica antes de que React monte, sin destello.
Sistema
Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.
Tres columnas: publicar, conversar y descubrir personas sin salir del feed, que se carga solo al llegar al final.
React 19 · IntersectionObserver
Respuestas a un solo nivel: basta para conversar sin hilos que se pierdan en profundidad.
React 19 · React Router 7
Pestañas Posts, Media y Likes; seguidores y seguidos son modales con dirección propia.
React 19 · React Router 7
Cada verbo con su icono, su color y su frase, y cada fila lleva a su origen exacto.
React 19 · lucide-react
Una sola caja para personas, texto y hashtags, con atajo /; escribir con # salta a las publicaciones.
React Router 7 · Rebote de 350 ms
Registrarse deja la sesión iniciada, y el acceso ofrece cuentas demo de un clic.
Zustand 5 · React 19
Una columna y una barra inferior de cuatro destinos; marcadores y ajustes, al menú de la cuenta.
Tailwind CSS 4 · Manifiesto instalable
Un like optimista se refleja a la vez en el feed, el detalle y el perfil, escrito en un solo sitio.
TanStack Query 5 · Zustand 5
Una sola promesa de refresco compartida: diez peticiones con el token caducado disparan un refresco, no diez.
Axios 1.20
Versionada bajo /api/v1/: 28 rutas y 37 operaciones, con el esquema OpenAPI generado del propio código.
Django REST Framework 3.17 · django-filter · django-cors-headers
Refresh rotado con lista negra y 10 intentos por minuto y por IP; cambiar la contraseña cierra las demás sesiones.
SimpleJWT 5.5 · Lista negra de tokens
Cursor por fecha e id: publicar algo nuevo no repite la página siguiente; los contadores son subconsultas, no JOIN.
CursorPagination de DRF · Subquery y Exists
Repost y cita son el mismo modelo; una restricción única condicional impide repostear dos veces lo mismo.
Django 6.0 · UniqueConstraint
Toda subida se valida, se rota por EXIF, pierde sus metadatos y se reescribe como WebP.
Pillow 12.3
Seguirse a uno mismo lo prohíbe la base de datos, no sólo la vista.
Django 6.0 · CheckConstraint
App propia con ocho verbos; al deshacer un like, un follow o un repost se borra su aviso.
Django 6.0
Se extraen al guardar; la tendencia cuenta los últimos siete días y se cachea cinco minutos.
Expresiones regulares · Caché de Django
Ocho modelos con índices escritos para las consultas que existen, como el de avisos no leídos.
PostgreSQL 17 · psycopg 3.3 · dj-database-url
Sólo tendencias y sugerencias: si cae, la aplicación se ralentiza, no se rompe.
Redis 8 · django-redis 7.0
Disco local por defecto; un bucket S3 compatible con sólo definir las variables.
django-storages 1.14
Además de lint, tipos y pruebas, valida migraciones, check --deploy y la construcción de las dos imágenes.
GitHub Actions · Ruff · ESLint · Vitest
Dos imágenes multi-stage que respetan $PORT: la misma corre en local y en Railway.
Docker · Docker Compose
Los dos servicios, configurados como código; el health-check sólo falla si cae la base de datos.
Railway · railway.json
Sirve la aplicación, aplica la CSP y las cabeceras de seguridad y devuelve index.html a las rutas del cliente.
nginx · Content-Security-Policy
WSGI: no hay nada en tiempo real que justifique ASGI.
Gunicorn 26 · WhiteNoise 6.12
El esquema sale del código: la documentación no puede quedarse vieja.
drf-spectacular 0.29 · Swagger UI · ReDoc
Integrado pero apagado salvo que exista SENTRY_DSN; no obliga a depender de un servicio externo.
Sentry SDK 2.70
Cliente
Feed
Tres columnas: publicar, conversar y descubrir personas sin salir del feed, que se carga solo al llegar al final.
- Tecnologías
- React 19 · IntersectionObserver
- Entrega a
- Feed por cursor
Tecnologías
55 herramientas en 10 áreas. En el sistema, cada módulo dice las suyas.
Lenguajes
- Python 3.13
- TypeScript 6.0
- JavaScript
- SQL
- HTML y CSS
Frontend
- React 19.2
- Vite 8.0
- React Router 7.18
- TanStack Query 5.101
- Zustand 5.0
- Axios 1.20
Interfaz
- Tailwind CSS 4.3
- lucide-react 1.18
- Inter (Fontsource) 5.3
- clsx 2.1
Backend
- Django 6.0
- Django REST Framework 3.17
- django-filter 25.2
- django-cors-headers 4.9
- Pillow 12.3
- python-dotenv 1.2
Autenticación
- SimpleJWT 5.5
- Lista negra de tokens
- Acceso con usuario o email
- Validadores de contraseña de Django
Contrato de API
- drf-spectacular 0.29
- OpenAPI 3.0
- Swagger UI
- ReDoc
Datos y caché
- PostgreSQL 17
- SQLite 3
- psycopg 3.3
- dj-database-url 3.1
- Redis 8
- django-redis 7.0
- django-storages 1.14
Infraestructura
- Docker
- Docker Compose
- nginx
- Gunicorn 26
- WhiteNoise 6.12
- Railway
- Sentry SDK 2.70
Calidad y CI
- Test runner de Django
- Vitest 5.0
- Testing Library 16.3
- jsdom 30.1
- Ruff 0.16
- ESLint 10.5
- typescript-eslint 8.61
- Prettier 3.9
- GitHub Actions
Herramientas
- Make
- EditorConfig
- Keep a Changelog
Resultados verificables
- Desplegado en Railway con demo pública; la web y la API corren como servicios separados sobre PostgreSQL y Redis
- Reescritura completa del proyecto 4 de CS50W, de un monolito con plantillas a una API REST versionada y una aplicación en React
- Feed paginado por cursor, con contadores resueltos por subconsultas y no por JOIN múltiples
- JWT con rotación de refresh y lista negra; cambiar la contraseña cierra las demás sesiones
- Reposts y citas, hashtags con tendencias, menciones, marcadores y notificaciones de ocho tipos
- 132 pruebas automatizadas y una CI que también valida migraciones, check --deploy y las dos imágenes Docker
Recorrido por módulos
62 pantallas en 9 módulos.
Feed y publicaciones11 pantallas

Publicar, conversar y descubrir personas en una sola vista de tres columnas.

El detalle reúne la publicación y toda su conversación.

El aviso lleva al comentario exacto y lo resalta.

Responder ocurre bajo el comentario, sin salir del hilo.

La imagen se ve antes de enviarla; el servidor la reescribe como WebP.

Following reúne lo de quien sigues y lo que publicas tú.

Mientras carga, el esqueleto ocupa el sitio exacto de la tarjeta.

Editar y borrar sólo aparecen sobre lo propio.

Al citar se ve la original completa, imagen incluida.

El contador de likes se abre como lista de personas.

El diálogo nombra lo que se pierde, no sólo la acción.
Perfiles y relaciones7 pantallas

El perfil une identidad, relaciones y lo publicado.

«Follows you» resuelve la relación de un vistazo.

La pestaña Media recorre el perfil por imagen.

Lo que alguien marca también dice quién es.

Editar el perfil entero sin salir de él, con contador en cada campo.

Seguidores y seguidos son modales con dirección propia: se pueden enlazar.

El grafo se recorre en las dos direcciones desde el perfil.
Notificaciones2 pantallas
Búsqueda y marcadores5 pantallas
Acceso y cuenta6 pantallas

La demo pública ofrece cuentas de prueba de un clic.

El error aparece en el propio formulario, sin recargar.

Crear la cuenta deja la sesión iniciada: una pantalla, no dos.

El menú de la cuenta reúne lo que no está en la navegación.

Cambiar la contraseña cierra la sesión en los demás dispositivos.

Borrar la cuenta enumera lo que se pierde y pide la contraseña.
Estados vacíos y errores5 pantallas
Modo oscuro6 pantallas

En oscuro, los bordes hacen el trabajo de las sombras.

La jerarquía del hilo se mantiene al cambiar de tema.

La portada en degradado también funciona sobre fondo oscuro.

Cada color de aviso tiene su variante oscura.

Claro, oscuro o el del sistema, aplicado antes de pintar.

El acceso y sus cuentas demo, también en oscuro.
API documentada1 pantalla
En el teléfono19 pantallas

Una columna y una barra inferior con cuatro destinos.

Publicar con imagen funciona igual en el teléfono.

El hilo completo se lee en una columna.

Responder sigue ocurriendo bajo el comentario.

El perfil apila portada, datos y pestañas.

La rejilla de medios pasa de tres columnas a dos.

Las listas de personas suben como hoja inferior.

El botón de seguir sigue a mano en el teléfono.

Cada tipo de aviso sigue distinguiéndose en pantalla estrecha.

Los marcadores se leen igual en una columna.

Sin consulta, la búsqueda propone temas.

La búsqueda por hashtag, igual en el teléfono.

Marcadores y ajustes viven en el menú de la cuenta.

Los ajustes se apilan en el mismo orden.

El vacío conserva su explicación en pantalla estrecha.

Modo oscuro y teléfono a la vez.

El perfil mantiene la jerarquía en oscuro.

Los colores de cada aviso aguantan el oscuro y la pantalla estrecha.

Las cuentas demo caben también en el teléfono.
8 min de lectura
El caso completo
Contexto
Network nació como el proyecto 4 de CS50W, el curso de desarrollo web de Harvard: una red social pequeña hecha con plantillas de Django y JavaScript sin framework. Mi primera entrega ya iba más allá de lo que pedía el enunciado —comentarios con respuestas, imágenes, borrado y búsqueda—, pero seguía siendo un monolito de curso.
Network 3.0 es una reescritura completa, no un retoque. Borré el monolito y en su lugar hay dos servicios independientes que sólo comparten un contrato HTTP: una API REST en Django y una aplicación de una sola página en React y TypeScript. La versión actual, la 3.1.0, está desplegada en Railway con una demo pública. Fueron un par de semanas de trabajo, en solitario.
Problema
Lo difícil de una red social pequeña no son las pantallas: es que todo está conectado con todo y todo cambia mientras lo miras. Un like toca el contador de la tarjeta, la pestaña Likes de un perfil y la lista de quién reaccionó, y crea un aviso para otra persona. El feed se mueve mientras alguien lo lee, cada tarjeta necesita cinco datos agregados y cualquier imagen que sube un usuario es, por definición, contenido en el que no se puede confiar.
Mi función
Lo hice solo: el diseño del producto y de la interfaz, la API, el frontend, las pruebas, los contenedores, la integración continua, el despliegue y la documentación.
Lo que pedía CS50W y lo que añadí
El enunciado pedía siete cosas: publicar texto, ver todas las publicaciones, un perfil con seguidores y botón de seguir, la página de las personas seguidas, paginación de diez en diez con botones, editar sin recargar y dar like sin recargar.
La versión 3 añade, encima:
- Arquitectura. API REST versionada y aplicación de una sola página, desplegables por separado; JWT con rotación y lista negra en lugar de la sesión; paginación por cursor, y un esquema OpenAPI 3 con Swagger y ReDoc.
- Producto. Reposts y citas, marcadores privados, hashtags con tendencias de la semana, menciones que notifican, notificaciones de ocho tipos, perfiles con portada y pestañas Posts, Media y Likes, sugerencias de a quién seguir, tema claro, oscuro o del sistema, cambio de contraseña y borrado de cuenta.
- Oficio. 132 pruebas automatizadas donde antes no había ninguna, integración continua, imágenes Docker, procesado de imágenes en el servidor y cabeceras de seguridad.
Restricciones
- Nada obligatorio para desarrollar. Arranca con SQLite y caché en memoria; PostgreSQL y Redis se activan con variables de entorno. Todo lo que usa la caché tiene que funcionar sin ella.
- Sin almacenamiento de objetos garantizado. Los archivos subidos van al disco salvo que se configure un bucket compatible con S3.
- Una sola persona y ningún usuario al que preguntar. Todas las decisiones de producto se tomaron sin datos de uso.
Cómo lo construí
Primero borré el monolito, en su propio commit; después vinieron el backend, el frontend y, al final, los contenedores. Cuando volví para llevarlo a producción, el orden fue este: primero la integración continua, luego las dependencias con avisos de seguridad y sólo después las funciones nuevas, el rediseño y la documentación.
El backend son cuatro apps de Django separadas por dominio y no por capa
técnica: core, users, posts y notifications. En el frontend, TanStack
Query gobierna todo lo que vive en el servidor y Zustand sólo lo que es del
cliente: la sesión, el tema y los avisos. Cada página se carga por separado.
Decisiones de arquitectura
- Paginación por cursor en los timelines. Ordenada por fecha e id, con el id como desempate: publicar algo nuevo no desplaza la página siguiente ni repite lo ya leído. Las listas acotadas, como personas o comentarios, siguen por número de página. La página sigue siendo de diez, como pedía CS50W, pero se carga sola al llegar al final.
- Contadores sin JOIN múltiples. Cada tarjeta trae likes, comentarios y
reposts como subconsultas independientes, y «¿le di like?», «¿lo reposteé?» y
«¿lo guardé?» como
EXISTS. La publicación citada recibe las mismas anotaciones. - JWT con rotación y lista negra. Cada uso del refresh emite uno nuevo e invalida el anterior, y cambiar la contraseña invalida todos los demás. En el cliente, las peticiones que fallan a la vez comparten un solo refresco.
- Repost y cita, un solo modelo. Una restricción única condicional impide repostear dos veces lo mismo sin impedir citarlo varias veces, y la base de datos prohíbe seguirse a uno mismo.
- Las notificaciones, un dominio propio. Una app de Django con sus ocho verbos, sus endpoints y un índice compuesto pensado para las dos consultas que existen: la lista y el contador de no leídas.
- Redis opcional y no crítico. Sólo guarda las tendencias y las sugerencias, con tiempos de espera cortos: si cae, la aplicación se ralentiza, no se rompe. El health-check informa de la caché, pero sólo marca el servicio como caído si falla la base de datos.
Seguridad e imágenes
- Cada imagen subida se valida con Pillow, se rota según su EXIF, pierde sus metadatos, se reescala según su uso y se reescribe como WebP, con un tope de 40 megapíxeles contra las bombas de descompresión.
- El registro, el acceso y el cambio de contraseña tienen su propio límite: diez peticiones por minuto y por IP.
- En producción, Django pone HSTS, cookies seguras y las cabeceras de seguridad, y nginx pone la política de seguridad de contenidos. La aplicación se niega a arrancar con la clave secreta de desarrollo.
Desafíos
- Que el feed no se repita al paginar: cursor con desempate por id.
- Que dos reposts simultáneos no rompan nada: la restricción única en la base de datos y el error de integridad capturado.
- Que un token caducado no dispare una tormenta de refrescos: una sola promesa compartida en el cliente HTTP.
- Que el tema no destelle al cargar: un script en
index.htmlaplica el tema antes de que React monte. - Que los avisos no se dupliquen ni queden huérfanos: se crean una sola vez y se borran cuando se deshace el like, el follow o el repost.
- Que los datos de demostración sean reproducibles: generadores con semilla fija.
Lo que enseñó el despliegue
El primer despliegue en Railway sacó a la luz lo que el entorno local no podía enseñar: los permisos del volumen de archivos, el directorio personal del usuario sin privilegios del contenedor, una migración que no coincidía con el modelo y la necesidad de sembrar la demo al arrancar. Los cuatro se resolvieron en un pull request propio, y la integración continua comprueba que no falte ninguna migración.
Diseño y UX
Interfaz propia con Tailwind CSS 4 y la tipografía Inter servida desde el propio sitio, sin peticiones a terceros. Todos los estados están diseñados: los de carga replican la tarjeta real, los vacíos explican qué irá ahí y ofrecen la acción que los llena, y los diálogos destructivos nombran lo que se pierde. El tema oscuro no es un filtro invertido y respeta el del sistema, y en el teléfono la navegación baja a una barra de cuatro destinos.
Lo que no tiene
Para que nadie lo dé por supuesto: no hay tiempo real —ni WebSockets ni sondeo; los datos se refrescan al volver a pedirlos—, no hay cola de tareas y no hay service worker. El manifiesto hace la web instalable, pero no funciona sin conexión.
Sobre las capturas
Las capturas salen de una instancia local sembrada con un elenco inventado —diez personas con avatares de iniciales generados por código— e imágenes abstractas generadas con Pillow. Los nombres y las cifras que aparecen, como likes o seguidores, no son métricas de nada. La demo pública usa otras cuentas de ejemplo, así que los nombres no coinciden. Las pantallas con algún defecto visible se dejaron fuera.
Probar la demo
La demo está en
web-production-9475c.up.railway.app.
Entra con ada, grace, linus, margaret, alan, katherine, tim o
hedy; la contraseña de todas es network123. La pantalla de acceso ofrece
cuatro de ellas con un clic, y también puedes crear tu propia cuenta. La
documentación de la API está en
Swagger.
Aprendizajes
Reescribir fue más limpio que refactorizar: al borrar primero el monolito, la API se diseñó sin heredar la forma de las plantillas. Y el despliegue enseñó lo que el desarrollo no podía: permisos, usuarios del contenedor y migraciones sólo fallan de verdad en producción.
Estado actual
Terminado y en funcionamiento en la versión 3.1.0: desplegado de forma permanente en Railway sobre PostgreSQL y Redis, con el repositorio público bajo licencia GPL-3.0, la integración continua en verde y 132 pruebas automatizadas —98 de backend y 34 de frontend— que pasan. No tiene usuarios reales, así que no publico cifras de uso: es un proyecto para demostrar cómo construyo.











