Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arquitectura dels frameworks

A Arquitectura hem vist arquitectures que es decideixen: on posem les fronteres, cap on apunten les dependències, quan compensa un nucli de domini aïllat. Un framework web no espera aquestes decisions: en porta unes quantes de fetes i ens hi fa entrar des de la primera línia de codi.

Aquesta pàgina dona el vocabulari per reconèixer quina arquitectura imposa un framework, perquè treballar a favor de l’arquitectura d’un framework és barat i treballar-hi en contra és car. Serveix per a dues coses: llegir un projecte que ja existeix i triar amb criteri quan el projecte és nou. Els conceptes són els d’Arquitectura, aquí traduïts al que veuràs realment.

Els exemples cobreixen els entorns de servidor més estesos: Django i FastAPI (Python), Spring (Java), Laravel i Symfony (PHP), ASP.NET Core (C#), Express i NestJS (Node.js), Ruby on Rails (Ruby) i Gin (Go). Rails hi és perquè és l’origen de bona part de les convencions que fan servir els altres, i Symfony perquè, sent del mateix llenguatge que Laravel, pren la decisió contrària en el punt que veurem que més importa. A les taules del final hi apareixen també Next.js i Phoenix, que serveixen de contrapunt quan alguna de les idees d’aquí no s’hi aplica.

Comencem pel que tenen en comú, que és més del que sembla, i després veurem en què es diferencien.

Peces comunes

La diferència entre una llibreria i un framework és qui té el control. Una llibreria la cridem nosaltres. Un framework ens crida a nosaltres: escrivim funcions i classes que ell decideix quan executar. És la inversió de control de Disseny aplicada, en lloc de a un objecte, a l’aplicació sencera.

D’aquí en surt una conseqüència que sovint passa desapercebuda: el framework és el composition root. El cablejat de components que a Arquitectura descrivíem com una responsabilitat externa a l’aplicació, aquí ja el fa el framework, i per això gairebé mai escrivim un main que construeixi res.

Tot framework web posa quatre peces, amb noms diferents però amb la mateixa funció:

  • El front controller: un punt d’entrada únic pel qual passen totes les peticions abans de ser repartides. No hi ha un fitxer per URL, sinó un sol porter.
  • L’encaminament (routing): la taula que decideix quin codi nostre atén cada ruta.
  • La cadena de middleware: una seqüència de baules que la petició travessa, i on cadascuna pot processar-la, aturar-la o deixar-la passar. És el patró chain of responsibility, i és on viuen les responsabilitats transversals (en anglès, cross-cutting concerns): feines que calen a moltes peticions però que no són la lògica de negoci de cap, com el registre, la sessió, l’autenticació, el CORS o el CSRF. Es diuen així perquè travessen totes les capes en lloc de pertànyer a una, i posar-les aquí evita haver-les de repetir a cada controlador.
  • El contenidor d’inversió de control: qui construeix els objectes i els proporciona les seves dependències.

Les tres primeres hi són sempre. La quarta, no. Spring té l’ApplicationContext, Laravel el seu service container, Symfony un contenidor configurable que és dels més elaborats que trobaràs, ASP.NET Core el porta integrat al propi framework i NestJS també en té un de propi: tots cinc construeixen un graf d’objectes de llarga vida en arrencar. FastAPI en fa una versió més modesta, resolent dependències per petició amb Depends, sovint per obtenir la sessió de base de dades o l’usuari autenticat. Django, Rails, Express i Gin no en tenen cap: els dos primers ho resolen amb convenció i configuració, i als altres dos cada mòdul construeix les seves dependències o se n’importa una instància compartida. És una de les raons per les quals el codi d’aquests frameworks té un aspecte tan diferent tot i resoldre els mateixos problemes.

La forma d’estendre el framework (apps a Django, engines i gems a Rails, starters a Spring, service providers a Laravel, bundles a Symfony, paquets amb els seus mètodes de registre a ASP.NET Core, mòduls a NestJS, middleware d’npm a Express) és l’estil microkernel aplicat al framework mateix: un nucli fix més peces connectables.

Acoblament al framework

Val la pena mirar tot això amb el criteri de fons d’acoblament i cohesió. Un framework sempre t’acobla a ell mateix: si desapareix, o si vols canviar-lo, has de reescriure tot el codi que el toca. La pregunta útil no és si hi ha acoblament, que n’hi ha sempre, sinó quina part del teu codi el pateix.

Si el framework només apareix als controladors i a la configuració, l’acoblament és a la frontera i el nucli és portable. Si apareix també als models i a les regles de negoci, l’aplicació és el framework. Aquesta és la idea que recorre tota la resta de la pàgina, i més avall veurem que la tria d’ORM la decideix gairebé sola.

El recorregut d’una petició

La millor manera de veure les quatre peces juntes és seguir una petició HTTP de punta a punta. Amb petites variacions de nom, el recorregut és el mateix a tots els frameworks.

flowchart TB
    CL(["Client"]) -->|"petició"| FC
    subgraph fw["Infraestructura del framework"]
        direction TB
        FC["Front controller"] --> MW["Cadena de middleware"] --> RT["Encaminador"]
    end
    RT --> CT
    subgraph app["Codi de l'aplicació"]
        direction TB
        CT["Controlador"] --> SV["Servei"] --> RP["Repositori o ORM"]
    end
    RP --> DB[("Base de dades")]
    MW -->|"resposta"| CL
  1. El servidor rep la petició i la lliura al front controller.
  2. La petició travessa la cadena de middleware: es desxifra la sessió, s’autentica l’usuari, es validen els tokens.
  3. L’encaminador compara la ruta amb la taula de rutes i tria el gestor.
  4. El controlador valida l’entrada, la converteix a tipus del nostre domini i delega. No conté lògica de negoci.
  5. El servei aplica les regles de negoci.
  6. El repositori o l’ORM tradueix les operacions sobre objectes a operacions sobre la base de dades.
  7. La resposta es construeix i torna cap enrere per la mateixa cadena de middleware, ara en sentit invers.

El diagrama deixa veure dues coses. La primera és on és la frontera entre el que posa el framework i el que escrius tu: el controlador. La segona és el pas 7, i és el detall que val la pena retenir: la cadena de middleware es recorre dues vegades, a l’anada i a la tornada. Per això hi encaixen tan bé les responsabilitats transversals, que sovint necessiten fer alguna cosa abans i després (mesurar el temps, afegir capçaleres, convertir una excepció en un codi HTTP).

El middleware no és l’únic lloc on apareixen. Quan la responsabilitat transversal no depèn de la petició sinó de la crida a un mètode, com les transaccions o la memòria cau, els frameworks l’apliquen amb anotacions o decoradors que embolcallen el mètode. Portada a l’extrem, aquesta idea és la programació orientada a aspectes (AOP), que és el mecanisme amb què Spring implementa coses com @Transactional.

Això també concreta on és la frontera de la qual parla la gestió d’errors. La frontera d’entrada on cal transformar errors interns en respostes és exactament aquesta cadena, i tots els frameworks ofereixen un lloc previst per fer-ho, ja sigui un middleware d’errors o un gestor d’excepcions global.

ASP.NET Core és el més clar per estudiar-ho, perquè el pipeline s’escriu explícitament al codi d’arrencada de l’aplicació, mentre que als altres queda més amagat dins de la configuració.

Model d’execució

El recorregut que acabem de veure amaga una decisió important: què passa mentre esperem. Una petició passa la major part del temps esperant la base de dades o un servei extern, i cada framework té una manera diferent de fer alguna cosa de profit durant aquesta espera. Els models de concurrència hi surten en detall; aquí ens interessa què impliquen per a l’arquitectura.

  • Un fil per petició (Spring MVC, Rails, Laravel, Symfony, Django amb WSGI, ASP.NET Core). El més senzill de raonar, perquè el codi es llegeix de dalt a baix i pots guardar coses associades al fil. Tradicionalment el límit era el nombre de fils del sistema operatiu, que no és gaire alt perquè cadascun ocupa memòria. Els fils virtuals de Java 21, que Spring Boot activa amb una sola opció de configuració, han tret bona part d’aquest sostre: el codi segueix sent seqüencial i fàcil de llegir, però n’hi poden conviure centenars de milers.
  • Bucle d’esdeveniments (Express, NestJS, FastAPI, Django amb ASGI). Un sol fil atén moltes peticions alternant entre elles quan alguna espera. Escala molt millor amb operacions d’entrada i sortida, però imposa una regla dura: si bloqueges, ho bloqueges tot. Una consulta síncrona o un càlcul llarg atura totes les peticions alhora, i per això aquests entorns exigeixen que tota la cadena sigui asíncrona.
  • Reactiu (Spring WebFlux, Vert.x). El mateix objectiu amb fluxos de dades i control de saturació (backpressure). Molt potent i força més difícil de llegir i de depurar. Des dels fils virtuals ha perdut terreny en les aplicacions de petició i resposta corrents, i queda sobretot per a l’streaming i per als serveis que han de regular la saturació.
  • Processos lleugers (Elixir amb Phoenix, Akka). Milers de processos aïllats que es comuniquen per missatges i que poden fallar i reiniciar-se per separat. És el pas de missatges portat al disseny sencer de la plataforma.

La conseqüència pràctica és que el model d’execució no es pot barrejar impunement. Fer servir un ORM síncron dins d’un gestor asíncron és l’error més habitual d’aquest terreny, i el símptoma és una aplicació que va bé amb un usuari i s’enfonsa amb cinquanta.

Quan no hi ha procés

El cas límit és el serverless (AWS Lambda, Azure Functions, Cloudflare Workers): no hi ha cap procés de llarga vida, sinó una funció que s’instancia quan arriba la petició i desapareix després. Trenca dues coses que hem donat per fetes a tota la pàgina:

  • No hi ha composition root a l’arrencada, o més ben dit, n’hi ha un per invocació. Construir un graf d’objectes gran cada vegada es paga en latència, i és part del problema de l’arrencada en fred (cold start). Per això aquí compensen els frameworks prims.
  • No hi ha estat a la memòria. Res del que guardis entre peticions sobreviu, de manera que la sessió, la memòria cau i les cues han de viure fora, en un servei a part.

És l’estil de contenidor que Arquitectura anomena funció serverless, i encaixa bé amb feines curtes i de càrrega irregular, no amb aplicacions que mantenen converses llargues amb el client.

Quan el servidor parla primer

Tota la pàgina dona per fet que el client pregunta i el servidor respon. Hi ha funcionalitats que no hi caben: notificacions, xats, progrés d’una tasca llarga, taulers que s’actualitzen sols. Aquí és el servidor qui ha de començar, i les vies són tres (vegeu Protocols i APIs de JavaScript):

  • Consulta periòdica (polling). El client pregunta cada pocs segons. No cal res del framework i funciona a tot arreu, a canvi de latència i de peticions inútils. Per a moltes funcionalitats és suficient, i convé descartar-la abans de complicar-se, no després.
  • SSE (server-sent events). Un canal d’un sol sentit, del servidor cap al client, per damunt d’HTTP corrent, amb reconnexió automàtica de franc. És l’opció adequada quan les dades només baixen.
  • WebSockets. Una connexió bidireccional i permanent, amb un protocol propi al qual s’arriba ascendint una petició HTTP. Cal quan les dues bandes envien coses.

Les dues últimes trenquen dos supòsits de la pàgina. El primer és el model d’execució: una connexió oberta ocupa recursos durant minuts o hores sense fer res, i un fil per connexió no s’aguanta. Per això aquest terreny és natural per al bucle d’esdeveniments i per als processos lleugers, i per això molts frameworks clàssics hi arriben per una porta lateral: Django ho fa amb ASGI i Channels, Laravel delega la connexió a un servei a part (Reverb o Pusher) i Rails hi posa Action Cable. Spring i ASP.NET Core ho porten integrat, aquest últim amb SignalR, i Phoenix és el cas on això no és cap afegit, sinó el motiu del framework.

El segon supòsit és que no hi ha estat entre peticions. Una connexió viu lligada a una instància concreta del servidor, de manera que amb més d’una instància un missatge emès en una no arriba als clients connectats a l’altra. La solució habitual és un intermediari de publicació i subscripció al mig, sovint Redis o un servei gestionat, que és exactament la peça que el desplegament ha de preveure. A més, la cadena de middleware no s’aplica igual: l’autenticació es comprova un sol cop, en obrir la connexió, i no a cada missatge.

Opinionats i microframeworks

El model d’execució és la primera diferència de pes. La segona, i la que més es nota en el codi de cada dia, és quantes decisions pren el framework en lloc teu. Ordenats per aquest eix, els nostres exemples formen tres grups:

  • Microframeworks: Express, Fastify i Gin. Aporten l’encaminament i la cadena de middleware, i s’aturen aquí. No imposen capes, ni ORM, ni contenidor de dependències, ni estructura de directoris. A Go aquesta preferència va més enllà del framework: la cultura del llenguatge desconfia de la màgia i sol començar per la llibreria estàndard.
  • Prims, però amb contracte: FastAPI. Segueix sense imposar ORM ni capes, però hi afegeix dues coses que Express no té: injecció de dependències integrada (Depends) i validació i documentació a partir de tipus, amb Pydantic i OpenAPI. És a dir, el disseny per contracte, comprovat pel framework en temps d’execució.
  • Opinionats, amb les piles posades: Rails, Django, Spring, Laravel, Symfony, ASP.NET Core i NestJS. Porten estructura, capes, convencions i, gairebé sempre, un ORM i un contenidor de dependències. El lema que els resumeix, convention over configuration, el va popularitzar Rails: si segueixes les convencions no has de configurar res, i el preu és que sortir-ne costa.

Val la pena fixar-se que Express i NestJS comparteixen llenguatge i runtime, i tot i així NestJS s’assembla molt més a Spring que a Express, decoradors inclosos: la posició en aquest eix no la marca el llenguatge sinó el framework.

Com se’n diu

Aquest eix té tres noms a la indústria, i els trobaràs tots tres:

  • Opinionated i unopinionated: el terme més precís per al que estem descrivint, o sigui fins a quin punt el framework imposa una manera de fer les coses.
  • Batteries included i microframework: com es descriuen ells mateixos. Django diu que ve amb les piles posades; Flask i Express es presenten com a microframeworks.
  • Heavyweight i lightweight: ve dels primers anys 2000, quan Spring i Hibernate es van presentar com l’alternativa lleugera als contenidors EJB de J2EE. Compte amb aquest, perquè avui “lleuger” també vol dir consum de memòria, temps d’arrencada o nombre de dependències, que és una cosa diferent.

On va a parar el cost

Sigui com sigui que en diguem, la tria no és una qüestió de gust ni de quantitat de codi. El que canvia és on va a parar el cost.

Amb un framework opinionat, moltes decisions ja estan preses: on van els fitxers, com es valida, com s’autentica, com es fan les migracions de l’esquema de la base de dades. Les convencions són compartides més enllà de l’equip, de manera que qui arriba nou a un projecte Django o Laravel ja sap on mirar. Funcionen, de fet, com les decisions explícites de què parlàvem: algú va decidir per tu i no cal tornar-ho a discutir. A canvi, hi ha molt a aprendre abans de ser productiu, i quan el teu cas no encaixa amb el que el framework espera, sortir-te’n costa més que si no hi hagués framework.

Amb un microframework tens el control i una superfície petita d’aprendre, però cap decisió no ve donada. Cada projecte inventa la seva estructura i, per tant, dos projectes Express de la mateixa empresa poden no assemblar-se gens. El cost no desapareix: es desplaça d’aprendre el framework a crear i mantenir les teves pròpies convencions, cosa que exigeix la disciplina d’equip de què parla Arquitectura en equip.

Un framework minimalista, doncs, no elimina les decisions d’arquitectura: només te les deixa a tu. Els projectes Express que acaben malament rarament fallen per Express, sinó perquè ningú no va decidir mai on anava la lògica de negoci. A l’altre extrem passa el contrari, i el risc és no adonar-te de quines decisions ha pres el framework per tu. Les seccions següents són, precisament, les decisions que val la pena mirar de prop.

Capes: controlador, servei, repositori

La divisió en controlador, servei i repositori és la separació horitzontal amb noms concrets, i és la forma que tindrà la major part del codi que escriuràs. Als frameworks opinionats ve donada; a un microframework no hi és fins que la crees tu, i és la primera decisió que caldrà prendre.

Val la pena traduir-la al vocabulari de l’arquitectura hexagonal, perquè encaixa millor del que sembla:

  • El controlador és un adaptador primari: converteix una petició HTTP en una crida al domini.
  • El servei és el més semblant a un cas d’ús.
  • El repositori és un port de sortida i la seva implementació concreta: l’aplicació hi demana entitats sense saber d’on surten.

Sobre els noms, un avís. Els patrons MVC, MVP i MVVM estan explicats a Patrons de disseny, i convé no barrejar-los amb el que veiem aquí. El controlador d’un framework de petició i resposta no és el controlador observador de l’MVC d’escriptori: no escolta esdeveniments ni notifica vistes, simplement atén una petició i retorna una resposta. Django ho fa evident anomenant view el que els altres anomenen controlador, i template el que els altres anomenen vista, i per això la seva arquitectura se sol descriure com MVT en lloc de MVC.

Transaction Script vs Domain Model

Un cop tenim les capes, queda la pregunta de què hi posem a dins. Hi ha dues maneres clàssiques d’organitzar la lògica de negoci:

  • El Transaction Script: un mètode per operació, que fa els passos en ordre de dalt a baix. Els models només transporten dades, sense comportament, i per això se’n diu model anèmic.
  • El Domain Model: objectes amb dades i comportament que representen els conceptes del negoci, i que protegeixen les seves pròpies regles. És el que hi ha darrere del DDD.

El codi per defecte de tots els frameworks que hem vist és Transaction Script, i convé dir clarament que no és cap error. Per a una aplicació CRUD, on la lògica és validar i desar, és la decisió correcta i la més barata: un domini ric que només trasllada camps d’un lloc a l’altre és cerimònia sense contrapartida.

El Domain Model compensa quan les regles de negoci són el problema difícil, quan hi ha invariants que s’han de complir sempre i quan la mateixa regla apareix en operacions diferents. Aquest és el criteri per decidir si val la pena tot l’aparell del DDD i de l’arquitectura hexagonal, o si el que tens al davant és un CRUD que no els necessita.

Active Record vs Data Mapper

La decisió del framework que més condiciona l’arquitectura és com mapeja els objectes a la base de dades. Hi ha dos patrons, explicats amb detall a Conceptes de persistència:

  • Active Record (Django ORM, Eloquent, Sequelize, Mongoose): el model és la fila de la taula i sap desar-se sol.
  • Data Mapper (JPA amb Hibernate, Entity Framework Core, Doctrine, SQLAlchemy): el mapatge viu en una capa a part i el model no sap que existeix la base de dades.

La divisió no depèn del llenguatge, sinó del framework, i el millor exemple és el PHP: Laravel fa servir Eloquent, que és Active Record, i Symfony fa servir Doctrine, que és Data Mapper. Mateix llenguatge, decisions oposades, i dues maneres molt diferents d’escriure la mateixa aplicació. I als frameworks que no imposen ORM la decisió torna a ser teva: a Express, a NestJS o a Gin tries la llibreria i, amb ella, el patró. A Node la tria habitual avui és Prisma o Drizzle, i el creixement d’aquesta última assenyala cap on bufa el vent: renuncia a l’abstracció gruixuda i es queda a prop del SQL. El cas més instructiu, però, és TypeORM, més antic i en retrocés, perquè implementa tots dos patrons i et deixa triar: converteix en una decisió explícita el que als altres frameworks queda amagat.

El que hi ha en joc és la regla de dependència: cap capa interna no ha de dependre d’una d’externa, i la persistència és tan externa com pugui ser-ho. Amb Active Record el domini depèn de la base de dades per construcció, perquè les entitats hereten de la classe base de l’ORM. Aïllar el domini vol dir mantenir un joc d’objectes paral·lel i traduir en tots dos sentits, o sigui pagar dues vegades. Amb Data Mapper les entitats són objectes normals i el port de sortida ja hi és pràcticament de sèrie.

Per això el mateix consell d’arquitectura hexagonal té preus tan diferents: surt barat amb un Data Mapper, com a Spring o a ASP.NET Core, i car a Django i a Laravel. En un framework Active Record la resposta pragmàtica no és un nucli hexagonal, sinó models amb comportament i una capa de serveis prima al damunt. Anar en contra del framework té un cost, i cal que sigui una decisió conscient.

Convé saber que hi ha una tercera resposta, que és no tenir ORM. Ecto, la capa de dades de Phoenix, n’és el cas més conegut: no manté identity map ni càrrega mandrosa, les consultes són explícites i els canvis passen per changesets. Renunciant a l’abstracció, la pregunta d’aquesta secció deixa de tenir sentit, i de passada el problema N+1 costa molt més de provocar sense adonar-se’n. És la direcció que assenyala també Drizzle a Node: menys màgia i més a prop del SQL.

Migracions de l’esquema

L’ORM no només decideix com llegeixes les dades: també decideix com canvia la taula on són. Una migració és un canvi de l’esquema de la base de dades guardat com un fitxer al projecte, amb un ordre i un identificador, de manera que qualsevol còpia es pot posar al dia aplicant les que li falten. Per què calen i com afecten el desplegament és a Evolució de l’esquema; aquí ens interessa qui les escriu, perquè això sí que ho decideix el framework.

La paraula tapa tres maneres de fer molt diferents, i la tria decideix on és la font de veritat de l’esquema:

  • Generades a partir dels models (Django, Doctrine, Entity Framework Core, i opcionalment Alembic). L’eina compara les classes amb l’historial de migracions i escriu el fitxer. La font de veritat és el codi. És molt còmode, però convé llegir el que genera abans d’aplicar-ho: un canvi de nom de camp el veu sovint com esborrar una columna i crear-ne una altra, i això és perdre dades.
  • Escrites a mà amb l’API del framework (Rails, Laravel, Ecto). El framework posa el versionat i el generador del fitxer buit, i el contingut el decideixes tu, escrit en el teu llenguatge. Res no endevina res, a canvi d’escriure més.
  • Escrites en SQL (Flyway, Liquibase, golang-migrate). La font de veritat és el SQL, i el codi no hi té cap paper. És l’opció habitual quan la base de dades és d’integració i la comparteixen diverses aplicacions, o quan qui mana l’esquema no és l’equip de desenvolupament.

És una decisió que va lligada al patró de mapatge de la secció anterior: amb Active Record l’esquema surt dels models gairebé sempre, i amb un framework prim la tria torna a ser teva. El que no convé és barrejar-ne dues, per exemple generar-ne i després editar l’esquema per fora, que és la manera segura d’acabar amb un entorn que ningú no sap reproduir.

Testabilitat

A Arquitectura dèiem que la dificultat d’escriure una prova és un senyal de disseny, i que necessitar una base de dades per comprovar una regla de negoci vol dir que hi falta un port. Val la pena adonar-se que la tria de framework decideix en bona part en quina fila d’aquella taula acabaràs, i és per això que la testabilitat és un criteri de tria i no una conseqüència que es descobreix al final.

  • Amb un Data Mapper i un contenidor de dependències (Spring, ASP.NET Core, NestJS), un servei rep els seus col·laboradors pel constructor i es pot provar amb dobles de prova, sense base de dades. El port ja hi és.
  • Amb Active Record (Django, Laravel), qualsevol regla que visqui al model arrossega l’ORM i, amb ell, una base de dades de proves. Són proves més lentes i més fràgils, i el framework hi posa facilitats precisament perquè dona per fet que les necessitaràs.
  • Amb un microframework (Express) tot depèn de com hagis muntat tu les dependències. Si les importes directament allà on les fas servir, no hi ha manera de substituir-les i acabes provant només per HTTP.

També canvia què pots automatitzar. Les funcions d’aptitud que comproven regles de dependència tenen eines madures en alguns ecosistemes i no en tots: a Java hi ha ArchUnit, i n’hi ha d’equivalents a .NET i a TypeScript, mentre que en altres entorns la regla “el domini no importa la infraestructura” es queda en un acord d’equip, que és exactament el que dèiem que es trenca tard o d’hora.

Res d’això descarta cap framework. Només diu quin preu pagaràs, i convé saber-lo abans i no després.

Acoblament amb el frontend

Fins ara hem donat per fet que la petició acaba amb una resposta, sense dir de quina mena. Aquí els frameworks es diferencien molt, i alguns tenen una opinió molt marcada sobre com ha de ser el client. Hi ha quatre posicions, ordenades segons quanta presentació es desplaça del servidor al navegador:

  1. HTML generat al servidor. El framework és l’amo de la vista i el navegador rep HTML acabat. És el model de Django Templates, Blade, Twig, ERB, Thymeleaf i Razor, i el de les aplicacions multipàgina (MPA) de què parla Aplicacions web.
  2. Reactivitat des del servidor. S’aconsegueix interactivitat sense escriure una SPA, enviant fragments d’HTML en lloc de dades: Livewire a Laravel, Hotwire a Rails, Blazor a ASP.NET Core, o HTMX, que funciona amb qualsevol backend. La lògica es queda al servidor.
  3. API més SPA. El backend només serveix JSON i el frontend és un projecte i un desplegable a part, normalment amb React, Vue o Angular. Vegeu Single-Page Application. És el terreny natural d’Express, FastAPI, NestJS i, a la pràctica, de la majoria de projectes Spring Boot.
  4. Meta-frameworks de pila completa. Next.js, Nuxt i SvelteKit posen frontend i backend al mateix projecte i el mateix llenguatge, de manera que la frontera entre tots dos deixa de ser una petició HTTP i passa a ser una crida a una funció que el framework decideix on executar. La presentació és tota del client, però l’acoblament no baixa: torna a pujar, perquè aquesta frontera la torna a definir el framework.

Sobre l’opinió, els extrems són clars. Laravel arriba a distribuir plantilles de projecte que ja munten React o Vue connectats amb Inertia, i ASP.NET Core ofereix Blazor per escriure el client en C#. Django i Spring són més neutrals, i Express i FastAPI no en tenen cap opinió: fan JSON i ja decidiràs tu qui se’l menja.

Què implica una SPA

La tercera posició és la més estesa avui i la que més canvia l’arquitectura, així que val la pena mirar-la de prop:

  • La capa de plantilles desapareix del backend i la presentació se’n va al client. Els patrons de Model-View, i MVVM en particular, s’apliquen allà, no aquí.
  • La frontera passa a ser una frontera de servei, amb tot el que implica: ara hi ha un contracte REST enmig i s’apliquen els criteris de disseny d’ Estils de frontera de servei.
  • Canvia la sessió. La galeta de sessió del servidor sol deixar pas a un token que el client guarda i envia, i apareix el CORS, perquè el frontend ja no es serveix del mateix origen.
  • Hi ha dos desplegables i, per tant, dues canonades de desplegament i dues versions que poden desincronitzar-se.
  • El risc principal és que la lògica de negoci se n’estigui anant al client. La validació del navegador és per a l’experiència d’usuari; el servidor ha de tornar a validar-ho tot, perquè és l’única frontera que l’usuari no controla.

Quan hi ha diversos clients (web, mòbil, socis externs) i cadascun necessita dades diferents, la resposta habitual és el BFF (backend for frontend): un servei prim per client que adapta el que ofereixen els serveis de sota, en lloc d’una API única que intenta acontentar tothom.

Qui decideix el contracte

Les quatre posicions no formen una escala d’acoblament: la 4, ja ho hem dit, en té tant com la 1. El que sí que les separa, i decideix si després podràs canviar les peces, és qui és l’amo del contracte. A les posicions 1, 2 i 4 el posa el framework: les plantilles de Django, els fragments de Livewire, les crides al servidor de Next.js. No l’has escrit tu i sovint ni el veus. A la posició 3 el decideix l’equip: uns recursos, uns camps i uns codis d’estat, documentats amb OpenAPI.

Aquesta diferència és exactament la dels mecanismes de separació. Un contracte imposat pel framework és una frontera de codi font: funciona perquè les dues bandes es despleguen juntes i, com dèiem allà, depèn de la bona fe. Un contracte decidit per l’equip és una frontera de servei: passa per un protocol, i el protocol l’obliga.

Les conseqüències van totes en parell:

  • Substituïbilitat. Amb el contracte imposat, les dues bandes van lligades: no hi pots penjar una app mòbil, i canviar-ne una vol dir tocar l’altra. Amb el contracte explícit, cada banda es pot reescriure per separat mentre es respecti el que hi ha escrit.
  • Evolució. El contracte explícit es pot fer créixer amb la regla del disseny per contracte: no requerir més ni prometre menys. És la mateixa norma que apliquem als mètodes d’una classe, ara entre dos desplegables.
  • Cost. El contracte imposat és molt més productiu: no hi ha serialització, ni versions, ni documentació que mantenir, i sovint els tipus es comproven de punta a punta. L’explícit te’l fa mantenir a tu, versions incloses, i el preu es paga cada dia.

No és, doncs, que un sigui millor que l’altre. És que decidir qui és l’amo del contracte és decidir si les peces seran substituïbles, i val més prendre aquesta decisió a consciència que descobrir-la el dia que algú demana una app mòbil.

Autenticació i autorització

Decidit qui és l’amo del contracte, la pregunta següent és qui hi ha a l’altra banda i què pot fer. Poques coses porten tanta feina i tan poc valor diferencial. Els conceptes (sessions, tokens, RBAC) són a Control d’accés; aquí ens interessen dues preguntes: quant en posa el framework i com de lligat t’hi quedes.

Quant en posa varia molt, i és una bona mostra de l’eix opinionat i prim:

  • Tot fet. Django porta model d’usuari, sessions, permisos i pantalles d’administració des del primer minut; Laravel reparteix la feina entre paquets propis (Fortify per a l’autenticació, Sanctum per als tokens, Gates i Policies per als permisos); ASP.NET Core té Identity; Rails genera el codi d’autenticació amb una ordre i te’l deixa al projecte perquè el mantinguis tu.
  • Un framework a part per configurar. Spring Security és el cas extrem: una cadena de filtres davant de la teva, amb un vocabulari propi que s’ha d’aprendre, molt potent i amb una corba dura. El SecurityBundle de Symfony i els guards de NestJS segueixen la mateixa idea a menor escala.
  • Res. Express, FastAPI i Gin no tenen model d’usuari. Et donen les peces (dependències, middleware, ajudes per a OAuth2) i la resta la muntes tu, amb llibreries com Passport o Lucia. La llibertat és real i la responsabilitat també: les decisions dolentes seran teves.

Sigui quin sigui el nivell, el repartiment és sempre el mateix. L’autenticació és una responsabilitat transversal i viu a la cadena de middleware: es resol una vegada, aviat, i la resta del codi ja treballa amb un usuari identificat. L’autorització no hi cap del tot. Les comprovacions gruixudes (aquest rol pot entrar en aquesta ruta) sí que hi van, però la pregunta freqüent és si aquest usuari és l’amo d’aquesta factura, i això depèn de les dades: baixa al servei o a la consulta. Per això Control d’accés insisteix que s’ha de comprovar a totes les capes, i no només allà on és còmode.

Fins on arriba l’acoblament

El model d’usuari sol viure a l’ORM del framework i acaba referenciat des de mig projecte, cosa que el converteix en una de les peces més cares de substituir. La forma de treure-se’l de sobre és la mateixa que ja hem vist amb el frontend: convertir una frontera de codi font en una frontera de servei. Delegant la identitat a un proveïdor extern amb OIDC (Keycloak, Auth0, Entra ID), l’aplicació es queda només amb la part de validar un token i llegir-ne les dades, i qui autentica deixa de ser una decisió del framework.

Tres coses no canvien mai. La primera, que si el frontend és una SPA la tria de sessió o token ja te la va fer l’apartat anterior, amb el CORS inclòs. La segona, que el resum d’una contrasenya i la generació de tokens es fan amb el que porta el framework, mai amb codi propi: tots duen un algorisme de resum lent i amb salt (bcrypt, Argon2 o PBKDF2) i el mantenen actualitzat per tu. El xifratge del transport, en canvi, no sol ser feina seva, sinó del servidor o del proxy que tingui al davant. I la tercera, que l’autorització és el punt on la lògica de negoci i la seguretat es toquen: tractar-la com a configuració, i no com a codi que es prova, és el camí curt cap a un forat.

Llenguatge i framework

Rarament es tria un framework aïllat: es tria un ecosistema. I d’aquestes dues decisions, la del llenguatge és molt més cara de desfer. Migrar d’Express a NestJS és una feina acotada; migrar de Node a Java és canviar de projecte. A la pràctica, doncs, l’ordre real acostuma a ser el contrari del que sembla: primer mires quin llenguatge sap i pot mantenir el teu equip, i això ja et deixa un grapat de frameworks per triar.

Val la pena veure fins a quin punt el llenguatge condiciona el que hem explicat:

  • El sistema de tipus decideix quines comprovacions et surten gratis. A Arquitectura dèiem que el tipatge fort és una funció d’aptitud, perquè detecta els canvis de signatura que afecten molts punts alhora. Java, C# i TypeScript t’ho donen; Python, PHP i Ruby no, i per això hi han anat afegint anotacions de tipus i comprovadors externs. FastAPI és el cas extrem d’aprofitar-ho: fa servir els tipus de Python com a contracte executable.
  • Com es declaren les interfícies decideix què costa un port. A Java i C# has de declarar la interfície explícitament. A Go les interfícies se satisfan estructuralment i es declaren allà on es consumeixen, de manera que un port de sortida es defineix a la capa que el necessita, que és exactament el que demana la regla de dependència. A Python o Ruby n’hi ha prou que l’objecte respongui als mètodes, cosa que fa els ports barats i alhora invisibles: com que no hi ha res escrit, la frontera només existeix si l’equip la respecta.
  • Les anotacions i decoradors expliquen per què tants frameworks s’assemblen. Les anotacions de Java, els atributs de C# i els decoradors de Python i TypeScript permeten declarar comportament damunt d’una classe o una funció, i és el que fa possible l’estil de Spring, NestJS i FastAPI. On no hi ha aquesta eina, com a Go o a Express, el registre és explícit i el codi és més llarg però també més fàcil de seguir.
  • El model d’execució ve sovint del runtime, no del framework. És el que vèiem al model d’execució: el bucle d’esdeveniments de Node o els processos lleugers d’Elixir són propietats de la plataforma que qualsevol framework de sobre hereta.
  • PHP mereix un comentari a part. Tradicionalment no manté cap procés entre peticions: cada petició arrenca l’aplicació sencera i, en acabar, no en sobreviu res. Això explica per què Laravel i Symfony estan pensats per arrencar de pressa i per què l’estat compartit va sempre a un servei extern. Avui hi ha maneres de mantenir el procés viu, com Laravel Octane, RoadRunner o FrankenPHP, però el disseny per defecte encara ve d’aquella herència. És, curiosament, el mateix model que hem vist al serverless.

El que el llenguatge no decideix ja ho hem trobat dues vegades: Express i NestJS comparteixen llenguatge i estan a extrems oposats de l’eix d’opinió, i Laravel i Symfony comparteixen llenguatge i prenen decisions oposades sobre el mapatge. El llenguatge acota el terreny; dins del terreny, la decisió continua sent teva.

Framework per framework

Cada framework posa un nom diferent a les mateixes coses, i aquest vocabulari és bona part del que fa que projectes que s’assemblen molt ho semblin poc. Aquestes taules serveixen de traducció i, on la decisió no és només de nom, diuen també què fa cadascun per defecte.

Per on entra la petició

FrameworkLlenguatgePunt d’entradaEncaminamentTemps real
DjangoPythonGestor WSGI o ASGIURLconf (urls.py)Channels, amb ASGI
RailsRubyconfig.ru (Rack)routes.rbAction Cable
SpringJavaDispatcherServlet@RequestMappingWebSocket amb STOMP
LaravelPHPpublic/index.phpFitxers de rutesReverb o Pusher
SymfonyPHPpublic/index.phpAtributs o routes.yamlMercure (SSE)
ASP.NET CoreC#Program.csEndpoint routingSignalR
ExpressJavaScriptapp.js o server.jsapp.get i Routerws o Socket.IO
FastAPIPythonObjecte appDecoradors i APIRouterWebSockets natius
NestJSTypeScriptmain.ts@Controller i decoradorsGateways
GinGofunc mainEncaminador de Gincoder/websocket
Next.jsJavaScriptEstructura de directorisPer fitxersServei a part
PhoenixElixirEndpointrouter.exChannels i LiveView

La columna de temps real diu qui porta el canal permanent a dins i qui el delega a un servei a part, que és una decisió de desplegament abans que de codi.

Com s’estructura el codi

FrameworkCapa controladoraContenidor IoCPeces transversalsUnitat modular
DjangoViewsNo en téMiddlewareApps
RailsControllersNo en téMiddleware RackEngines i gems
Spring@ControllerApplicationContextFiltres i interceptorsStarters i mòduls
LaravelControllersService containerMiddlewareService providers
SymfonyControllersContenidor de serveisEvent listenersBundles
ASP.NET CoreControllers o minimal APIsIServiceCollectionMiddlewarePaquets NuGet
ExpressGestors de rutaNo en téMiddlewarePaquets npm
FastAPIFuncions de rutaDepends, per peticióMiddleware i dependènciesAPIRouter i paquets
NestJSControllersContenidor propiMiddleware, guards, interceptors@Module
GinHandlersNo en téMiddlewarePaquets
Next.jsRoute handlersNo en téproxy.tsPaquets npm
PhoenixControllersNo en téPlugsContexts

Dades i vista

FrameworkORM habitualPatró de mapatgeMigracionsVista per defecte
DjangoDjango ORMActive RecordIntegradesDjango Templates
RailsActive RecordActive RecordIntegradesERB, amb Hotwire
SpringJPA amb HibernateData MapperFlyway o LiquibaseThymeleaf
LaravelEloquentActive RecordIntegradesBlade
SymfonyDoctrineData MapperDoctrine MigrationsTwig
ASP.NET CoreEntity Framework CoreData MapperEF Core MigrationsRazor o Blazor
ExpressA triarEl de l’ORM triatLes de l’ORM triatCap, motors opcionals
FastAPIA triar, sovint SQLAlchemyData MapperAlembicCap, JSON
NestJSPrisma, Drizzle o TypeORMEl de l’ORM triatLes de l’ORM triatCap, JSON
GinA triar (GORM, sqlc)El de la llibreriaEina externahtml/template
Next.jsA triar (Prisma, Drizzle)El de l’ORM triatPrisma MigrateReact, integrat
PhoenixEcto, que no és un ORMConsultes explícitesIntegrades a EctoHEEx i LiveView

Als microframeworks el patró de mapatge no el marca el framework sinó l’ORM que triïs, i n’hi ha que et deixen triar a tu. La columna de la vista, per la seva banda, deixa veure a simple cop d’ull quins frameworks esperen servir HTML i quins esperen servir dades. I a Rails el nom de l’ORM és literalment Active Record, cosa que explica per què el patró es coneix tant: Rails el va convertir en la manera per defecte de fer aplicacions web durant una dècada. La columna de migracions es llegeix amb les tres famílies de Migracions de l’esquema.

Seguretat

Els protocols i els algorismes són els mateixos per a tothom, i estan explicats a Tecnologies, protocols i pràctiques; el que canvia és quins parla el framework sense ajuda i què fa servir per defecte.

FrameworkAutenticacióPermisosProtocolsContrasenyes
Djangodjango.contrib.authPermisos i grupsOIDC per paquetsPBKDF2
RailsGenerador d’autenticacióLlibreries (Pundit)OmniAuthbcrypt
SpringSpring Security@PreAuthorizeOAuth2, OIDC i SAMLBCrypt delegat
LaravelFortify i SanctumGates i PoliciesPassport i Socialitebcrypt o Argon2
SymfonySecurityBundleVotersBundles (OAuth, SAML)Argon2id
ASP.NET CoreIdentityPolítiquesOIDC i JWT integratsPBKDF2
ExpressCap (Passport)CapEstratègies de PassportLlibreria
FastAPIAjudes per a OAuth2Per dependènciesOAuth2 i JWT per llibreriaLlibreria
NestJSGuards amb PassportGuards i decoradorsEstratègies de PassportLlibreria
GinCapCapLlibreriesx/crypto/bcrypt
Next.jsLlibreries (Better Auth)Al proxy.tsProveïdors de la llibreriaLlibreria
PhoenixGenerador phx.gen.authAl contextUeberauthbcrypt

Aquesta taula torna a dibuixar l’eix d’opinió amb una sola paraula: allà on hi diu “cap” tens tota la llibertat i tota la feina, mentre que els generadors de Rails i Phoenix són el terme mitjà, perquè et donen el codi fet però te’l deixen al projecte perquè el mantinguis tu.

Cap a la pràctica

Amb això ja tens el vocabulari. Fer-lo servir per llegir un projecte que no has escrit, per triar framework quan el projecte és nou, per deixar les peces preparades per ser substituïdes i per posar ordre quan el framework no te’n posa, és el que veiem a Frameworks a la pràctica.

Referències

Last change: , commit: 8f2429a