El mòdul anterior va tancar el recorregut tècnic i va acabar amb una promesa: construir CicloUrbano sencer, de principi a fi, amb Vite i React Router. Aquesta lliçó és el primer pas, i és el que més gent es salta: abans d'escriure una línia de JSX cal saber què es construeix, per a qui, amb quines dades i amb quines decisions tècniques. Un projecte que comença per npm create vite sense haver respost aquestes preguntes acaba, sense excepció, refactoritzant-se a mig camí.

Faràs tres coses, en aquest ordre. Primer, definir el producte: l'abast en una frase, les dues persones usuàries, les històries amb els seus criteris d'acceptació i —tan important com l'anterior— la llista explícita del que no entra a la primera versió. Segon, fixar les decisions d'arquitectura en una acta que justifiqui cada elecció i anomeni l'alternativa descartada, perquè d'aquí a tres mesos ningú no hagi de reconstruir el raonament. I tercer, muntar el bastiment real: Vite, dependències, scripts, linter, formatador, variables d'entorn, estructura de carpetes, json-server sembrat i un primer commit que ja arrenca.

En acabar tindràs un repositori que es clona, s'instal·la i s'executa, amb l'API de desenvolupament responent i sense ni una sola pantalla escrita. Les pantalles són la lliçó següent.

Contingut

  1. El producte en una frase
  2. Persones usuàries i els seus objectius
  3. Històries d'usuari i criteris d'acceptació
  4. El que queda fora de la primera versió, i per què
  5. Model de dades definitiu
  6. El db.json inicial complet
  7. Mapa de pantalles i arbre de rutes
  8. L'acta de decisions d'arquitectura
  9. Crear el projecte amb Vite
  10. Dependències: què s'instal·la i per què
  11. El package.json complet
  12. Qualitat del codi: ESLint, Prettier i .editorconfig
  13. Variables d'entorn amb import.meta.env
  14. Estructura de carpetes: per tipus o per funcionalitat
  15. L'API de desenvolupament: json-server en marxa
  16. Convencions de l'equip
  17. El pla de treball: increments verticals
  18. El primer commit

  1. El producte en una frase

Si no pots descriure el que construeixes en una frase, encara no saps el que construeixes. La d'aquest projecte:

CicloUrbano és una aplicació web privada que permet a una persona client reservar bicicletes d'una xarxa urbana per estacions, i a una persona operària mantenir l'estat de la flota.

Aquesta frase carrega més informació de la que sembla:

Fragment Què decideix
«aplicació web» No és mòbil (encara que 10-05 va deixar la porta oberta)
«privada» Tot el que és interessant passa després d'identificar-se: no hi ha contingut públic per indexar
«reservar bicicletes» El camí dels diners del producte. Tota la resta el serveix
«per estacions» Les estacions són un eix de navegació, no un adorn
«mantenir l'estat de la flota» Hi ha un segon rol amb permisos diferents

La paraula «privada» és la que decideix l'arquitectura de renderitzat, i ho fa abans que es discuteixi cap eina. Hi tornarem a l'apartat 8.

  1. Persones usuàries i els seus objectius

Dues persones, ni una més. Un producte amb cinc perfils a la primera versió no en té cap ben resolt.

Client Operari
Exemple del domini Ana Ribera (usr-01) Marc Solé (usr-02)
Objectiu principal Aconseguir una bicicleta quan la necessita Que la flota estigui operativa
Freqüència d'ús Ràfegues curtes, diverses vegades per setmana Sessions llargues, cada dia
Context Mòbil, al carrer, amb pressa Escriptori, al taller
Què el frustra No saber si la bicicleta estarà disponible en arribar No saber quina bicicleta porta dies aturada
Què necessita veure primer Bicicletes disponibles a prop Bicicletes en manteniment
Permisos Veure el catàleg, reservar, cancel·lar el que és seu Tot l'anterior més canviar l'estat d'una bicicleta

D'aquest quadre en surten dues conseqüències de disseny que ja no es discuteixen després:

  • La pantalla d'inici és el catàleg, no un tauler de control. El client és qui més hi entra, i hi entra amb pressa.
  • El taller és una pantalla a part amb accés per rol, no un mode ocult del catàleg. Barrejar els dos usos en una pantalla obligaria a amagar controls per permís a cada targeta, que és exactament el tipus de complexitat que es paga durant anys.

  1. Històries d'usuari i criteris d'acceptació

Una història d'usuari té una forma fixa —com a X vull Y per a Z— i només serveix si porta criteris d'acceptació comprovables. «Que el catàleg funcioni bé» no és un criteri; «en filtrar per elèctrica només apareixen les de tipus electrica» sí que ho és. A 11-04 cadascun d'aquests criteris es convertirà en una prova, i per això estan redactats així des d'avui.

Imprescindibles

ID Història Criteris d'acceptació
H1 Com a client vull veure el catàleg de bicicletes per saber què hi ha disponible Es llisten les 5 bicicletes amb model, tipus, estat, estació i preu/hora · Mentre carreguen es veu un esquelet, no un text · Si l'API falla, hi ha missatge d'error i botó de reintent
H2 Com a client vull filtrar per tipus i cercar per model per trobar ràpid el que necessito El filtre viu a la URL (?tipo=electrica) i és compartible per enllaç · La cerca per text és local i amb retard · Filtre i cerca es combinen
H3 Com a client vull veure la fitxa d'una bicicleta per decidir si la reservo Ruta /bicicletas/:bicicletaId · Mostra estació, preu i estat · Un identificador inexistent dona un 404 propi, no una pantalla en blanc
H4 Com a persona usuària vull identificar-me per accedir al que és meu Formulari amb validació accessible · En entrar es torna a la pantalla d'origen, no a l'inici · La sessió sobreviu a una recàrrega
H5 Com a client vull reservar una bicicleta disponible per usar-la Formulari amb bicicleta, inici, hores i condicions · No es pot reservar una que no estigui disponible · En crear-la, avís d'èxit i redirecció a /reservas · El catàleg reflecteix el canvi
H6 Com a client vull veure i cancel·lar les meves reserves per gestionar els meus plans /reservas llista només les de l'usuari identificat · Cancel·lar demana confirmació · El canvi es veu a l'instant i es confirma contra el servidor
H7 Com a operari vull canviar l'estat d'una bicicleta per retirar-la o tornar-la al servei /taller només accessible amb rol operario · Un client que hi entri per URL veu «sense permisos» · El canvi es reflecteix al catàleg públic
H8 Com a persona usuària vull veure les estacions i la seva flota per orientar-me per la ciutat /estaciones amb les 3 estacions · /estaciones/:estacionId amb pestanyes de flota i incidències · La pestanya activa és a la URL

Desitjables

ID Història Per què és desitjable i no imprescindible
D1 Tema clar/fosc amb preferència recordada Millora real, però ningú deixa de reservar per no tenir-lo
D2 Avisos temporals d'èxit i error a tota l'aplicació Es pot començar amb missatges en línia; els avisos globals poleixen l'experiència
D3 Avís en perdre la connexió Només importa al mòbil al carrer; el cas es degrada de manera acceptable sense ell
D4 Resum de flota per estat al catàleg Informatiu. Útil per a l'operari, prescindible per al client

