BlogDestacada

Como construí Canis, una app colaborativa para mis mascotas desde la base de datos, backend y frontend

Un recorrido técnico por el modelo de datos, el backend en FastAPI (Python) y el frontend en Nuxt (Vue) de Canis.

7 mayo 202613 min de lecturaFoto de Cristóbal Medrano A.Cristóbal Medrano A.
ArquitecturaFastAPIPythonNuxtVuePostgreSQL

Antes de describir la arquitectura detrás de Canis, es importante entender cuál fue el problema que me llevó a diseñar y construir esta app.

¿Cuál era el problema que quería resolver?

Para responder esto, necesito dar un poco de contexto.

Tengo dos perritos, Bucky y Toffee. Con Bucky todo era simple: una libreta de vacunas, un solo veterinario, ningún problema de organización.

Pero justo cuando Toffee llegó a nuestras vidas, la veterinaria donde llevaba a Bucky cerró su sucursal. Eso me obligó a buscar un nuevo lugar. Como Toffee estaba empezando su calendario de vacunas, en cada veterinaria nueva le entregaban su propia libreta.

Al principio no parecía un problema: tenía una libreta para Bucky y otra para Toffee. Pero la cosa se empezó a desordenar rápido.

La veterinaria donde llevaba a Toffee también cerró en medio de su proceso de vacunación, así que su siguiente dosis fue en otro lugar. Nueva ficha, nueva libreta. Después vinieron viajes, cambios de fechas, olvidos… y cada vez que no llevaba la libreta, terminaba con un papel suelto o directamente con otro carnet nuevo.

Sin darme cuenta, terminé con una libreta de Bucky y tres de Toffee.

Y eso no fue lo peor. Un día, cuando lo llevé a ponerle el chip, se me cayeron todas las libretas del bolsillo (iban todas juntas dentro de una que utilizaba como carpeta). Por suerte alguien las dejó apoyadas en un árbol y pude recuperarlas cuando volví sobre mis pasos. Pero ese susto fue suficiente para mí.

Con eso entendí que el problema no era solo el desorden.

El problema era que toda la información importante como vacunas, controles e historial estaba:

  • fragmentada en múltiples libretas.
  • dependía de documentos físicos fáciles de perder u olvidar.
  • y no era accesible cuando realmente la necesitaba.

Además, tampoco podía compartir fácilmente esa información con mi familia, que también participa en el cuidado de los perritos.

Quería algo simple: tener toda la información en un solo lugar, ordenada, accesible desde cualquier parte y segura, sin importar a qué veterinaria fuera o si llevaba algo físico conmigo.

Ahí empezó el desafío real.

Ya no se trataba solo de guardar datos o imágenes. Necesitaba poder manejar la ficha completa de cada perrito: vacunas, visitas, antiparasitarios, historial. Y además, poder compartir todo eso con otras personas.

La idea de implementación debía ser simple, no quería dedicarle tanto tiempo a esto, ya que no es el único side-project en el que trabajo. También construyo un asistente financiero que hasta la fecha me encanta y hace maravillas por mí, pero eso es para otro post.

Entonces, la idea era construir algo que me sirviera a mí y a mi familia, pero que no me tomara tanto tiempo desarrollarlo, además de que fuese barato de mantener. ¿Pude subir todo a un Excel o a un Drive y compartir la carpeta? Sí, pero no se sentiría igual. Quería una experiencia más integrada.

Con esa idea de fondo, la aplicación terminó tomando una forma bastante clara. Por un lado, necesitaba un backend suficientemente serio para manejar identidad, permisos, historial clínico y datos compartidos. Por otro lado, necesitaba un frontend que pudiera sentirse ligero, usable desde el teléfono y compatible con una autenticación moderna sin complicar la experiencia.

En este artículo dejo una base técnica de qué decisiones tomé y de cómo está estructurada hoy la aplicación: cómo modelé la base de datos, cómo organicé el backend y cómo dividí el frontend.

El punto de partida: qué tenía que resolver la app

Canis en un inicio solo debía resolver estos puntos:

  • historial clínico con visitas.
  • diagnósticos y tratamientos.
  • registro de vacunas.

pero rápidamente empezó a crecer más allá de eso:

  • mascotas compartidas entre varias personas.
  • invitaciones para colaborar sobre una mascota.
  • catálogos personales de vacunas.
  • registro de tratamientos antiparasitarios.
  • almacenamiento de imágenes en la nube.
  • importación asistida de certificados de vacunación.
  • etc.

Cómo pensé la base de datos

