Tutti i playbookNext.js + Flutter Product Monorepo

Aggiungere un endpoint al contratto condiviso

Aggiungere un'operazione al contratto OpenAPI, servirla da una Route Handler Next.js protetta dalla RLS e chiamarla da Flutter con il client generato.

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 /v2 accanto al vecchio.
  • apps/mobile/lib/api/generated/product_api.dart non si modifica mai a mano: cambia il generatore ed esegui npm 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

  1. 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 in supabase/migrations/202609280001_shared_product_contract.sql. Descrivila in DATABASE.md.

  2. In contracts/openapi.json aggiungi il percorso con un operationId stabile e lo schema sotto components.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" } } } } }
      }
    }
    

    NoteList ha un array obbligatorio items di Note; Note ha i campi obbligatori id (uuid), body e createdAt (date-time). Aumenta info.version (minor per una modifica compatibile).

  3. 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 classe Note con una factory fromJson (DateTime.parse per createdAt) e il metodo Future<List<Note>> listNotes(); in ProductApi. Se lo schema ha un enum, costruisci i valori dal contratto come per BillingStatus.

  4. Esegui npm run contract:generate. npm run contract:check confronta solo il file Dart con l'output del generatore, quindi estendi anche tool/test-openapi-contract.mjs: aggiungi il percorso all'elenco verificato e controlla i campi obbligatori che la classe Dart legge.

  5. Crea apps/web/app/api/v1/notes/route.ts sul modello di apps/web/app/api/v1/me/route.ts: leggi il bearer token, passalo a createSupabaseServerClient(token) perché le query girino come quell'utente, chiama auth.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_at diventa createdAt). Le handler GET sono dinamiche per default: non aggiungere cache a risposte che dipendono dall'utente.

  6. In apps/mobile/lib/api/supabase_product_api.dart implementa listNotes con _get('/api/v1/notes') (Next.js serve il contratto sotto /api) e leggi items con Note.fromJson.

  7. Aggiungi test in apps/mobile/test/supabase_product_api_test.dart con MockClient: la richiesta porta il bearer token, la risposta viene letta e uno stato diverso da 200 lancia ProductApiException.

  8. Documenta l'operazione in API.md.

Completato quando

  • i comandi di verifica superano e npm run contract:check non segnala differenze dopo npm 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.md descrive la nuova operazione.