La distinció no és cosmètica: si el termini s'escurça, es talla per D1-D4 sense tocar H1-H8. Tenir aquesta línia traçada abans de començar és el que impedeix que un projecte entregui deu coses a mitges en lloc de vuit acabades.

  1. El que queda fora de la primera versió, i per què

Aquesta llista és tan important com l'anterior, i convé escriure-la i compartir-la, perquè el que no està escrit s'acaba demanant a mig projecte.

Fora d'abast Motiu
Pagaments reals Requereix passarel·la, compliment normatiu i auditoria. El preu es mostra i es calcula; no es cobra
Registre d'usuaris i recuperació de contrasenya Els usuaris es sembren a db.json. La identificació real és del servidor (11-05)
Mapa geogràfic d'estacions Afegeix una biblioteca pesant i una clau de servei. Es llisten per barri
Notificacions push Exigeix servidor, permisos i service worker. Fora
Tauler d'estadístiques Cap de les dues persones usuàries l'ha demanat per al seu objectiu principal
Internacionalització L'aplicació és d'una ciutat i un idioma. S'anota com a full de ruta a 11-05
Mode sense connexió Complexitat alta, valor incert fins a tenir usuaris reals
Aparador públic amb SEO És contingut públic: si algun dia es fa, es fa amb Next.js (10-02), com a projecte a part

Fixa't en l'última fila. Decidir que alguna cosa està fora no és decidir que mai es farà: és decidir que no competeix pel temps d'aquesta versió, i de passada deixar anotat com es faria si arriba el moment.

  1. Model de dades definitiu

Quatre entitats. Les coneixes de tot el curs; aquí queden fixades amb les seves relacions.

