/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).

Una pintura de un niño abriendo regalos de navidad.
Un programador teniendo libertad tecnológica.

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.

Screenshot del dashboard de Cloudflare.
El Dashboard de Cloudflare. Eso, los datos de Google Search Console y los SELECT COUNT(*) FROM son las únicas métricas que veo.

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.

Screenshot de GraphiQL.
Una query en GraphiQL, la herramienta de desarrollo de GraphQL.

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:

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 😀

Estructura del JSON de una pregunta.
La estructura del JSON de una pregunta.

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

Screenshot del workspace de Figma de medici.
Algún día voy a ordenar el Figma (no, probablemente no).

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é.