La primera decisión importante fue elegir la base de datos. Quería algo que me permitiera evolucionar en el tiempo sin tener que preocuparme demasiado por futuras migraciones o cambios de esquema.

Por eso decidí usar PostgreSQL, principalmente por su versatilidad y su soporte para tipos de datos avanzados como jsonb, que permiten manejar información más flexible sin necesidad de crear nuevas tablas cuando no es estrictamente necesario.

Adicionalmente a esto, utilicé Alembic para manejar las migraciones. Esto me permitió tener control sobre la evolución del esquema, pudiendo agregar o modificar tablas y campos de forma incremental.

El modelo fue iterando con el tiempo. Partió siendo algo mucho más simple (mascotas, vacunas, visitas y usuarios), pero fue creciendo a medida que aparecían nuevas necesidades.

Hoy, el esquema se ve así:

Este esquema refleja no solo las necesidades originales, sino también cómo la aplicación fue creciendo.

El corazón del modelo

La mejor forma de entender el esquema es mirarlo desde sus piezas principales.

users representa la identidad base. Desde ahí cuelgan tanto las cuentas de autenticación como los catálogos de vacunas. Pero lo más importante no está en esa tabla, sino en cómo se relaciona con las mascotas.

pets es la entidad central del dominio. No solo guarda información descriptiva, también actúa como pivote de casi todo lo demás: visitas, tratamientos y relaciones con usuarios.

Esa relación se modela a través de user_pets. En vez de asumir una mascota = un dueño, esta tabla intermedia permite asociar múltiples usuarios con distintos roles. Ese pequeño cambio habilita colaboración real y evita tener que rediseñar el modelo más adelante.

En la parte clínica, visits es el evento base. Sobre esa entidad se construye el historial: diagnósticos, tratamientos, notas y costos. A partir de ahí, administered_vaccines registra vacunas aplicadas dentro de una visita.

El modelo también separa definición de ocurrencia. vaccines funciona como catálogo, mientras que administered_vaccines representa eventos concretos. Esto mantiene el historial limpio y permite que el catálogo evolucione de forma independiente.

Finalmente, parasite_treatments y pet_invitations cubren dos necesidades claras: seguimiento preventivo y colaboración entre usuarios.

Que me gusta de este esquema

Hay tres cosas que este modelo resuelve bien.

Primero, modela colaboración real. No asume una mascota como propiedad individual, sino como una entidad compartida.

Segundo, distingue correctamente entre definición y evento. Una vacuna no es lo mismo que una vacuna aplicada. Esa separación evita duplicación y hace el modelo más expresivo.

Tercero, es flexible en el tiempo. Por ejemplo, existe un campo llamado common_names en vacunas que apareció después para soportar alias y mejorar el matching en la importación asistida con IA. El modelo no nació perfecto, pero me permite evolucionarlo en el tiempo.

Cómo organicé el backend

El backend está construido en Python con FastAPI y sigue un enfoque de monolito modular.

Para no partir desde cero, tomé como referencia una estructura de buenas prácticas (como la propuesta por Auth0) y la adapté al contexto del proyecto.

No es la única forma de organizar un backend en FastAPI, pero fue un buen punto de partida para una aplicación con un dominio que ya tenía cierta complejidad y necesitaba crecer sin volverse inmanejable.

Estructura general

La estructura separa responsabilidades de forma bastante clara:

  • app/main.py actúa como composition root.
  • app/api/routes/ define el borde HTTP por dominio.
  • app/api/deps.py concentra dependencias compartidas.
  • app/core/ agrupa configuración, seguridad, base de datos y piezas transversales.
  • app/models.py y app/schemas.py separan persistencia y contratos.
  • app/crud.py centraliza la lógica de aplicación.
  • app/services/ encapsula integraciones externas.

El flujo es simple: una request entra por FastAPI, resuelve dependencias (como sesión o CSRF) y delega rápidamente. Las rutas quedan como una capa delgada: reciben, validan y responden.

Para proyectos más grandes suelo modularizar por dominio, pero en este caso prioricé simplicidad para no sobrediseñar desde el inicio.

El frontend de Canis

El frontend está construido con Nuxt 4 (Vue 3) y Typescript, usando Tailwind CSS para el diseño visual. Desde el inicio opté por un enfoque mobile-first, ya que el uso principal ocurre en dispositivos móviles.

La elección de Vue fue principalmente práctica. Necesitaba:

  • manejar estado reactivo sin demasiada configuración,
  • iterar rápido en la UI.
  • mantener el código simple de entender y mantener.