erDiagram
    ESTACIO ||--o{ BICICLETA : "allotja"
    BICICLETA ||--o{ RESERVA : "es objecte de"
    USUARI ||--o{ RESERVA : "realitza"

    ESTACIO {
        string id PK "est-01"
        string nom "Plaça Major"
        string barri "Centre"
        number places "20"
    }
    BICICLETA {
        string id PK "bici-001"
        string model "Urbana Clàssica"
        string tipus "urbana | electrica | carga"
        string estat "disponible | alquilada | mantenimiento"
        string estacioId FK "est-01"
        number preuHora "2.5"
    }
    USUARI {
        string id PK "usr-01"
        string nom "Ana Ribera"
        string email "[email protected]"
        string rol "cliente | operario"
    }
    RESERVA {
        string id PK "res-01"
        string bicicletaId FK "bici-002"
        string usuari FK "usr-01"
        string dataInici "2026-05-04T09:00"
        number hores "2"
        string estat "activa | confirmada | cancelada"
    }

Tres decisions del model que convé entendre abans de codificar:

  1. Les relacions es guarden per identificador, mai imbricant l'objecte sencer. Una reserva guarda bicicletaId, no una còpia de la bicicleta. Si s'imbriqués, el preu de la reserva quedaria congelat en el moment de crear-la i qualsevol canvi a la bicicleta deixaria còpies desincronitzades per tota la base de dades. És la mateixa normalització que es va aplicar a l'estat de Redux a 07-04, i pel mateix motiu.
  2. L'estat és una cadena d'un conjunt tancat, no un booleà. disponible | alquilada | mantenimiento admet un quart estat el dia que calgui; disponible: true/false obliga a afegir un altre booleà i a raonar sobre combinacions impossibles.
  3. El camp de la reserva es diu usuari i conté un identificador. És una incoherència de nom respecte a bicicletaId, i es conserva perquè així es va fixar a tot el curs: canviar-ho ara trencaria validarReserva, les proves i els gestors de MSW. S'anota com a deute tècnic menor a l'acta, que és exactament el que es fa en un projecte real amb una incoherència innòcua ja estesa.

  1. El db.json inicial complet

Aquest fitxer és la base de dades de desenvolupament i, alhora, la llavor que 11-04 restaurarà abans de cada prova d'extrem a extrem. Va a l'arrel del repositori.

{
  "bicicletas": [
    { "id": "bici-001", "model": "Urbana Clàssica", "tipus": "urbana", "estat": "disponible", "estacioId": "est-01", "preuHora": 2.5 },
    { "id": "bici-002", "model": "Elèctrica Pro", "tipus": "electrica", "estat": "alquilada", "estacioId": "est-01", "preuHora": 4.0 },
    { "id": "bici-003", "model": "Càrrega Max", "tipus": "carga", "estat": "mantenimiento", "estacioId": "est-02", "preuHora": 5.5 },
    { "id": "bici-004", "model": "Urbana Clàssica", "tipus": "urbana", "estat": "disponible", "estacioId": "est-03", "preuHora": 2.5 },
    { "id": "bici-005", "model": "Elèctrica Pro", "tipus": "electrica", "estat": "disponible", "estacioId": "est-02", "preuHora": 4.0 }
  ],
  "estaciones": [
    { "id": "est-01", "nom": "Plaça Major", "barri": "Centre", "places": 20 },
    { "id": "est-02", "nom": "Parc Nord", "barri": "Nord", "places": 15 },
    { "id": "est-03", "nom": "Estació Central", "barri": "Eixample", "places": 30 }
  ],
  "usuarios": [
    { "id": "usr-01", "nom": "Ana Ribera", "email": "[email protected]", "rol": "cliente" },
    { "id": "usr-02", "nom": "Marc Solé", "email": "[email protected]", "rol": "operario" }
  ],
  "reservas": [
    { "id": "res-01", "bicicletaId": "bici-002", "usuari": "usr-01", "dataInici": "2026-05-04T09:00", "hores": 2, "estat": "activa" }
  ]
}

El conjunt està triat perquè cada estat i cada cas límit tingui almenys un representant, que és la propietat que ha de complir qualsevol joc de dades de desenvolupament:

Cas que cal poder provar Dada que el cobreix
Bicicleta reservable bici-001, bici-004, bici-005
Bicicleta no reservable per estar llogada bici-002
Bicicleta no reservable per manteniment bici-003
Dues bicicletes amb el mateix model (claus de llista) bici-001 i bici-004
Estació amb diverses bicicletes est-01 i est-02
Estació sense bicicletes disponibles est-01 té una llogada; est-02, una en manteniment
Usuari amb reserves usr-01
Usuari sense cap reserva (estat buit) usr-02
Els tres tipus de bicicleta urbana, elèctrica i de càrrega

Aquest penúltim cas és el que més s'oblida i el que més errors produeix: si mai no veus la llista buida en desenvolupament, no la dissenyes, i l'usuari que estrena l'aplicació es troba un buit en blanc.

  1. Mapa de pantalles i arbre de rutes

Les rutes van quedar fixades al mòdul 6. Aquí es comprova que cadascuna serveix una història i que cap història es queda sense pantalla.

flowchart TD
    RAIZ["/ · Disseny (Outlet)"]
    RAIZ --> IDX["index · PaginaCataleg · H1 H2"]
    RAIZ --> BICI["bicicletas/:bicicletaId · PaginaFitxaBicicleta · H3"]
    RAIZ --> EST["estaciones"]
    EST --> ESTI["index · PaginaEstacions · H8"]
    EST --> DET[":estacionId · PaginaDetallEstacio · H8"]
    DET --> FLO["index · PestanyaFlota"]
    DET --> INC["incidencias · PestanyaIncidencies"]
    RAIZ --> ACC["acceso · PaginaAcces · H4"]
    RAIZ --> PROT["🔒 RutaProtegida (sense path)"]
    PROT --> RES["reservas"]
    RES --> RESI["index · PaginaReserves · H6"]
    RES --> NUE["nueva · PaginaNovaReserva · H5"]
    PROT --> ROL["🔒 RequereixRol operario (sense path)"]
    ROL --> TAL["taller · PaginaTaller · H7"]
    RAIZ --> NF["* · PaginaNoTrobada"]
    style PROT fill:#fde68a
    style ROL fill:#fed7aa
    style TAL fill:#fecaca

I la taula de cobertura, que és la que es revisa per detectar buits:

Ruta Pantalla Història Accés
/ PaginaCataleg H1, H2 Públic
/bicicletas/:bicicletaId PaginaFitxaBicicleta H3 Públic
/estaciones PaginaEstacions H8 Públic
/estaciones/:estacionId PaginaDetallEstacio (+ pestanyes) H8 Públic
/acceso PaginaAcces H4 Públic
/reservas PaginaReserves H6 Identificat
/reservas/nueva PaginaNovaReserva H5 Identificat
/taller PaginaTaller H7 Rol operario
* PaginaNoTrobada Públic
(error de ruta) PaginaErrorRuta via errorElement
(sense permisos) PaginaSensePermisos H7

Un detall que canvia respecte al mòdul 6: /reservas entra dins de la branca protegida. En el seu moment es va deixar pública per no complicar els exemples; al projecte real, veure reserves exigeix sessió, perquè són dades personals. És el tipus d'ajust que apareix just en fer aquesta taula, i per això es fa la taula.

  1. L'acta de decisions d'arquitectura

Aquest és el document més valuós de la lliçó. Cada fila registra què es decideix, per què, i què es descarta. Es desa al repositori com a DECISIONS.md i s'actualitza quan alguna cosa canvia; mai no s'esborra una fila, s'afegeix la nova amb la seva data.

# Decisió Per què Alternativa descartada i motiu
A1 Vite + React Router, aplicació de client L'aplicació és privada, interactiva i després d'identificar-se: no hi ha SEO a guanyar i el primer pintat no és un factor de negoci. Arrencada instantània del servidor de desenvolupament i desplegament com a estàtics Next.js: aporta SSR/SSG que aquí no s'aprofiten, i afegeix un servidor a mantenir. Criteri aplicat a 10-02
A2 React Router v7 en mode de dades (createBrowserRouter + RouterProvider) Rutes imbricades, errorElement per branca i càrrega mandrosa declarativa BrowserRouter amb <Routes>: sense errorElement ni API de dades. Descartat a 06-01
A3 TanStack Query per a l'estat del servidor Memòria cau, deduplicació, revalidació, estats de càrrega i error i invalidació després de mutar: tot el que caldria escriure a mà Redux per a tot: obliga a reimplementar memòria cau i cicle de vida (07-06). useEffect + fetch: sense memòria cau ni deduplicació
A4 Redux Toolkit per a l'estat del client compartit (sessió, catàleg) Selecció granular, DevTools amb viatge en el temps, reductors purs fàcils de provar Context per a tot: repinta tots els consumidors davant de qualsevol canvi (07-02)
A5 Context per a tema i avisos Canvien poc i els necessita tot l'arbre. És exactament el seu cas d'ús Redux: vàlid, però afegeix cerimònia a dues dades trivials
A6 La URL per al filtre per tipus Un catàleg filtrat ha de poder-se compartir per enllaç i sobreviure a una recàrrega Estat local o Redux: es perd en recarregar i no és compartible
A7 CSS Modules Àmbit local sense dependències, sense cost en execució, suportat per Vite de sèrie Tailwind: excel·lent, però afegeix configuració i una corba pròpia. CSS-in-JS: cost en execució i fricció amb RSC (10-03)
A8 Vitest + Testing Library + MSW + Cypress Vitest comparteix configuració amb Vite; el trofeu de proves de 09-01 aplicat tal qual Jest: exigeix transformador propi i duplicar la configuració. Sense e2e: deixa fora enrutament real, CSS i persistència
A9 JavaScript, amb migració a TypeScript prevista L'equip el domina i la primera versió s'ha d'entregar. L'estructura ja és compatible: tipus en un fitxer, validació centralitzada TypeScript des del dia u: millor a mitjà termini, però frena l'arrencada. Es planifica a 11-05 amb el que s'ha vist a 10-04
A10 json-server només en desenvolupament Dona una API REST completa sobre un JSON en un minut API pròpia: fora d'abast. Es documenta a 11-05 què caldria de veritat

I l'assignació d'estat, que és l'aplicació literal de la taxonomia de 07-01 a aquest projecte:

Tipus d'estat Exemple a CicloUrbano Eina Per què
Del servidor Bicicletes, estacions, reserves, usuaris TanStack Query És una còpia local d'alguna cosa remota; necessita memòria cau i revalidació
De client compartit Usuari identificat, terme de cerca, ordre Redux Toolkit Diversos components llunyans el llegeixen i l'escriuen
D'interfície global Tema, avisos Context Poc canvi, molts consumidors
D'URL ?tipo=electrica, pestanya activa React Router Ha de ser compartible i navegable
Local Modal obert, esborrany del formulari useState Ningú més ho necessita

  1. Crear el projecte amb Vite

Amb l'acta signada, el bastiment és mecànic.

npm create vite@latest ciclourbano -- --template react
cd ciclourbano
npm install
npm run dev

Què ha fet cada línia:

  • npm create vite@latest descarrega i executa el generador oficial. El -- separa els arguments de npm dels del generador; sense ell, npm intentaria interpretar --template com a seu.
  • --template react tria la plantilla de React amb JavaScript. Existeix react-ts per a TypeScript i react-swc per usar SWC en lloc de Babel; amb React 19 i el complement oficial, la diferència de velocitat en un projecte d'aquesta mida és irrellevant.
  • npm run dev aixeca el servidor de desenvolupament a http://localhost:5173.

Comprova la versió de React abans de seguir, perquè el projecte assumeix React 19:

npm ls react
# [email protected]
# └── [email protected]

  1. Dependències: què s'instal·la i per què

Res no s'instal·la «perquè sí». Cada paquet respon a una fila de l'acta.

# Producció
npm install react-router @reduxjs/toolkit react-redux @tanstack/react-query

# Desenvolupament
npm install -D @tanstack/react-query-devtools
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D msw cypress start-server-and-test
npm install -D eslint-plugin-jsx-a11y prettier eslint-config-prettier
npm install -D json-server npm-run-all
Paquet On Per a què Decisió
react-router Producció Enrutament al client (v7, paquet únic) A2
@reduxjs/toolkit Producció createSlice, configureStore, Immer inclòs A4
react-redux Producció useSelector, useDispatch, Provider A4
@tanstack/react-query Producció Estat del servidor amb memòria cau A3
@tanstack/react-query-devtools Desenvolupament Inspector de la memòria cau. No entra al paquet de producció A3
vitest + jsdom Desenvolupament Executor de proves i DOM simulat A8
@testing-library/* Desenvolupament Renderitzat, asercions i interacció realista A8
msw Desenvolupament Simulació de la xarxa a nivell de petició A8
cypress Desenvolupament Proves d'extrem a extrem en navegador real A8
start-server-and-test Desenvolupament Espera que Vite i l'API responguin abans de llançar Cypress A8
eslint-plugin-jsx-a11y Desenvolupament Regles d'accessibilitat al linter Accessibilitat com a requisit
prettier + eslint-config-prettier Desenvolupament Formatat automàtic sense barallar-se amb ESLint Convencions
json-server Desenvolupament API REST de desenvolupament A10
npm-run-all Desenvolupament Executar Vite i l'API en paral·lel amb una sola comanda Comoditat

La distinció entre dependencies i devDependencies no és burocràcia: el que hi ha a dependencies pot acabar al paquet que es descarrega l'usuari. Posar json-server o Cypress allà no trencaria el build de Vite, però sí que engreixa qualsevol instal·lació de producció i dona un senyal fals sobre el que l'aplicació necessita per funcionar.

  1. El package.json complet

{
  "name": "ciclourbano",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "api": "json-server --watch db.json --port 3001",
    "dev:todo": "npm-run-all --parallel dev api",
    "lint": "eslint . --max-warnings 0",
    "formato": "prettier --write \"src/**/*.{js,jsx,css,json}\"",
    "formato:comprobar": "prettier --check \"src/**/*.{js,jsx,css,json}\"",
    "test": "vitest",
    "test:ejecutar": "vitest run",
    "cobertura": "vitest run --coverage",
    "cy:abrir": "cypress open",
    "cy:ejecutar": "cypress run",
    "e2e": "start-server-and-test dev:todo \"http://localhost:5173|http://localhost:3001/bicicletas\" cy:ejecutar",
    "pruebas:todas": "npm-run-all lint test:ejecutar e2e"
  },
  "dependencies": {
    "@reduxjs/toolkit": "^2.5.0",
    "@tanstack/react-query": "^5.62.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-redux": "^9.2.0",
    "react-router": "^7.1.0"
  },
  "devDependencies": {
    "@tanstack/react-query-devtools": "^5.62.0",
    "@testing-library/jest-dom": "^6.6.0",
    "@testing-library/react": "^16.1.0",
    "@testing-library/user-event": "^14.5.0",
    "@vitejs/plugin-react": "^4.3.0",
    "@vitest/coverage-v8": "^2.1.0",
    "cypress": "^13.17.0",
    "eslint": "^9.17.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-jsx-a11y": "^6.10.0",
    "eslint-plugin-react-hooks": "^5.1.0",
    "eslint-plugin-react-refresh": "^0.4.0",
    "jsdom": "^25.0.0",
    "json-server": "^0.17.4",
    "msw": "^2.7.0",
    "npm-run-all": "^4.1.5",
    "prettier": "^3.4.0",
    "start-server-and-test": "^2.0.0",
    "vite": "^6.0.0",
    "vitest": "^2.1.0"
  }
}

Els scripts es mereixen un comentari, perquè són la interfície del projecte per a qualsevol que arribi nou:

Script Què fa Quan s'usa
dev Servidor de Vite al 5173 Cada dia
api json-server al 3001 vigilant db.json Cada dia, en un altre terminal
dev:todo Els dos en paral·lel La comanda de treball habitual
build Construeix dist/ per a producció Abans de desplegar (11-05)
preview Serveix dist/ per comprovar-la Després de build
lint ESLint amb zero avisos tolerats A cada commit i a CI
formato Prettier reescriu els fitxers En desar o abans de commit
formato:comprobar Prettier només comprova, sense escriure A CI
test Vitest en mode vigilància Mentre es programa
test:ejecutar Vitest una vegada i surt A CI
cobertura Informe de cobertura Revisions periòdiques
e2e Aixeca tot, espera i llança Cypress A CI i abans d'una entrega
pruebas:todas Linter + unitàries + e2e, en aquest ordre La porta de qualitat completa

El --max-warnings 0 de lint és deliberat: un avís que no trenca res s'acumula fins que n'hi ha cent vint i ningú els mira. O importen i fallen, o es desactiva la regla.

  1. Qualitat del codi: ESLint, Prettier i .editorconfig

Vite genera un eslint.config.js bàsic. Aquest és el del projecte, amb les dues addicions que exigeix l'acta: accessibilitat i regles de hooks.

// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-config-prettier';

export default [
  { ignores: ['dist', 'coverage', 'cypress/videos', 'cypress/screenshots'] },
  {
    files: ['**/*.{js,jsx}'],
    languageOptions: {
      ecmaVersion: 2022,
      globals: globals.browser,
      parserOptions: {
        ecmaFeatures: { jsx: true },
        sourceType: 'module'
      }
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
      'jsx-a11y': jsxA11y
    },
    rules: {
      ...js.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,

      // Un import no utilitzat és soroll; una variable d'error sí que pot quedar-se
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],

      // Vite necessita que cada mòdul exporti només components per al refresc ràpid
      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],

      // Elevades a error: són les errades que el mòdul 5 i el 3 van costar més d'explicar
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'error',
      'jsx-a11y/label-has-associated-control': 'error',
      'jsx-a11y/no-autofocus': 'warn'
    }
  },
  prettier   // SEMPRE l'últim: desactiva les regles d'estil que xoquen amb Prettier
];

