La Raya · backend (2): el rediseño — modelos, API DRF, JWT y salto a uv
Publicado el 12/06/2026
En la entrada anterior conté por qué tiré de la primera versión de La Raya: había dejado de ser "una web de fichas" para convertirse en un agregador de contenido local de la frontera hispano-portuguesa. Este post es sobre la base que monté para sostener esa idea.
Modelos pensados para datos que llegan solos
La diferencia clave entre un CMS y un agregador está en los modelos. Cuando escribes a mano, te basta con campos editoriales. Cuando el 90% del contenido va a llegar scrapeado de docenas de fuentes, los modelos tienen que responder a otras preguntas: ¿cómo sé que dos eventos son el mismo? ¿de dónde salió este dato? ¿cómo lo ordeno y lo filtro a escala?
Quedaron, sobre Wagtail, unos pocos modelos centrales:
LocalidadPage: la raíz de todo el contenido local. Cada pueblo de la Raya, con sus coordenadas.EventoPageyNoticiaPage: lo que se agrega, con fecha, categoría y la URL de la fuente original.VenuePage: recintos (cines, teatros, salas) donde ocurren los eventos, para no repetir la dirección en cada evento.
Wagtail sigue siendo útil aquí: el árbol de páginas modela bien "este evento cuelga de esta localidad", y el editor permite curar a mano lo que haga falta. Pero los modelos se diseñaron mirando al pipeline, no al editor.
Una API de dominio, no solo la genérica de Wagtail
Wagtail trae una API de páginas genérica, y es cómoda, pero devuelve "páginas" con su forma interna. La app móvil no quiere eso: quiere /api/eventos/?localidad_id=…&ordering=-fecha_inicio, paginado y con exactamente los campos que pinta.
Así que monté una API de dominio con DRF: vistas de listado y detalle por recurso (eventos, noticias, localidades, gastronomía, turismo, venues), serializers tipados, paginación propia y filtros de verdad (por localidad, por fecha, por texto). La API genérica de Wagtail se queda para casos puntuales; la de dominio es la que consume la app.
Pequeñas decisiones que se pagan solas:
- Serializers bien tipados y documentados con
drf-spectacular(esquema OpenAPI), para que la app sepa qué esperar. - Paginación estándar en todos los listados, porque "devuélvemelo todo" funciona en la demo y revienta en producción.
IsAuthenticatedOrReadOnlypor defecto: lectura pública (la app no necesita login para listar), escritura protegida.
JWT y una app accounts
Aunque el contenido es de lectura pública, quería autenticación para el futuro (favoritos, panel, escritura). Añadí JWT con djangorestframework-simplejwt: access + refresh, rotación de refresh y blacklist al hacer logout (logout "de verdad", invalidando el token, no solo borrándolo en el cliente). Una app accounts encapsula el registro/login. Nada exótico, pero dejarlo montado pronto evita retoques incómodos después.
uv, tipos y tests: la fontanería que no se ve
Migré la gestión de dependencias a uv: instalaciones rápidas y reproducibles, pyproject.toml como fuente de verdad y un requirements.txt regenerable para el Docker. Parece un detalle menor, pero cuando el proyecto crece y lo metes en contenedores, la velocidad y la reproducibilidad de las dependencias se notan a diario.
También empecé a tomarme en serio el tipado de los serializers y a añadir tests sobre las piezas con lógica real (filtros de fecha, i18n). No es cobertura por la cobertura: son las partes donde un cambio silencioso te rompe la app sin que te enteres.
Lo que me llevo de esta fase
- Los modelos cuentan la historia del proyecto. Si los tuyos están pensados para editar a mano y tu realidad es scrapear, vas a pelearte con ellos cada semana.
- Una API de dominio paga su coste enseguida. Exponer "páginas" genéricas es cómodo para ti y un incordio para quien la consume. Mejor dar exactamente lo que la app necesita.
- Invierte pronto en plataforma (uv, JWT, paginación, esquema). Es aburrido y es justo lo que te deja iterar rápido luego sin miedo.
Con esto, la base estaba lista. Pero una base vacía no sirve de nada: la siguiente entrada va de lo más divertido y lo más frágil del proyecto a la vez —enseñar a la máquina a llenar la guía sola— con el sistema de scraping.