/dev: ¿Cómo está hecho medici?
Con más de 35 000 líneas de código y después de reescribir gran parte de la aplicación, es hora de hablar de un tema que muchos (nadie) estaban esperando: ¿cómo está hecho medici?
Este post es un deep-dive tecnológico con muchas abreviaciones y palabras técnicas. Hecha la advertencia, podemos empezar.
Como única persona atrás de medici, me puedo dar rienda suelta a la hora de elegir qué cosas usar para implementarlo. medici es la colección de lenguajes, patrones y tecnologías más divertidas de usar (para mí) y cutting-edge (hasta cierto punto) al día de hoy.
«¿“Escatimar”? No me suena esa librería» (yo, probablemente).
Frontend
El frontend (la interfaz) es una aplicación web escrita en TypeScript y hecha en React con Next.js (con el App Router).
Next fue una incorporación reciente. Hasta hace poco, medici era una SPA, empaquetada con una configuración custom de webpack “inspirada” en la que usa CRA, con la que venía trabajando en varios proyectos anteriores; pero como esta es una aplicación que se beneficia de tener buen SEO y algunas de las últimas funcionalidades de React solo están disponibles con SSR (como los RSCs), decidí embarcarme en el rewrite. La migración a Next me llevó alrededor de 1 mes y no fue trivial, pero en retrospectiva, valió la pena. Aparte de la gran mejora en el SEO, tiene un montón de funcionalidades que con mi configuración de webpack eran imposibles. Aunque no todo fue positivo: por ejemplo, trabajar con las varias capas de caché de Next es complicado y es fácil cometer errores usándolas.
Para los estilos uso Panda, también una incorporación reciente y la revelación del 2023. Hacía tiempo estaba buscando una librería de CSS en JS atómico y generado en tiempo de compilación, y Panda tenía todo lo que quería. Gracias a Panda, implementar la funcionalidad del cambio de apariencia (clara/oscura) fueron 2 patadas. La migración del viejo Emotion fue fácil.
Como un buen fan de GraphQL, uso Relay para comunicarme con el backend. La cantidad de tiempo de desarrollo que me han ahorrado no tiene nombre.
No uso scripts de analíticas ni trackers. Como usuario, no me gusta que me trackeen, y como desarrollador, enlentecen la página y los datos que me dan tampoco me influyen en las decisiones que tomo.
El frontend está deployado en el edge en Cloudflare Pages y todas las imágenes (de preguntas, materias, avatares, etc.) están en un bucket de Cloudflare R2.
Backend
El backend (el servidor) es una API GraphQL escrita en Rust con Axum y async-graphql.
Rust es la estrella del backend, una de las razones por la que medici tiene tan pocos bugs y,
al ser uno de los lenguajes más rápidos, un solo EC2 t4g.nano
(2 vCPUs, 0.5 GiB de RAM) me alcanza y
sobra para abastecer a toda la FMED, incluso en los días pico antes de los parciales
(según mis estimaciones; no lo probé todavía).
El backend usa Docker y está deployado en ECS.
Actualmente está corriendo en un EC2 t3.micro
con un load balancer de
ELB adelante y
una distribución de CloudFront
como CDN, que baja el costo del
ingreso y egreso de datos y comprime las respuestas del servidor.
La base de datos es PostgreSQL, alojada en
RDS y corriendo en un servidor db.t4g.micro
.
Como no me gustan los ORMs
(y la aplicación es relativamente chica),
uso SQLx para comunicarme con la base de datos;
hay un poco de repetición en las queries, sobre todo en las más básicas
tipo CRUD, pero en mi opinión, el rendimiento y
la flexibilidad de escribir SQL directo
hace que valga la pena.
También uso mínimamente DynamoDB para guardar metadata de la aplicación, como los cambios del estado de salud (cuándo se cayó, cuánto tiempo estuvo caída, etc.).
Las tareas periódicas (mantenimiento de la base de datos, chequeador de salud de la aplicación) se hacen con Lambdas escritas en Rust y Go e iniciadas por eventos de EventBridge Scheduler. Las 2 son código libre: ticker manda «ticks» cada X tiempo al servidor para que inicie las tareas de mantenimiento y watchman chequea la salud. Go sigue siendo el lenguaje más sobrevalorado que hay, en mi humilde opinión 😂.
Para el caché y las funcionalidades en tiempo real (por ejemplo, las pruebas grupales)
uso Redis en ElastiCache,
en un servidor cache.t3.micro
.
Las funcionalidades en tiempo real están hechas con GraphQL subscriptions y usan Redis
como pub/sub y WebSockets en el frontend.
Todo lo de AWS está en la región sa-east-1
(São Paulo, la más cerca de Uruguay) para tener la menor latencia posible.
Base de datos de preguntas
Trabajar con la data de parciales y exámenes es, sorpresivamente, uno de los aspectos más complejos de medici. Es una colección de datos, la mayor parte del tiempo, estáticos, actualmente en el órden de las decenas de MB que crece constantemente. La pregunta de dónde y cómo guardar esta información no es trivial. Desde el inicio, sabía que la solución tenía que cumplir con una serie de requisitos; tenía que:
- ser fácilmente editable para corregir errores, agregar nuevas preguntas, nuevas materias, etc.;
- tener alguna forma de validar y formatear los datos (validar que todas las preguntas tengan una (y solo una) respuesta correcta, eliminar preguntas repetidas, etc.);
- ser resiliente y foolproof: si estaban en una base de datos y por alguna razón la tiraba sin querer (ups 😅), no podía perder los datos para siempre;
- tener un paper trail de ediciones y la capacidad de revertir a versiones anteriores;
- tener la posibilidad de hacerla open source y editable por el público en general.
Consideré varias opciones, pero la solución que elegí fue: un repositorio en GitHub como fuente de verdad donde guardo un JSON por cada materia y una CLI en Rust que valida la estructura de los JSONs, el texto de las preguntas y todo lo que se puede validar y/o corregir automáticamente.
La CLI, además, se encarga de sincronizar las preguntas del repo a la misma base de datos que usa la aplicación, con un sistema de hashes del contenido para ahorrarse mandar data que no cambió desde la última sincronización. Gran parte de la lógica de sincronizado es código libre y está disponible en medici-uy/rust-shared.
En la actualidad, funciona a la perfección y esta solución es de la que estoy más orgulloso. 😀
Infraestructura y DevOps
Como sabrán, el tiempo libre de un estudiante de medicina es prácticamente inexistente limitado,
por eso trato de automatizar todo lo que puedo. Todos los repositorios
tienen CI/CD
con GitHub Actions.
Todos los deployments están a un click de distancia y el único trabajo manual que requiere medici
(además del diseño y desarrollo) es la subida de preguntas a la plataforma.
Todos los recursos de AWS que uso están definidos con CDK en un repo aparte. CDK (y CloudFormation) es lento y a veces tira errores indescifrables, pero me facilita mucho el deployment de la infraestructura y es una de las razones por la que elegí AWS como proveedor.
Otras cosas
- Los diseños de la aplicación y las imágenes para las redes sociales los hago en Figma.
- Los memes que pongo en Instagram y TikTok los hago con DaVinci Resolve.
- Uso Sentry para monitorear errores en todos los niveles de la aplicación.
- Los emails transaccionales están en un repositorio de GitHub, los templates usan MJML y uso SES para mandarlos.
- Las imágenes de materias, avatares y todas las imágenes en la aplicación que parecen que las hizo una IA, están hechas con DALL·E 2.
- Todas las preguntas son categorizadas automáticamente en temas por GPT-4 como parte del pipeline de data.
- Este blog está hecho con Astro y deployado en Cloudflare Pages.
Bueno, creo que no me faltó nada grande.
Y ahora a ver si alguna de las empresas me paga por todos los productos que patrociné.