Tres punts que cal entendre, no copiar:

  • react-hooks/exhaustive-deps com a error, no com a warn. És la decisió més discutida de qualsevol configuració de React i aquí es pren a consciència: a 05-02 es va veure que una dependència omesa produeix dades obsoletes que no fallen en desenvolupament i sí en producció. Com a avís, s'ignora; com a error, obliga a resoldre'l o a justificar l'excepció amb un comentari eslint-disable-next-line que queda visible a la revisió de codi.
  • jsx-a11y en mode recomanat. Caça a l'editor la imatge sense alt, l'onClick sobre un div sense rol ni teclat, i l'etiqueta sense control associat. No substitueix la revisió manual de 03-06, però elimina el 80 % dels errors habituals abans que existeixin.
  • prettier va l'últim de l'array. eslint-config-prettier no afegeix regles: desactiva les d'ESLint que xoquen amb el formatat. Si es posés abans, les regles posteriors les tornarien a activar i tindries el linter i el formatador barallant-se a cada desada.
// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none",
  "arrowParens": "always"
}
# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

.editorconfig és el que evita el clàssic «el fitxer sencer apareix modificat al diff» quan algú treballa amb Windows: fixa els finals de línia i la indentació a nivell d'editor, abans que Prettier hi intervingui.

# .gitignore
node_modules
dist
dist-ssr
coverage
*.local