Vue resolvía bien ese escenario. Su sistema de reactividad es directo. Con la Composition API modelé el estado y la lógica sin depender de múltiples bibliotecas externas.

Nuxt, por su parte, me permitió partir con una base sólida: routing, SSR y estructura de proyecto ya resueltos. Eso redujo bastante el tiempo de desarrollo.

Cómo organicé el frontend

Si el backend está organizado por módulos simples, el frontend está organizado por capas funcionales (layers).

La app usa el sistema de layers de Nuxt 4, separando el dominio en: core, auth, pets, vaccines, records y collaborators.

Cada layer funciona como una unidad relativamente independiente, con su propia estructura interna y responsabilidades. No llega a ser un microfrontend, pero se acerca a esa idea sin introducir su complejidad.

En lugar de organizar el código por tipo (componentes, servicios, etc.), cada layer agrupa todo lo que pertenece a una capacidad del producto. Eso reduce el acoplamiento y hace que trabajar en una funcionalidad sea más directo: casi todo vive en el mismo lugar.

Un patrón simple para mover datos

La UI repite bastante un flujo que llevo probando hace mucho tiempo

  • los componentes se enfocan en la visualización.
  • los composables manejan estado y coordinan acciones.
  • los services encapsulan el contrato HTTP.
  • y Zod valida los datos en el borde.

Este patrón se repite en todos los módulos. Por ejemplo, en mascotas, usePets() gestiona estado local y mutaciones, mientras petService se encarga de hablar con la API y normalizar la respuesta.

Cada capa tiene una responsabilidad clara y juntas forman un flujo predecible.

La pieza más delicada

useApi() probablemente sea la pieza más crítica del frontend, porque resuelve la fricción entre SSR y un backend autenticado por cookies.

Es un wrapper que construí sobre ofetch que se encarga de:

  • reenviar cookies en SSR.
  • propagar Set-Cookie al cliente.
  • adjuntar el token CSRF cuando corresponde.
  • intentar refresh ante respuestas 401.
  • reintentar la request original una vez.

Funciona tanto en servidor como en cliente, lo que permite mantener una experiencia consistente.

Sesión y SSR

La sesión se maneja a través de useUserSession(), que mantiene el estado del usuario y expone operaciones como fetch y clear.

Encima de eso, plugins de Nuxt intentan hidratar la sesión en SSR cuando es posible y la revalidan en cliente si hace falta. Esto evita inconsistencias visuales durante la hidratación.

La forma de la interfaz

A nivel visual, la app no está pensada como un panel de escritorio tradicional.

Se apoya en Nuxt UI y Tailwind, pero con una lógica mobile-first: columna central, ancho contenido, header sticky y navegación inferior flotante.

Esta decisión no es solo estética, también define cómo se usa la aplicación.

Un flujo de punta a punta

Un ejemplo simple muestra cómo encajan todas las piezas:

Lo importante no es solo que funcione, sino que cada capa tiene una responsabilidad clara. Esa separación es la que permite que el sistema siga siendo modificable a medida que el dominio crece.

Dónde está desplegada la app

Canis está desplegada completamente en Railway. La decisión fue principalmente pragmática: necesitaba algo simple de operar, con buen soporte para despliegues rápidos y sin tener que dedicar tiempo a infraestructura.

El entorno funciona con un esquema de cold start, lo que significa que la primera request después de un período de inactividad puede ser más lenta. Es un trade-off consciente: a cambio de esa latencia inicial, el costo de hosting se mantiene bajo y predecible.

Por delante, Cloudflare cumple dos roles importantes:

  • protege la aplicación a nivel de red.
  • actúa como capa de almacenamiento para imágenes a través de R2.

Esta combinación mantiene la arquitectura simple, pero lo suficientemente robusta para el uso actual del proyecto.

Cierre

Canis partió como una necesidad bastante concreta: dejar de depender de papeles y ordenar la información de mis mascotas. Pero en el proceso terminó convirtiéndose en un sistema con varias capas, decisiones técnicas y un modelo que fue evolucionando junto con el producto.

Sigue siendo un proyecto personal, pero está construido con la intención de escalar sin perder simplicidad. Cada decisión (desde la base de datos hasta la organización del frontend) apunta a eso: mantener el sistema flexible, entendible y fácil de extender.

Hoy ya resuelve el problema original de forma bastante sólida y al mismo tiempo deja espacio para seguir iterando sobre nuevas ideas.

Si te interesa explorarlo, puedes ver la aplicación en:

Canis - Una app colaborativa de registro y seguimiento de mascotas