Progettazione pratica delle API pubbliche per chi crea SaaS per la prima volta: scegli versioning, paginazione, rate limit, documentazione e un piccolo SDK che puoi consegnare in fretta.

Un'API pubblica non è solo un endpoint che la tua app espone. È una promessa alle persone fuori dal tuo team che il contratto continuerà a funzionare, anche mentre cambi il prodotto.
La parte difficile non è scrivere la v1. È mantenerla stabile mentre correggi bug, aggiungi funzionalità e impari cosa vogliono davvero i clienti.
Le scelte iniziali si ripercuotono più avanti come ticket di supporto. Se le risposte cambiano forma senza avviso, se la nomenclatura è incoerente, o se i client non riescono a capire se una richiesta è riuscita, crei attrito. Quell'attrito si trasforma in sfiducia, e la sfiducia fa sì che le persone smettano di costruire sulla tua piattaforma.
La velocità conta anche. La maggior parte dei primi builder SaaS ha bisogno di consegnare qualcosa di utile in fretta e poi migliorarlo. Il compromesso è semplice: più velocemente spedisci senza regole, più tempo passerai a disfare quelle decisioni quando arrivano utenti reali.
«Abbastanza buono» per la v1 di solito significa un piccolo insieme di endpoint che mappano azioni reali degli utenti, nomi e forme di risposta coerenti, una strategia di cambiamento chiara (anche se è solo "v1"), paginazione prevedibile e rate limit sensati, e documentazione che mostri esattamente cosa inviare e cosa aspettarsi in risposta.
Un esempio concreto: immagina che un cliente costruisca un'integrazione che crea fatture ogni notte. Se poi rinomini un campo, cambi il formato delle date o inizi a restituire risultati parziali senza avviso, il loro job fallisce alle 2 di mattina. Incolperanno la tua API, non il loro codice.
Se costruisci con uno strumento chat-driven come Koder.ai, è allettante generare molti endpoint velocemente. Va bene, ma mantieni la surface pubblica piccola. Puoi mantenere endpoint interni privati mentre impari cosa dovrebbe far parte del contratto a lungo termine.
Una buona progettazione di API pubbliche inizia scegliendo un piccolo set di sostantivi (risorse) che corrispondono al modo in cui i clienti parlano del tuo prodotto. Mantieni i nomi delle risorse stabili anche se il tuo database interno cambia. Quando aggiungi funzionalità, preferisci aggiungere campi o nuovi endpoint invece di rinominare risorse core.
Un set pratico di partenza per molti prodotti SaaS è: users, organizations, projects ed events. Se non riesci a spiegare una risorsa in una frase, probabilmente non è pronta per essere pubblica.
Mantieni l'uso di HTTP noioso e prevedibile:
GET legge dati (nessun effetto collaterale)POST crea qualcosa (o avvia un'azione)PATCH aggiorna pochi campiDELETE rimuove o disabilita qualcosaL'autenticazione non deve essere raffinata nel giorno uno. Se la tua API è principalmente server-to-server (clienti che chiamano dal loro backend), le API key spesso bastano. Se i clienti devono agire come singoli utenti finali, o prevedi integrazioni di terze parti dove gli utenti concedono accesso, OAuth è generalmente più adatto. Scrivi la decisione in linguaggio semplice: chi è il chiamante e su quali dati può intervenire?
Imposta le aspettative presto. Sii esplicito su cosa è supportato vs cosa è "best effort". Per esempio: gli endpoint di lista sono stabili e backwards-compatible, ma i filtri di ricerca possono espandersi e non sono garantiti esaustivi. Questo riduce i ticket di supporto e ti lascia libertà di migliorare.
Se stai costruendo su una piattaforma "vibe-coding" come Koder.ai, tratta l'API come un prodotto contratto: mantieni il contratto piccolo all'inizio, poi falla crescere in base all'uso reale, non alle ipotesi.
Il versioning riguarda soprattutto le aspettative. I client vogliono sapere: la mia integrazione si romperà la prossima settimana? Tu vuoi spazio per migliorare le cose senza paura.
Il versionamento tramite header può sembrare pulito, ma è facile nasconderlo nei log, nelle cache e negli screenshot di supporto. Il versionamento nell'URL è di solito la scelta più semplice: /v1/.... Quando un cliente ti manda una richiesta fallita, puoi vedere subito la versione. Rende anche facile eseguire v1 e v2 in parallelo.
Una modifica è breaking se un client ben comportato potrebbe smettere di funzionare senza cambiare il proprio codice. Esempi comuni:
customer_id a customerId)Un cambiamento sicuro è uno che i client vecchi possono ignorare. Aggiungere un nuovo campo opzionale è di solito sicuro. Per esempio, aggiungere plan_name alla risposta di GET /v1/subscriptions non spezzerà i client che leggono solo status.
Una regola pratica: non rimuovere o riutilizzare campi all'interno della stessa major version. Aggiungi nuovi campi, mantieni quelli vecchi e ritirali solo quando sei pronto a deprecare l'intera versione.
Tienila semplice: annuncia le deprecazioni presto, restituisci un messaggio di warning chiaro nelle risposte e fissa una data di fine. Per una prima API, una finestra di 90 giorni è spesso realistica. Durante quel periodo, mantieni v1 funzionante, pubblica una breve nota di migrazione e assicurati che il supporto possa indicare in una frase: v1 funziona fino a questa data; ecco cosa è cambiato in v2.
Se costruisci su una piattaforma come Koder.ai, tratta le versioni API come snapshot: rilascia miglioramenti in una nuova versione, mantieni stabile la vecchia e chiudila solo dopo aver dato tempo ai clienti di migrare.
La paginazione è dove si guadagna o si perde fiducia. Se i risultati saltano tra richieste, la gente smette di fidarsi della tua API.
Usa page/limit quando il dataset è piccolo, la query è semplice e gli utenti spesso vogliono la pagina 3 di 20. Usa la paginazione basata su cursore quando le liste possono crescere molto, arrivano nuovi elementi spesso o l'utente può ordinare e filtrare molto. La paginazione basata su cursore mantiene la sequenza stabile anche quando arrivano nuovi record.
Alcune regole mantengono la paginazione affidabile:
I totali sono complicati. Un total_count può essere costoso su tabelle grandi, specialmente con filtri. Se puoi fornirlo a basso costo, includilo. Se non puoi, omettelo o rendilo opzionale tramite una flag di query.
Here are simple request/response shapes.
// Page/limit
GET /v1/invoices?page=2&limit=25&sort=created_at_desc
{
"items": [{"id":"inv_1"},{"id":"inv_2"}],
"page": 2,
"limit": 25,
"total_count": 142
}
// Cursor-based
GET /v1/invoices?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDozMDowMFoiLCJpZCI6Imludl8xMDAifQ==
{
"items": [{"id":"inv_101"},{"id":"inv_102"}],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDoyNTowMFoiLCJpZCI6Imludl8xMjUifQ=="
}
I rate limit servono meno a essere severi e più a restare online. Proteggono la tua app da picchi di traffico, il tuo DB da query costose fatte troppo spesso e il tuo conto da bollette infra impreviste. Un limite è anche un contratto: i client sanno quale è l'uso normale.
Parti semplice e aggiusta dopo. Scegli qualcosa che copra l'uso tipico con margine per brevi burst, poi osserva il traffico reale. Se non hai dati, un default sicuro è un limite per API key come 60 richieste al minuto più un piccolo burst. Se un endpoint è molto più pesante (come search o export), dagli un limite più restrittivo o una regola di costo separata invece di penalizzare ogni richiesta.
Quando applichi i limiti, rendi facile per i client fare la cosa giusta. Restituisci un 429 Too Many Requests e includi alcuni header standard:
X-RateLimit-Limit: il massimo permesso nella finestraX-RateLimit-Remaining: quanti ne restanoX-RateLimit-Reset: quando la finestra si resetta (timestamp o secondi)Retry-After: quanto aspettare prima di ritentareI client dovrebbero trattare il 429 come una condizione normale, non come un errore da combattere. Un pattern di retry educato tiene contente entrambe le parti:
Retry-After quando è presenteEsempio: se un cliente esegue una sync notturna che colpisce pesantemente la tua API, il suo job può distribuire le richieste in un minuto e rallentare automaticamente sui 429 invece di far fallire l'intera esecuzione.
Se gli errori della tua API sono difficili da leggere, i ticket di supporto si accumulano velocemente. Scegli una forma di errore e usala dappertutto, anche per i 500. Uno standard semplice è: code, message, details e un request_id che l'utente può incollare nella chat di supporto.
Here is a small, predictable format:
{
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"details": {
"fields": [
{"name": "email", "issue": "must be a valid email"},
{"name": "plan", "issue": "must be one of: free, pro, business"}
]
},
"request_id": "req_01HT..."
}
}
Usa i codici HTTP nello stesso modo ogni volta: 400 per input non valido, 401 quando l'auth manca o è invalida, 403 quando l'utente è autenticato ma non autorizzato, 404 quando una risorsa non viene trovata, 409 per conflitti (come un valore unico duplicato o stato sbagliato), 429 per rate limit e 500 per errori server. La coerenza batte l'intelligenza.
Rendi gli errori di validazione facili da risolvere. Gli hint a livello di campo dovrebbero puntare al nome del parametro che la tua documentazione usa, non a una colonna interna del DB. Se c'è un requisito di formato (data, valuta, enum), dì cosa accetti e mostra un esempio.
I retry sono dove molte API generano duplicati involontari. Per azioni POST importanti (pagamenti, creazione fatture, invio email), supporta le idempotency key così i client possono ritentare in sicurezza.
Idempotency-Key su endpoint POST selezionati.Quell'header evita molti edge case dolorosi quando le reti sono instabili o i client subiscono timeout.
Immagina che gestisci un SaaS semplice con tre oggetti principali: projects, users e invoices. Un project ha molti users, e ogni project riceve fatture mensili. I client vogliono sincronizzare le fatture nel loro strumento contabile e mostrare la fatturazione base nella loro app.
Una v1 pulita potrebbe essere così:
GET /v1/projects/{project_id}
GET /v1/projects/{project_id}/invoices
POST /v1/projects/{project_id}/invoices
Ora succede un cambiamento breaking. In v1 memorizzi gli importi delle fatture come intero in centesimi: amount_cents: 1299. Più avanti, ti servono multi-valuta e decimali, quindi vuoi amount: "12.99" e currency: "USD". Se sovrascrivi il vecchio campo, ogni integrazione esistente si rompe. Il versionamento evita il panico: mantieni v1 stabile, rilascia /v2/... con i nuovi campi e supporta entrambi finché i client non migrano.
Per elencare le fatture, usa una forma di paginazione prevedibile. Per esempio:
GET /v1/projects/p_123/invoices?limit=50&cursor=eyJpZCI6Imludl85OTkifQ==
200 OK
{
"data": [ {"id":"inv_1001"}, {"id":"inv_1000"} ],
"next_cursor": "eyJpZCI6Imludl8xMDAwIn0="
}
Un giorno un cliente importa fatture in loop e raggiunge il rate limit. Invece di errori casuali, ottiene una risposta chiara:
429 Too Many RequestsRetry-After: 20{ "error": { "code": "rate_limited" } }Dalla loro parte, il client può sospendere per 20 secondi, poi ripartire dallo stesso cursor senza riscaricare tutto o creare fatture duplicate.
Un lancio v1 va meglio se lo tratti come un piccolo rilascio di prodotto, non come un mucchio di endpoint. L'obiettivo è semplice: le persone possono costruirci sopra e tu puoi continuare a migliorarlo senza sorprese.
Inizia scrivendo una pagina che spiega a cosa serve la tua API e cosa non è. Mantieni la surface abbastanza piccola da poterla spiegare a voce in un minuto.
Usa questa sequenza e non andare avanti finché ogni passo non è abbastanza buono:
Se costruisci con un workflow che genera codice (per esempio usando Koder.ai per scaffolding di endpoint e risposte), fai comunque il test del fake-client. Il codice generato può sembrare corretto ma essere comunque scomodo da usare.
Il rendimento sono meno email di supporto, meno hotfix e una v1 che puoi davvero mantenere.
Un primo SDK non è un secondo prodotto. Pensalo come un wrapper sottile e amichevole sopra la tua API HTTP. Deve rendere facili le chiamate comuni, ma non deve nascondere come funziona l'API. Se qualcuno ha bisogno di una funzione che non hai incapsulato, deve comunque poter scendere al raw HTTP.
Scegli un linguaggio per partire, basato su cosa usano davvero i tuoi clienti. Per molte API B2B SaaS spesso è JavaScript/TypeScript o Python. Consegnare uno SDK solido batte consegnarne tre a metà.
Un buon set di partenza è:
Puoi costruirlo a mano o generarlo da una spec OpenAPI. La generazione è ottima quando la spec è accurata e vuoi tipi coerenti, ma spesso produce molto codice. All'inizio, un client minimale scritto a mano più un file OpenAPI per la doc è di solito sufficiente. Puoi passare a client generati dopo senza rompere gli utenti, purché l'interfaccia pubblica dell'SDK rimanga stabile.
La versione dell'API deve seguire le tue regole di compatibilità. La versione dell'SDK deve seguire le regole di packaging.
Se aggiungi nuovi parametri opzionali o nuovi endpoint, di solito è un bump minore dell'SDK. Riserva rilasci major dell'SDK per breaking change nell'SDK stesso (metodi rinominati, default cambiati), anche se l'API è rimasta la stessa. Questa separazione mantiene gli upgrade tranquilli e i ticket di supporto bassi.
La maggior parte dei ticket di supporto non riguarda bug. Riguarda sorprese. La progettazione API pubblica riguarda soprattutto l'essere noiosi e prevedibili così il codice cliente continua a funzionare mese dopo mese.
Il modo più veloce per perdere fiducia è cambiare le risposte senza dirlo a nessuno. Se rinomini un campo, cambi un tipo o inizi a restituire null dove prima c'era un valore, romperai i client in modi difficili da diagnosticare. Se proprio devi cambiare comportamento, versiona, o aggiungi un nuovo campo e mantieni quello vecchio per un po' con un piano di sunset chiaro.
La paginazione è un altro assalitore ricorrente. I problemi emergono quando un endpoint usa page/pageSize, un altro usa offset/limit e un terzo usa cursori, tutti con defaults diversi. Scegli un pattern per la v1 e usalo ovunque. Mantieni anche l'ordinamento stabile, così la pagina successiva non salta o ripete elementi quando arrivano nuovi record.
Gli errori creano molti scambi quando sono incoerenti. Un fallimento comune è un servizio che ritorna { "error":"..." } e un altro { "message":"..." }, con codici di stato diversi per lo stesso problema. I client allora costruiscono handler sporchi endpoint-specifici.
Ecco cinque errori che generano le thread di email più lunghe:
Un'abitudine semplice aiuta: ogni risposta dovrebbe includere un request_id, e ogni 429 dovrebbe spiegare quando ritentare.
Prima di pubblicare qualsiasi cosa, fai un'ultima verifica concentrata sulla coerenza. La maggior parte dei ticket di supporto succede perché piccoli dettagli non combaciano tra endpoint, doc ed esempi.
Controlli rapidi che catturano la maggior parte dei problemi:
Dopo il lancio, osserva cosa gli utenti colpiscono realmente, non quello che speravi usassero. Una piccola dashboard e una review settimanale sono sufficienti all'inizio.
Monitora questi segnali per primi:
Raccogli feedback senza riscrivere tutto. Aggiungi un percorso breve per segnalare problemi nella tua doc e tagga ogni report con endpoint, request id e versione client. Quando risolvi qualcosa, preferisci cambi additivi: nuovi campi, nuovi parametri opzionali o un nuovo endpoint, invece di rompere il comportamento esistente.
Prossimi passi: scrivi una spec API di una pagina con le tue risorse, il piano di versioning, le regole di paginazione e il formato d'errore. Poi produci documentazione e un piccolo SDK starter che copra autenticazione più 2–3 endpoint core. Se vuoi muoverti più veloce, puoi redigere spec, docs e uno starter SDK da un piano chat-based usando strumenti come Koder.ai (la sua modalità planning è utile per mappare endpoint ed esempi prima di generare codice).
Inizia con 5–10 endpoint che mappano azioni reali dei clienti.
Una buona regola: se non riesci a spiegare una risorsa in una frase (cos'è, chi la possiede, come si usa), lasciala privata finché non impari dall'uso.
Scegli un piccolo insieme di sostantivi stabili (risorse) che i clienti già usano nel loro linguaggio e mantieni quei nomi stabili anche se il tuo database interno cambia.
Starter comuni per SaaS sono users, organizations, projects ed events—poi aggiungi altro solo quando c'è domanda chiara.
Usa i significati standard e sii coerente:
GET = lettura (senza effetti collaterali)POST = crea o avvia un'azionePATCH = aggiornamento parzialeDELETE = rimuove o disabilitaIl vantaggio principale è la prevedibilità: i client non devono indovinare cosa fa un metodo.
Di default scegli la versione nell'URL come /v1/....
È più facile da vedere nei log e negli screenshot, più semplice da debug con i clienti e comodo per eseguire v1 e v2 in parallelo quando serve un cambiamento breaking.
Una modifica è breaking se un client corretto può smettere di funzionare senza cambiare il suo codice. Esempi comuni:
Aggiungere un nuovo campo opzionale è di solito sicuro.
Tieni le cose semplici:
Un default pratico è una finestra di 90 giorni per un'API v1, così i clienti hanno tempo per migrare senza panico.
Scegli un pattern e mantienilo su tutti gli endpoint di lista.
Definisci sempre un ordine di default e un tie-breaker (per esempio + ) così i risultati non saltano.
Parti con un limite chiaro per chiave (per esempio 60 richieste/minuto più un piccolo burst), poi aggiusta in base al traffico reale.
Quando limiti, restituisci 429 e includi:
X-RateLimit-LimitUsa un unico formato di errore ovunque (inclusi i 500). Una forma pratica è:
code (identificatore stabile)message (leggibile da umani)details (problemi a livello di campo)request_id (per il supporto)Mantieni anche i codici di stato coerenti (400/401/403/404/409/429/500) così i client possono gestire gli errori in modo pulito.
Se generi molti endpoint rapidamente (ad esempio con Koder.ai), mantieni la surface pubblica piccola e trattala come un contratto a lungo termine.
Fai questo prima del lancio:
POST importanticreated_atidX-RateLimit-RemainingX-RateLimit-ResetRetry-AfterQuesto rende i retry prevedibili e riduce i ticket di supporto.
Poi pubblica un SDK piccolo che aiuti con auth, timeout, retry per richieste sicure e paginazione—senza però nascondere come funziona l'HTTP sottostante.