# Entorn: s'ignoren els .env reals, es versiona l'exemple
.env
.env.*
!.env.example

# Cypress
cypress/videos
cypress/screenshots
cypress/downloads

# Editor i sistema
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.log

  1. Variables d'entorn amb import.meta.env

La URL de l'API no pot estar escrita a mà en quinze fitxers. Vite exposa les variables d'entorn a través d'import.meta.env, amb una regla estricta: només les que comencen per VITE_ arriben al codi del client.

# .env  (NO es versiona)
VITE_URL_API=http://localhost:3001
VITE_NOM_APP=CicloUrbano
# .env.example  (SÍ es versiona: és la plantilla)
# Copia aquest fitxer a .env i ajusta els valors.
# URL base de l'API REST de desenvolupament (json-server)
VITE_URL_API=http://localhost:3001
# Nom visible de l'aplicació
VITE_NOM_APP=CicloUrbano
// src/configuracio.js
export const URL_API = import.meta.env.VITE_URL_API ?? 'http://localhost:3001';
export const NOM_APP = import.meta.env.VITE_NOM_APP ?? 'CicloUrbano';
export const ES_DESENVOLUPAMENT = import.meta.env.DEV;   // booleà que Vite injecta sempre

Per què un mòdul configuracio.js en lloc de llegir import.meta.env allà on calgui:

  1. Un únic punt de veritat. El dia que la variable canviï de nom, es toca un fitxer.
  2. Valors per defecte en un sol lloc, amb ??, perquè clonar el repositori i oblidar-se del .env no trenqui l'arrencada.
  3. Es pot substituir a les proves amb un vi.mock d'un mòdul propi; amb import.meta.env escampat, no.

I l'avís que es repetirà a 11-05 amb totes les lletres: import.meta.env és text que s'incrusta al JavaScript que es descarrega el navegador. Qualsevol persona pot llegir-lo amb dos clics. Una clau d'API privada, una contrasenya o un testimoni d'administració no hi van mai, ni tan sols «temporalment mentre provem».

  1. Estructura de carpetes: per tipus o per funcionalitat

Hi ha dues escoles, i totes dues tenen raó en el seu context:

Per tipus (components/, hooks/, pagines/) Per funcionalitat (reserves/, cataleg/, sessio/)
Trobar un component pel seu nom Immediat Cal saber a quina funcionalitat pertany
Treballar en una funcionalitat completa Saltes entre quatre carpetes Tot junt
Esborrar una funcionalitat sencera Cal caçar fitxers per tot arreu S'esborra la carpeta
Projecte petit (< 30 fitxers) Còmode Sobredimensionat
Projecte gran o amb diversos equips Carpetes de 60 fitxers Escala millor
Risc típic Un components/ ingovernable Discutir a quina funcionalitat pertany cada cosa

CicloUrbano adopta un híbrid, que és el que fa la majoria de projectes reals d'aquesta mida: per tipus el que és compartit, per funcionalitat l'estat de domini. Aquesta última part ja venia decidida des de 07-05.

ciclourbano/
├── cypress/
│   ├── e2e/                    # acces.cy.js, reservar.cy.js, cancellar.cy.js
│   └── support/                # comandos.js: cy.perTestId, cy.sembrarDades, cy.accedirCom
├── public/                     # fitxers servits tal com són (favicon, robots.txt)
├── src/
│   ├── api/                    # client.js i funcions per recurs. NO sap res de React
│   ├── magatzem/                # magatzem.js: configureStore i la seva composició
│   ├── components/              # reutilitzables, sense ruta pròpia
│   │   └── base/                # sistema de disseny: Boto, Camp, Panell, Etiqueta, Modal
│   ├── consultes/                # clientConsultes.js, claus.js i els hooks de TanStack Query
│   ├── contextos/                # ProveidorTema, contextos d'avisos, Proveidors.jsx
│   ├── dades/                     # domini.js: dades d'exemple mentre no hi ha xarxa (11-02)
│   ├── funcionalitats/            # estat de client organitzat per domini
│   │   ├── cataleg/               # sliceCataleg.js i els seus selectors
│   │   ├── reserves/               # sliceReserves.js i els seus selectors
│   │   └── sessio/                 # sliceSessio.js i els seus selectors
│   ├── hooks/                      # useAlternar, useMagatzemLocal, useDebounce, useEsdevenimentTeclat…
│   ├── pagines/                    # una per ruta: PaginaCataleg, PaginaReserves…
│   ├── proves/                     # configuracio.js, utilitats.jsx, gestors.js, servidor.js
│   ├── utilitats/                  # classes.js, validarReserva.js, monitoritzacio.js, disponibilitat.js
│   ├── configuracio.js             # lectura única d'import.meta.env
│   ├── rutes.jsx                   # createBrowserRouter: el mapa complet
│   ├── index.css                   # reinici + variables :root + tema fosc
│   └── main.jsx                    # composició de proveïdors
├── .editorconfig
├── .env.example
├── .gitignore
├── .prettierrc
├── DECISIONS.md                    # l'acta de l'apartat 8
├── db.json                         # base de dades de desenvolupament i llavor de les proves
├── eslint.config.js
├── package.json
├── README.md
└── vite.config.js

Dues regles de col·locació que eviten la majoria de les discussions:

  • Un fitxer de prova viu al costat del codi que prova (TargetaBicicleta.jsx i TargetaBicicleta.test.jsx a la mateixa carpeta). Només la infraestructura de proves és a src/proves/. Així, en esborrar un component, la seva prova se'n va amb ell.
  • Un component puja a components/ quan l'usa la segona pantalla, no abans. Fins llavors viu al costat de la seva pàgina. Generalitzar amb un sol cas d'ús produeix abstraccions equivocades.

  1. L'API de desenvolupament: json-server en marxa

npm run api
  \{^_^}/ hi!
  Loading db.json
  Done

  Resources
  http://localhost:3001/bicicletas
  http://localhost:3001/estaciones
  http://localhost:3001/usuarios
  http://localhost:3001/reservas

Comprovació obligatòria abans de seguir. Si això no respon, cap pantalla d'11-03 funcionarà i perdràs una hora buscant l'error a React:

# Llista completa
curl http://localhost:3001/bicicletas

# Filtre per camp: json-server el dona gratis
curl "http://localhost:3001/bicicletas?tipo=electrica"

# Un recurs concret
curl http://localhost:3001/bicicletas/bici-003

# Crear (el que farà H5)
curl -X POST http://localhost:3001/reservas \
  -H "Content-Type: application/json" \
  -d '{"id":"res-99","bicicletaId":"bici-001","usuari":"usr-01","dataInici":"2026-06-01T10:00","hores":2,"estat":"activa"}'

# I desfer l'experiment
curl -X DELETE http://localhost:3001/reservas/res-99

El que json-server dona de sèrie i el projecte aprofitarà:

Capacitat Exemple S'usa a
Llistat GET /bicicletas H1
Detall GET /bicicletas/bici-001 H3
Filtre per camp GET /bicicletas?tipo=urbana H2
Filtre per relació GET /reservas?usuari=usr-01 H6
Creació POST /reservas H5
Modificació parcial PATCH /bicicletas/bici-001 H7
404 real GET /bicicletas/no-existeix H3

