Quando usarlo
Web e mobile hanno bisogno degli stessi nuovi dati o di una nuova azione, per
esempio le note dell'utente autenticato. I passi usano GET /v1/notes come
esempio.
Regole da rispettare
- Prima viene
contracts/openapi.json: cambia il contratto, poi il codice da entrambe le parti. Non definire il tipo separatamente in TypeScript e Dart. - Aggiungere un'operazione o un campo facoltativo è compatibile e resta in
/v1. Rimuovere o rinominare un campo, o cambiarne il tipo, richiede un nuovo percorso/v2accanto al vecchio. apps/mobile/lib/api/generated/product_api.dartnon si modifica mai a mano: cambia il generatore ed eseguinpm run contract:generate.- È il server a decidere chi è l'utente e cosa può leggere. La handler accetta
il cookie di sessione (web) o
Authorization: Bearer <token>(mobile) e la RLS limita le righe. Non fidarti mai di un id utente o di un ruolo inviati dal client.
Passi
Se i dati sono nuovi, aggiungi una tabella in un nuovo file sotto
supabase/migrations/(append-only), con la RLS abilitata e una policy per ogni operazione consentita, come insupabase/migrations/202609280001_shared_product_contract.sql. Descrivila inDATABASE.md.In
contracts/openapi.jsonaggiungi il percorso con unoperationIdstabile e lo schema sottocomponents.schemas. Racchiudi le liste in un oggetto, così in seguito si può aggiungere un campo senza rompere i client:"/v1/notes": { "get": { "operationId": "listNotes", "responses": { "200": { "description": "Notes of the signed-in user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NoteList" } } } } } } }NoteListha un array obbligatorioitemsdiNote;Noteha i campi obbligatoriid(uuid),bodyecreatedAt(date-time). Aumentainfo.version(minor per una modifica compatibile).tool/generate-dart-client.mjsè un piccolo generatore scritto per questo contratto: scrive le classi Dart da un testo fisso e dal contratto legge solo i valori degli enum. Aggiungi a quel testo la classeNotecon una factoryfromJson(DateTime.parsepercreatedAt) e il metodoFuture<List<Note>> listNotes();inProductApi. Se lo schema ha un enum, costruisci i valori dal contratto come perBillingStatus.Esegui
npm run contract:generate.npm run contract:checkconfronta solo il file Dart con l'output del generatore, quindi estendi anchetool/test-openapi-contract.mjs: aggiungi il percorso all'elenco verificato e controlla i campi obbligatori che la classe Dart legge.Crea
apps/web/app/api/v1/notes/route.tssul modello diapps/web/app/api/v1/me/route.ts: leggi il bearer token, passalo acreateSupabaseServerClient(token)perché le query girino come quell'utente, chiamaauth.getUser(token)e rispondi 401 senza utente. Interroga la tabella senza filtrare su un id utente preso dalla richiesta e converti le colonne nei nomi del contratto (created_atdiventacreatedAt). Le handlerGETsono dinamiche per default: non aggiungere cache a risposte che dipendono dall'utente.In
apps/mobile/lib/api/supabase_product_api.dartimplementalistNotescon_get('/api/v1/notes')(Next.js serve il contratto sotto/api) e leggiitemsconNote.fromJson.Aggiungi test in
apps/mobile/test/supabase_product_api_test.dartconMockClient: la richiesta porta il bearer token, la risposta viene letta e uno stato diverso da 200 lanciaProductApiException.Documenta l'operazione in
API.md.
Completato quando
- i comandi di verifica superano e
npm run contract:checknon segnala differenze doponpm run contract:generate; - la handler risponde 401 senza sessione o token e restituisce solo le righe dell'utente autenticato, dal browser e con un bearer token;
API.mddescrive la nuova operazione.