I el que no dona, i cal tenir present des d'ara: no valida res, no autentica ningú i no comprova permisos. Un PATCH des de la consola del navegador canvia l'estat de qualsevol bicicleta sense sessió. És acceptable en desenvolupament i és la raó per la qual 11-05 insisteix que l'autorització de veritat es comprova sempre al servidor.

  1. Convencions de l'equip

Escrites al README.md, perquè una convenció que només és al cap de qui la va inventar no és una convenció.

Noms

Element Convenció Exemple
Component PascalCase, fitxer .jsx igual que el component TargetaBicicleta.jsx
Pàgina Prefix Pagina PaginaNovaReserva.jsx
Hook use + camelCase useMagatzemLocal.js
Utilitat camelCase, exportació amb nom validarReserva.js
Estils Component.module.css al costat del component TargetaBicicleta.module.css
Gestor intern gestionarX gestionarEnviar
Prop de callback alX alSeleccionar
Acció de Redux Substantiu en passat reservaConfirmada
Selector seleccionarX seleccionarUsuari
Fitxers Sense accents ni ç Capcalera.jsx, no Capçalera.jsx

Exportacions: export default per a components i pàgines; exportació amb nom per a hooks, utilitats, slices i selectors. Barrejar els dos criteris en el mateix tipus de fitxer és el que produeix importacions inconsistents.

Ordre d'importacions, sempre el mateix, amb línia en blanc entre grups:

// 1. React i biblioteques externes
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { useSelector } from 'react-redux';

// 2. Mòduls propis, del més general al més concret
import { useCrearReserva } from '../consultes/reservas.js';
import { validarReserva } from '../utilitats/validarReserva.js';
import FormulariReserva from '../components/FormulariReserva.jsx';

// 3. Estils, sempre al final
import estils from './PaginaNovaReserva.module.css';

Missatges de commit, en format convencional, en català i en imperatiu:

feat(cataleg): filtrar bicicletes per tipus des de la URL
fix(reserves): impedir reservar una bicicleta en manteniment
test(formulari): cobrir els missatges de validació accessibles
refactor(api): extreure l'embolcall de fetch a src/api/client.js
docs(readme): documentar l'arrencada amb dev:todo
chore(deps): actualitzar vitest a la 2.1

El prefix no és decoració: permet generar el registre de canvis automàticament i, sobretot, obliga que un commit faci una sola cosa. Si dubtes entre feat i fix, probablement el commit contingui dos canvis i calgui partir-lo.

  1. El pla de treball: increments verticals

Aquí hi ha la decisió de mètode que més influeix en com se sent el projecte mentre es construeix.

Per capes (horitzontal) Per increments verticals
Ordre de treball Tots els components → tota l'API → totes les proves Una història completa de punta a punta, després la següent
Primera demo possible Al final En acabar la primera història
Risc detectat Tard, quan tot està escrit Aviat, a la primera integració
Sensació d'avenç Nul·la durant setmanes Contínua
Risc real Descobrir a l'última setmana que el model no encaixa Refactoritzar alguna cosa del que s'ha fet en arribar la tercera història

Es tria vertical, amb una salvetat honesta: aquest mòdul està organitzat per capes —interfície, estat, proves, desplegament— perquè explicar requereix agrupar conceptes, mentre que construir requereix entregar valor aviat. És una diferència important i convé tenir-la clara: l'ordre del llibre no és l'ordre del taller.

Tot i així, dins de cada lliçó es treballa per històries completes. I si estiguessis fent aquest projecte en un equip real, l'ordre seria aquest:

flowchart LR
    subgraph L1["11-01 · Bastiment"]
        A1["Producte i acta"] --> A2["Vite, linter, entorn"] --> A3["db.json + API viva"]
    end
    subgraph L2["11-02 · Interfície"]
        B1["Sistema de disseny"] --> B2["Disseny + rutes"] --> B3["Pantalles amb dades estàtiques"]
    end
    subgraph L3["11-03 · Estat i API"]
        C1["Capa de dades"] --> C2["Query + mutacions"] --> C3["Redux, context, URL"]
    end
    subgraph L4["11-04 · Proves"]
        D1["Unitàries"] --> D2["Components"] --> D3["Integració"] --> D4["E2E + CI"]
    end
    subgraph L5["11-05 · Producció"]
        E1["build i entorns"] --> E2["Desplegament"] --> E3["Monitorització"]
    end
    L1 --> L2 --> L3 --> L4 --> L5

I l'ordre de les històries, prioritzat per risc primer:

  1. H4 (accés) abans que res: és la porta de tota la resta i toca sessió, formulari, redirecció i persistència. Si alguna cosa ha de sortir malament en l'arquitectura, surt malament aquí.
  2. H1 + H2 (catàleg i filtre): la pantalla més visitada, i la que valida la capa de dades.
  3. H5 (reservar): el camí dels diners. Toca mutació, validació, invalidació i navegació.
  4. H6 (les meves reserves i cancel·lar): reutilitza gairebé tot l'anterior.
  5. H3, H8 (fitxa i estacions): lectura pura, poc risc.
  6. H7 (taller): el rol es prova amb tota la resta ja en peu.
  7. D1-D4 si queda temps.

La regla que governa la llista: el que pot enfonsar el projecte va primer. Deixar la identificació per al final és l'error clàssic, perquè és justament la funcionalitat que travessa totes les capes i obliga a refer el que ja s'havia donat per bo.

  1. El primer commit

Un commit inicial ha de complir una condició: qui el cloni, el pot arrencar.

git init
git add .
git commit -m "chore: bastiment inicial de CicloUrbano amb Vite, linter i API de desenvolupament"

I el README.md que l'acompanya, que és el primer que llegeix qualsevol:

# CicloUrbano

Aplicació web de lloguer de bicicletes urbanes per estacions.

## Requisits
- Node.js 20 o superior

## Arrencada

npm install cp .env.example .env npm run dev:todo # Vite a :5173 i json-server a :3001

## Scripts
| Comanda | Què fa |
|---|---|
| `npm run dev:todo` | Aplicació i API en paral·lel |
| `npm run lint` | ESLint sense tolerància a avisos |
| `npm test` | Vitest en mode vigilància |
| `npm run e2e` | Cypress sobre l'aplicació aixecada |
| `npm run build` | Construcció de producció a `dist/` |

## Documentació
- `DECISIONS.md`: acta de decisions d'arquitectura
- `db.json`: dades de desenvolupament i llavor de les proves

La llista de comprovació abans de donar el bastiment per acabat:

  • [ ] npm install funciona en un clon net
  • [ ] npm run dev serveix l'aplicació al 5173
  • [ ] npm run api respon al 3001 amb els quatre recursos
  • [ ] npm run lint acaba sense errors ni avisos
  • [ ] .env està ignorat i .env.example versionat
  • [ ] DECISIONS.md té les deu files de l'acta
  • [ ] El README.md permet arrencar sense preguntar res a ningú

Errors Comuns i Consells

  • Començar pel codi i planificar sobre la marxa. És l'error de fons d'aquesta lliçó. Sense històries amb criteris d'acceptació no saps quan has acabat, i sense acta de decisions cada discussió tècnica es repeteix cada dues setmanes. Mitja jornada de planificació estalvia setmanes.
  • Històries sense criteris comprovables. «El catàleg ha de ser ràpid» no es pot verificar ni convertir en prova. «El catàleg mostra l'esquelet mentre carrega i la llista tan bon punt arriben les dades» sí.
  • No escriure el que queda fora. L'abast que no es nega explícitament s'assumeix inclòs. La taula de l'apartat 4 és la que protegeix l'entrega.
  • Imbricar objectes en el model de dades. Guardar la bicicleta sencera dins de la reserva sembla còmode el primer dia i produeix dades desincronitzades el segon. Identificadors, sempre.
  • Un joc de dades de desenvolupament sense casos límit. Si totes les bicicletes estan disponibles i tots els usuaris tenen reserves, mai no veuràs l'estat buit ni el botó deshabilitat, i arribaran trencats a producció.
  • Posar eines de desenvolupament a dependencies. Cypress i json-server en producció són un símptoma que ningú no ha mirat el package.json des que es va generar.
  • Deixar exhaustive-deps com a avís. Amb el temps se n'acumulen desenes i deixen de llegir-se. Com a error, es resol o es justifica amb un comentari visible.
  • Posar eslint-config-prettier en qualsevol posició menys l'última. Deixa de fer efecte i acabes amb el linter i el formatador en conflicte a cada desada.
  • Secrets en variables VITE_. Tot el que comença per VITE_ viatja al navegador en text pla. No hi ha excepció, ni «només mentre provem».
  • Consell: crea DECISIONS.md des del primer dia i afegeix una fila cada vegada que discuteixis una elecció tècnica. El valor no és al document, és a no tornar a tenir la mateixa conversa.
  • Consell: verifica l'API amb curl abans d'escriure el primer fetch. Descartar la meitat del sistema en trenta segons estalvia depuracions llarguíssimes.

Exercicis

Exercici 1. El client demana una història nova: «com a client vull rebre un avís 10 minuts abans que acabi la meva reserva». Escriu-la amb el format de l'apartat 3, amb criteris d'acceptació comprovables. Després decideix si és imprescindible o desitjable, i si entra o no a la primera versió, justificant-ho amb l'abast de l'apartat 1 i la taula de l'apartat 4. Si decideixes deixar-la fora, redacta la fila corresponent.

Exercici 2. L'equip proposa afegir una entitat Incidencia per a la pestanya d'incidències de /estaciones/:estacionId, amb la informació d'una avaria reportada. Defineix els seus camps i les seves relacions, afegeix-la al diagrama d'entitat-relació i al db.json amb dos registres d'exemple coherents amb el cànon, i explica quins recursos nous apareixerien a json-server. Indica a més quina clau de TanStack Query li correspondria segons la fàbrica del projecte.

Exercici 3. Un company proposa tres canvis sobre l'acta: (a) usar BrowserRouter amb <Routes> perquè «és més senzill», (b) guardar la llista de bicicletes a Redux «per tenir-ho tot en un lloc», i (c) posar la clau d'un servei de mapes a VITE_CLAU_MAPES per usar-la des del client. Respon a cadascun amb l'argument tècnic corresponent i digues quina fila de l'acta ho cobreix. Per al tercer, explica a més què caldria per usar aquest servei sense exposar la clau.

Solucions

Solució 1.

ID Història Criteris d'acceptació
D5 Com a client vull rebre un avís 10 minuts abans que acabi la meva reserva per tornar la bicicleta a temps Amb una reserva activa o confirmada l'hora de fi de la qual estigui a menys de 10 minuts, es mostra un avís persistent amb el temps restant i un enllaç a la reserva · L'avís desapareix en finalitzar la reserva o en cancel·lar-la · Si hi ha diverses reserves properes a vèncer, es mostra la més propera · El càlcul usa dataInici + hores i s'actualitza cada minut

Classificació: desitjable (D5), fora de la primera versió. Els tres arguments:

  1. No bloqueja l'objectiu principal de cap de les dues persones. Ana pot reservar i usar la bicicleta sense l'avís; Marc no el necessita en absolut.
  2. L'avís realment útil és el que arriba amb l'aplicació tancada, i això són notificacions push, que estan explícitament fora d'abast per exigir servidor, permisos i service worker. Un avís que només es veu amb la pestanya oberta resol una fracció petita del problema real i pot donar una falsa sensació de cobertura.
  3. Requereix un temporitzador global que reavaluï cada minut i una font de veritat de l'hora actual, cosa que complica les proves (cal fixar el rellotge) per a un benefici marginal en aquesta versió.

Fila per a la taula de l'apartat 4:

Fora d'abast Motiu
Avisos de fi de reserva La versió valuosa exigeix notificacions push, que ja estan fora d'abast. La variant dins de la pestanya cobreix pocs casos reals i afegeix un temporitzador global difícil de provar. Es reconsidera quan hi hagi servidor propi (11-05)

I l'anotació honesta: quan arribi l'API real, aquesta història puja a imprescindible, perquè el negoci penalitza la devolució tardana i avisar és més barat que cobrar recàrrecs.

Solució 2.

Camps i relacions. Una incidència pertany a una bicicleta (que al seu torn és en una estació) i la reporta un usuari:

erDiagram
    BICICLETA ||--o{ INCIDENCIA : "acumula"
    USUARI ||--o{ INCIDENCIA : "reporta"
    INCIDENCIA {
        string id PK "inc-01"
        string bicicletaId FK "bici-003"
        string reportadaPer FK "usr-01"
        string data "2026-05-02T18:30"
        string tipus "frens | roda | bateria | altre"
        string descripcio "El fre del darrere patina"
        string estat "oberta | en_curs | resolta"
    }

Registres per a db.json, coherents amb el cànon —bici-003 és en manteniment, així que és la candidata natural a tenir una incidència oberta, i bici-002 pot tenir-ne una ja resolta:

{
  "incidencias": [
    {
      "id": "inc-01",
      "bicicletaId": "bici-003",
      "reportadaPer": "usr-01",
      "data": "2026-05-02T18:30",
      "tipus": "frens",
      "descripcio": "El fre del darrere patina amb càrrega.",
      "estat": "oberta"
    },
    {
      "id": "inc-02",
      "bicicletaId": "bici-002",
      "reportadaPer": "usr-02",
      "data": "2026-04-28T09:15",
      "tipus": "bateria",
      "descripcio": "La bateria no arribava al 60 % d'autonomia anunciada.",
      "estat": "resolta"
    }
  ]
}

Decisió de modelatge important: la incidència s'associa a la bicicleta, no a l'estació, encara que la pestanya que la mostra sigui la d'una estació. El motiu és que una avaria viatja amb la bicicleta: si bici-003 es trasllada a est-03, el seu historial ha d'anar amb ella. La pestanya d'incidències d'una estació s'obté llavors derivant: les bicicletes d'aquesta estació, i les incidències d'aquestes bicicletes. Modelar la relació al revés obligaria a reescriure l'estacionId de cada incidència a cada trasllat.

Recursos que apareixen a json-server:

Petició Ús
GET /incidencias Totes
GET /incidencias?bicicletaId=bici-003 Historial d'una bicicleta
GET /incidencias?estat=oberta Cua de treball del taller (H7)
POST /incidencias Reportar-ne una de nova
PATCH /incidencias/inc-01 Canviar-ne l'estat

I la clau de consulta, aprofitant que la fàbrica del projecte ja l'havia previst:

// src/consultes/claus.js
estaciones: {
  totes: () => ['estaciones'],
  detall: (id) => ['estaciones', id],
  incidencias: (id) => ['estaciones', id, 'incidencias']   // ja existia
},
incidencias: {
  totes: () => ['incidencias'],
  deBicicleta: (bicicletaId) => ['incidencias', { bicicleta: bicicletaId }]
}

La jerarquia importa: invalidar ['estaciones', 'est-02'] invalida també ['estaciones', 'est-02', 'incidencias'], perquè TanStack Query compara les claus per prefix. És justament el comportament que es vol en resoldre una incidència.

Solució 3.

(a) BrowserRouter amb <Routes>. Cobert per la fila A2. És cert que és més senzill d'escriure, però el projecte necessita tres coses que aquest mode no dona:

  • errorElement per branca: sense ell, un error en carregar la fitxa d'una bicicleta enfonsa tota l'aplicació en lloc de deixar la capçalera i el menú drets. És la diferència entre una pantalla en blanc i un error contingut.
  • lazy a nivell de ruta amb l'enrutador gestionant la càrrega, que és el que fa possible la divisió de codi de 08-04 sense embolicar cada pantalla a mà.
  • L'API de dades (useNavigation, useRouteError, handle per a les molles de pa), que ja s'usa a MollesDePa.

I l'argument decisiu: migrar després costa més que començar bé, perquè implica reescriure l'arbre de rutes sencer quan ja hi ha pantalles al damunt.

(b) La llista de bicicletes a Redux. Cobert per A3 i per la taula d'assignació d'estat. «Tenir-ho tot en un lloc» sona a ordre, però barreja dues coses de naturalesa diferent: les bicicletes són estat del servidor, una còpia local d'una dada que viu en una altra màquina i que pot canviar sense que l'aplicació se n'assabenti. Ficar-les a Redux obliga a escriure a mà, i a mantenir, tot això:

Necessitat Amb TanStack Query Amb Redux a mà
Memòria cau per clau Inclosa slice amb estructura pròpia
Deduplicació de peticions simultànies Inclosa condition al thunk
Estats de càrrega i error isPending, isError Tres camps per recurs a extraReducers
Revalidació en tornar a la pestanya refetchOnWindowFocus Efecte propi amb visibilitychange
Dades obsoletes i refresc en segon pla staleTime No existeix: o hi ha dada o no n'hi ha
Invalidar després d'una mutació invalidateQueries Despatxar i recarregar a mà

És exactament la feina que 07-06 va mostrar que no val la pena reescriure. Redux es queda amb el que sí que és seu: sessió, terme de cerca i ordre.

(c) La clau del servei de mapes a VITE_CLAU_MAPES. És el més greu dels tres. Tot el que comença per VITE_ se substitueix literalment al codi durant la construcció i acaba en un fitxer de dist/ que qualsevol pot obrir:

npm run build
grep -r "CLAU" dist/assets/*.js    # allà està, en text pla

No hi ha ofuscació que ho arregli: el navegador ha de poder llegir-la per usar-la, per tant l'usuari també. Les conseqüències són consum facturat al teu compte i, segons el servei, accés a dades que no haurien de sortir.

Què fer en el seu lloc, per ordre de preferència:

  1. Que la petició al servei surti del servidor. El client crida la teva API, la teva API crida el servei amb la clau i retorna el resultat. La clau mai no surt de la màquina.
  2. Si el servei està pensat per al client —molts proveïdors de mapes ofereixen claus públiques—, usar aquest tipus de clau i restringir-la al tauler del proveïdor per domini d'origen, per quota i per permisos mínims. Continua sent visible, però només funciona des del teu domini i amb un sostre de despesa.
  3. Mai usar una clau amb permisos d'escriptura o de facturació des del client, en cap circumstància.

I com que l'exercici parteix d'una història que està fora d'abast —el mapa geogràfic—, la resposta completa inclou recordar-ho: no cal resoldre el problema de la clau encara, perquè la funcionalitat no entra en aquesta versió.

Conclusió

Aquesta lliçó ha convertit una idea en un projecte que es pot començar a construir, i ho ha fet en l'ordre correcte: primer el producte, després les decisions, i només al final les eines.

Del producte queda fixat l'essencial: CicloUrbano en una frase que ja decideix l'arquitectura en declarar-se privada, dues persones usuàries amb objectius i contextos diferents que justifiquen per què l'inici és el catàleg i el taller una pantalla a part, vuit històries imprescindibles i quatre desitjables amb criteris d'acceptació comprovables —que a 11-04 es convertiran literalment en proves—, i una llista explícita del que queda fora amb el seu motiu, que és la que protegeix l'entrega quan algú proposi afegir un mapa a mig camí.

Del domini queda el model definitiu amb les seves quatre entitats i tres regles que es respecten sense excepció: relacions per identificador i mai objectes imbricats, estats com a cadena d'un conjunt tancat en lloc de booleans, i les incoherències heretades anotades com a deute en comptes d'arreglades a destemps. El db.json no és un munt de dades de farciment: cada estat, cada cas límit i l'estat buit tenen el seu representant, perquè el que no es veu en desenvolupament no es dissenya.

Del rumb tècnic queda l'acta de decisions, deu files amb la seva justificació i la seva alternativa descartada: Vite en lloc de Next.js perquè l'aplicació és privada i interactiva; React Router en mode de dades per errorElement, lazy i l'API de dades; TanStack Query per al servidor i Redux Toolkit per al client, amb context per a tema i avisos i la URL per al filtre; CSS Modules; el quartet Vitest, Testing Library, MSW i Cypress; i JavaScript ara amb la porta oberta a TypeScript. Junt amb l'acta, la taula d'assignació d'estat que respon per endavant a la pregunta que més vegades es repeteix en un projecte de React: on viu aquesta dada?

I del bastiment queda un repositori funcionant: Vite amb React 19, dependències triades una a una i ben separades entre producció i desenvolupament, un package.json amb catorze scripts que són la interfície del projecte, ESLint amb jsx-a11y i exhaustive-deps elevat a error, Prettier l'últim de la cadena, .editorconfig, .gitignore, variables d'entorn centralitzades a configuracio.js amb .env.example versionat i cap secret, una estructura de carpetes híbrida —per tipus el que és compartit, per funcionalitat l'estat de domini—, json-server sembrat i verificat amb curl, les convencions de l'equip escrites al README.md, i un pla de treball per increments verticals que ataca primer el que té més risc.

Curs de React

Mòdul 1: Introducció a React

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats