Consumo, Test e Mock di API REST

Postman
Sezione intitolata “Postman”Postman è uno strumento per l’invio di richieste HTTP e per il collaudo (testing) di API, utile sia in fase di sviluppo sia in fase di verifica funzionale.
- Sito ufficiale: Postman
- Postman Agent: Postman Agent
- Learning Center: Learning Center
- Postman Echo Docs: Postman Echo Docs
Testing di API con Postman
Sezione intitolata “Testing di API con Postman”Materiale introduttivo consigliato:
- Introduzione a Postman: Introduzione a Postman
- Invio della prima richiesta: Invio della prima richiesta
- Modelli (templates) di collection Postman Echo: Modelli di collection Postman Echo
Per utilizzare modelli di richieste già pronti sono disponibili almeno due procedure:
- esportare il modello in un file locale e successivamente importarlo nel proprio workspace di Postman;
- effettuare il fork di una collection (operazione che richiede un profilo pubblico di Postman).
Di seguito sono illustrate entrambe le procedure.
Esportazione e importazione di una collection pubblica
Sezione intitolata “Esportazione e importazione di una collection pubblica”
La collection deve essere salvata localmente in formato JSON.
Per importare la collection nel proprio workspace:

Selezionare “Choose Files” e quindi scegliere il file JSON scaricato tramite l’export. Dopo questa operazione la collection comparirà nel workspace.
Fork di una collection
Sezione intitolata “Fork di una collection”Nell’elenco dei modelli disponibili selezionare Postman Echo e quindi “Create a Fork”. In questo modo viene creata una copia della collection nel proprio workspace, con un nome scelto dall’utente, all’interno del proprio account Postman.
Per poter effettuare il fork di una collection pubblica è necessario rendere pubblico il proprio profilo: Come creare un profilo pubblico Postman

Dopo la creazione del fork, la collection risulterà presente nel workspace dell’account.
Un profilo pubblico Postman consente inoltre di disporre di una pagina di presentazione raggiungibile all’indirizzo https://www.postman.com/UserName sulla quale è possibile pubblicare collection (rendendole copiabili da terzi tramite fork o import).
Utilizzo di Postman con la collection Postman Echo
Sezione intitolata “Utilizzo di Postman con la collection Postman Echo”Riferimento: Invio delle richieste
Dopo l’importazione della collection di Postman Echo (con una delle modalità descritte), è possibile utilizzare le richieste contenute nella collection per familiarizzare con lo strumento.

Eseguire le seguenti richieste:
GET https://postman-echo.com/get?foo1=bar1&foo2=bar2
POST https://postman-echo.com/post
PUT https://postman-echo.com/put
PATCH https://postman-echo.com/patch
DELETE https://postman-echo.com/delete
Utilizzo della collection Postman API
Sezione intitolata “Utilizzo della collection Postman API”Dopo aver importato nel proprio workspace la collection di Postman API, eseguire le seguenti richieste:

GET https://api.getpostman.com/collections
GET https://api.getpostman.com/collections/{{collection_uid}}
POST https://api.getpostman.com/collections
PUT https://api.getpostman.com/collections/{{collection_uid}}
DEL https://api.getpostman.com/collections/{{collection_uid}}
API Key per le Postman API
Sezione intitolata “API Key per le Postman API”Per effettuare le richieste verso le Postman API è obbligatorio utilizzare una API Key. La documentazione specifica che:
- l’API Key deve essere inviata in ogni richiesta mediante header
X-Api-Key; - l’API Key identifica il chiamante e abilita l’accesso a ciò che è visibile nell’account Postman;
- per comodità operativa, la chiave può essere memorizzata in una variabile d’ambiente (environment) e richiamata automaticamente dalle richieste in collection.
Riferimenti utili:
- Dashboard delle integrazioni Postman (per la gestione delle chiavi): https://go.postman.co/integrations/services/pm_pro_api
- Variabili d’ambiente in Postman: https://www.getpostman.com/docs/environments
- Collections: https://www.getpostman.com/docs/collections
Per creare una API Key nel proprio account aprire la pagina:
https://web.postman.co/settings/me/api-keys
Selezionare “Generate API Key”, assegnare un nome alla chiave e salvare il valore in modo sicuro.

Con il valore generato, creare una variabile d’ambiente per la chiave da utilizzare nelle richieste.

La variabile d’ambiente viene denominata postman_api_key e viene richiamata nelle richieste della collection Postman API. Ad esempio:
GET https://api.getpostman.com/collections

Dalla risposta alla richiesta precedente si ottiene la lista degli id/uid delle collection, ad esempio:
"collections": [ { "id": "196c9bb1-39d7-4724-a769-44a90f858db9", "name": "Sample Collection 25", "owner": "1643371", "createdAt": "2022-02-06T16:17:49.000Z", "updatedAt": "2022-02-06T16:45:08.000Z", "uid": "1643371-196c9bb1-39d7-4724-a769-44a90f858db9", "isPublic": false }Con il valore del campo uid si può creare un’ulteriore variabile d’ambiente, ad esempio collection_uid, utilizzabile per richieste del tipo:
GET https://api.getpostman.com/collections/{{collection_uid}}
PUT https://api.getpostman.com/collections/{{collection_uid}}
DEL https://api.getpostman.com/collections/{{collection_uid}}
Autorizzazione delle API in Postman
Sezione intitolata “Autorizzazione delle API in Postman”Riferimento principale: https://learning.postman.com/docs/sending-requests/authorization/
- No Auth: https://learning.postman.com/docs/sending-requests/authorization/#no-auth
- API key: https://learning.postman.com/docs/sending-requests/authorization/#api-key
- API authentication and authorization in Postman: https://learning.postman.com/docs/sending-requests/authorization/authorization
- Approfondimento (IT): https://it.wikipedia.org/wiki/Basic_access_authentication
Sito ufficiale: https://curl.se/
Versione di cURL preinstallata in Windows
Sezione intitolata “Versione di cURL preinstallata in Windows”In Windows 10 e Windows 11 curl.exe risulta generalmente già disponibile. Tuttavia, come indicato in https://curl.se/windows/microsoft.html, la versione distribuita da Microsoft può presentare limitazioni rispetto alle build ufficiali del progetto cURL.
Tra le differenze più comuni (a titolo esemplificativo):
- assenza del supporto alla compressione (l’opzione
--compressednon funziona); - assenza del supporto PSL (Public Suffix List), con riduzione di alcune protezioni contro “super cookie”;
- assenza del supporto HTTP/2 e HTTP/3;
- supporto ridotto di protocolli (ad esempio niente SCP/SFTP);
- build gestita e distribuita separatamente da Microsoft, potenzialmente con aggiornamenti meno frequenti rispetto alle release del progetto.
Alias in PowerShell
Sezione intitolata “Alias in PowerShell”Riferimento: https://curl.se/windows/microsoft.html
Approfondimento sul tema dell’alias curl in Windows PowerShell:
In Windows PowerShell (ma non più in PowerShell 7+) curl può essere un alias del cmdlet Invoke-WebRequest, la cui sintassi differisce sensibilmente da curl.exe.
Documentazione del cmdlet: Invoke-WebRequest
Una soluzione consiste nell’invocare esplicitamente l’eseguibile con estensione:
curl.exe ...
Approfondimento: https://stackoverflow.com/a/77305766
Per verificare gli alias in Windows PowerShell:
Get-Alias
Documentazione: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-alias
Oppure, sfruttando le pipeline per ottenere un risultato analogo a grep:
Get-Alias | Out-String -Stream | Select-String -Pattern 'curl'
Approfondimenti:
- Pipeline (IT): https://learn.microsoft.com/it-it/powershell/module/microsoft.powershell.core/about/about_pipelines
Select-String: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-string

In PowerShell 7+ questo problema non si presenta tipicamente perché l’alias di curl è stato rimosso:

Se in Windows PowerShell si invoca curl senza .exe, può essere richiamato Invoke-WebRequest e le opzioni tipiche di cURL generano errore. In tale caso è necessario utilizzare curl.exe oppure usare PowerShell 7+.
Osservazione importante
Sezione intitolata “Osservazione importante”Windows PowerShell e PowerShell sono prodotti separati.
- Windows PowerShell è la versione fornita con Windows, basata su .NET Framework, eseguibile solo su Windows. La versione più recente è Windows PowerShell 5.1; non riceve più nuove funzionalità.
- PowerShell (7+) è basato su .NET moderno ed è disponibile su Windows, Linux e macOS. Il supporto dipende dalla versione di .NET utilizzata.
Riferimento al ciclo di vita: https://learn.microsoft.com/it-it/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.4
Installazione di cURL
Sezione intitolata “Installazione di cURL”Riferimento: https://curl.se/download.html
Poiché la versione preinstallata può essere limitata, può essere opportuno installare una build completa e aggiornata.
Opzioni comuni:
- Installazione con
winget:
winget install curl.curl
- Download dell’eseguibile da:
Si osservi che, anche installando una versione più recente, il comando curl -V può continuare a mostrare la versione preinstallata da Microsoft se il PATH di sistema privilegia ancora C:\Windows\System32.
Per individuare quale eseguibile viene effettivamente richiamato:
- in CMD:
where curl - in PowerShell:
where.exe curl(poichéwheresenza.exeè alias diWhere-Object)


Nel caso mostrato, cURL è richiamato da %appdata%\Local\Microsoft\WinGet\Links\curl.exe.
Per usare la versione aggiornata è necessario fare in modo che il percorso sopra riportato compaia prima di C:\Windows\System32 nel PATH di sistema.
Settings → Environment Variables:

Dopo l’aggiornamento del PATH è opportuno chiudere e riaprire la shell e verificare la versione:

Mini tutorial introduttivo su cURL
Sezione intitolata “Mini tutorial introduttivo su cURL”cURL è uno strumento a riga di comando per trasferire dati con URL. Supporta numerosi protocolli (HTTP, HTTPS, FTP, SFTP, ecc.) ed è particolarmente utile per testare API REST.
Proxy (ambiente scolastico)
Sezione intitolata “Proxy (ambiente scolastico)”In presenza di proxy (scenario frequente in rete scolastica) è necessario configurare le variabili d’ambiente del proxy per poter accedere a Internet.
In PowerShell:
$env:http_proxy="proxy:3128"
$env:https_proxy="proxy:3128"
In CMD:
set http_proxy=proxy:3128
set https_proxy=proxy:3128
In Linux bash:
export http_proxy="proxy:3128"
export https_proxy="proxy:3128"
Sintassi base
Sezione intitolata “Sintassi base”curl [opzioni] [URL]Scaricare un file
Sezione intitolata “Scaricare un file”Salvare con il nome originale del server:
curl -O https://jsonplaceholder.typicode.com/usersSalvare con un nome specifico:
curl -o lista_utenti.json https://jsonplaceholder.typicode.com/usersScaricare con follow dei redirect:
curl -L -O https://esempio.com/file.zipMostrare progresso di download:
curl --progress-bar -O https://esempio.com/file.zipRiprendere un download interrotto:
curl -C - -O https://esempio.com/file.zipOperazioni CRUD su API REST
Sezione intitolata “Operazioni CRUD su API REST”Per i seguenti esempi si utilizza l’endpoint di test: https://jsonplaceholder.typicode.com/users
GET - Recuperare dati
Sezione intitolata “GET - Recuperare dati”Recuperare tutti gli utenti:
curl https://jsonplaceholder.typicode.com/usersRecuperare un utente specifico:
curl https://jsonplaceholder.typicode.com/users/1Salvare la risposta in un file:
curl -o utente.json https://jsonplaceholder.typicode.com/users/1Mostrare solo gli header della risposta (utile per debug):
curl -I https://jsonplaceholder.typicode.com/users/1Mostrare header e corpo della risposta:
curl -i https://jsonplaceholder.typicode.com/users/1Seguire i redirect automaticamente:
curl -L https://jsonplaceholder.typicode.com/usersPOST - Creare una risorsa
Sezione intitolata “POST - Creare una risorsa”Creare un nuovo utente con dati inline:
# versione Bashcurl -X POST https://jsonplaceholder.typicode.com/users \ -H "Content-Type: application/json" \ -d '{ "name": "Mario Rossi", "username": "mrossi", "email": "mario.rossi@example.com" }'# versione PowerShellcurl -X POST https://jsonplaceholder.typicode.com/users ` -H "Content-Type: application/json" ` -d '{ "name": "Mario Rossi", "username": "mrossi", "email": "mario.rossi@example.com" }'Creare da un file JSON:
# versione Bash# Creare un file `nuovo_utente.json` con i dati del nuovo utentecat > nuovo_utente.json <<EOF{ "name": "Mario Rossi", "username": "mrossi", "email": "mario.rossi@example.com"}EOF
curl -X POST https://jsonplaceholder.typicode.com/users \ -H "Content-Type: application/json" \ -d @nuovo_utente.json# versione PowerShell# Creare un file `nuovo_utente.json` con i dati del nuovo utente@"{ "name": "Mario Rossi", "username": "mrossi", "email": "mario.rossi@example.com"}"@ > nuovo_utente.json
# In PowerShell, il carattere @ ha un significato speciale (splatting)# Per passare @filename a curl, usare virgolette singolecurl -X POST https://jsonplaceholder.typicode.com/users ` -H "Content-Type: application/json" ` -d '@nuovo_utente.json'PUT - Aggiornare completamente una risorsa
Sezione intitolata “PUT - Aggiornare completamente una risorsa”Sostituire completamente un utente (tutti i campi richiesti):
# versione Bashcurl -X PUT https://jsonplaceholder.typicode.com/users/1 \ -H "Content-Type: application/json" \ -d '{ "id": 1, "name": "Leanne Graham Modificata", "username": "Bret", "email": "Sincere@april.biz" }'# versione PowerShellcurl -X PUT https://jsonplaceholder.typicode.com/users/1 ` -H "Content-Type: application/json" ` -d '{ "id": 1, "name": "Leanne Graham Modificata", "username": "Bret", "email": "leanne.g@april.biz" }'PATCH - Aggiornare parzialmente una risorsa
Sezione intitolata “PATCH - Aggiornare parzialmente una risorsa”Modificare solo alcuni campi:
# versione Bashcurl -X PATCH https://jsonplaceholder.typicode.com/users/1 \ -H "Content-Type: application/json" \ -d '{"email": "nuova.email@example.com"}'# versione PowerShellcurl -X PATCH https://jsonplaceholder.typicode.com/users/1 ` -H "Content-Type: application/json" ` -d '{"email": "nuova.email@example.com"}'DELETE - Eliminare una risorsa
Sezione intitolata “DELETE - Eliminare una risorsa”Eliminare un utente:
curl -X DELETE https://jsonplaceholder.typicode.com/users/1Opzioni utili aggiuntive
Sezione intitolata “Opzioni utili aggiuntive”Visualizzare informazioni dettagliate della connessione:
curl -v https://jsonplaceholder.typicode.com/users/1Silenziare output (solo dati):
curl -s https://jsonplaceholder.typicode.com/users/1Combinare silenzioso con mostrare errori:
curl -sS https://jsonplaceholder.typicode.com/users/1Specificare un timeout (secondi):
curl --max-time 10 https://jsonplaceholder.typicode.com/usersInviare dati form-encoded (come un form HTML):
curl -X POST https://jsonplaceholder.typicode.com/users \ -d "name=Mario Rossi" \ -d "username=mrossi"Autenticazione Basic:
curl -u username:password https://api.esempio.com/protettoAutenticazione Bearer Token:
curl -H "Authorization: Bearer TUO_TOKEN" https://api.esempio.com/protettoAutenticazione con API Key (header X-API-Key):
curl -H "X-API-Key: TUA_CHIAVE_API" https://api.esempio.com/protettoOppure in una richiesta POST con piú header:
curl -X POST https://api.esempio.com/users \ -H "Content-Type: application/json" \ -H "X-API-Key: TUA_CHIAVE_API" \ -d '{"name":"Mario Rossi"}'Specificare il metodo HTTP esplicitamente (GET è default):
curl -X GET https://jsonplaceholder.typicode.com/usersEsempi pratici combinati
Sezione intitolata “Esempi pratici combinati”GET con header personalizzati e salvataggio:
curl -H "Accept: application/json" \ -H "User-Agent: MyApp/1.0" \ -o output.json \ https://jsonplaceholder.typicode.com/usersPOST con autenticazione e verifica:
curl -X POST https://jsonplaceholder.typicode.com/users \ -H "Content-Type: application/json" \ -H "Authorization: Bearer abc123" \ -d '{"name":"Test User"}' \ -vRiferimenti rapidi
Sezione intitolata “Riferimenti rapidi”| Opzione | Descrizione |
|---|---|
-X | Specifica il metodo HTTP (GET, POST, PUT, PATCH, DELETE) |
-H | Aggiunge un header HTTP |
-d | Invia dati nel body della richiesta |
-o | Salva output in un file con nome specificato |
-O | Salva output mantenendo il nome originale del server |
-I | Scarica solo gli header (HEAD request) |
-i | Include gli header nella risposta |
-L | Segue i redirect (location) |
-v | Modalità verbose (debug) |
-s | Silenzioso (nasconde progresso) |
-S | Mostra errori anche in modalità silenziosa |
-u | Username e password per autenticazione |
@file | Legge dati da un file |
Risorse aggiuntive
Sezione intitolata “Risorse aggiuntive”- Comandi di cURL in Windows: https://ss64.com/nt/curl.html
- Comandi di cURL in bash: https://ss64.com/bash/curl.html
- Guida generale: https://everything.curl.dev/cmdline
- Modifica del metodo HTTP: https://everything.curl.dev/http/modify/method
- Esempio (IP pubblico): https://linuxconfig.org/how-to-use-curl-to-get-public-ip-address
Verifica dei certificati SSL (HTTPS)
Sezione intitolata “Verifica dei certificati SSL (HTTPS)”Se si prova a eseguire una richiesta HTTPS con cURL, ad esempio:
curl https://jsonplaceholder.typicode.com/userssi potrebbe riscontrare un errore di questo tipo:
curl: (60) SSL certificate problem: unable to get local issuer certificateMore details here: https://curl.se/docs/sslcerts.htmlcurl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.Il problema è dovuto al fatto che cURL non riesce a verificare l’autenticità del certificato digitale presentato dal server remoto. Per risolvere questo problema è necessario configurare cURL in modo che possa accedere ai certificati delle Certification Authorities (CA).
Una Certification Authority (CA) è un’entità terza fidata (come Let’s Encrypt, DigiCert, GlobalSign) responsabile dell’emissione e della gestione dei certificati digitali. Il suo ruolo è quello di garantire crittograficamente che una determinata chiave pubblica appartenga effettivamente all’organizzazione o al dominio che dichiara di possederla, agendo di fatto come un “notaio digitale”. Le CA consentono di validare il certificato digitale del server, seguendo il meccanismo di verifica basato su crittografia asimmetrica e firma digitale.
Nelle installazioni di cURL più recenti è possibile utilizzare l’opzione --ca-native per sfruttare lo store di certificati di Windows, oppure specificare un file con i certificati delle CA tramite l’opzione --cacert, inoltre le versioni più recenti di cURL supportano la feature CAcert che include il bundle Mozilla dei certificati root (ad esempio aggiornati al 2025-12-02 nella release 8.18.0).
Di seguito viene fornita una breve introduzione ai concetti fondamentali alla base del meccanismo di verifica dei certificati digitali, al fine di comprendere meglio il motivo per cui è necessario configurare cURL per accedere ai certificati delle CA in alcuni scenari.
Introduzione ai concetti fondamentali
Sezione intitolata “Introduzione ai concetti fondamentali”Quando si accede a un sito HTTPS, il browser o il client deve risolvere un problema fondamentale: come può essere certo di comunicare davvero con il server che dichiara di essere? Un attaccante potrebbe fingersi il server legittimo (attacco “man-in-the-middle”). Per prevenire questo rischio, si utilizza un sistema basato sulla crittografia asimmetrica, sul concetto di firma digitale e sull’infrastruttura a chiave pubblica (PKI) gestita dalle Certification Authorities (CA).
Il ruolo della CA nel processo di emissione e verifica di un certificato può essere riassunto nel seguente schema:
sequenceDiagram
participant Server as Server (es. esempio.com)
participant CA as Certification Authority
participant Client as Client (Browser/cURL)
Server->>Server: Genera Coppia di Chiavi (Pubblica/Privata)
Server->>CA: Invia CSR (Certificate Signing Request) con Chiave Pubblica
CA->>CA: Verifica l'identità del richiedente (es. controllo dominio)
CA->>Server: Emette Certificato firmato con la Chiave Privata della CA
Client->>Server: Richiesta di connessione HTTPS
Server->>Client: Invia il proprio Certificato
Client->>Client: Verifica la firma del Certificato usando la Chiave Pubblica della CA
Client->>Client: Se la firma è valida, stabilisce la connessione sicura
Crittografia a doppia chiave: pubblica e privata
Sezione intitolata “Crittografia a doppia chiave: pubblica e privata”A differenza della crittografia tradizionale (simmetrica) che usa una sola chiave segreta, la crittografia asimmetrica utilizza una coppia di chiavi matematicamente correlate:
- Chiave privata: deve essere tenuta segreta dal proprietario. Serve a firmare documenti o a decifrare messaggi ricevuti.
- Chiave pubblica: può essere distribuita liberamente. Serve a verificare le firme o a cifrare messaggi destinati al proprietario.
La caratteristica fondamentale è che ciò che viene cifrato con una chiave può essere decifrato solo con l’altra chiave della coppia. Questo meccanismo permette due operazioni cruciali:
- Riservatezza: chiunque può cifrare un messaggio con la chiave pubblica del destinatario, ma solo il destinatario (con la sua chiave privata) può leggerlo.
- Autenticità: solo il proprietario può firmare un messaggio con la sua chiave privata, ma chiunque può verificare la firma usando la chiave pubblica.
Cos’è l’hashing
Sezione intitolata “Cos’è l’hashing”Prima di firmare un documento (come un certificato), non si cifra l’intero file, ma si calcola il suo hash (o impronta digitale). L’hashing è una funzione matematica che:
- Trasforma dati di qualsiasi dimensione in una stringa di lunghezza fissa (es. 256 bit)
- È deterministica: gli stessi dati producono sempre lo stesso hash
- È univoca: è praticamente impossibile che due documenti diversi producano lo stesso hash
- È one-way: dall’hash non si possono risalire ai dati originali
Un esempio semplice: se si calcola l’hash della parola “hello” con SHA-256, si ottiene sempre:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Modificare anche una sola lettera (“hallo”) produce un hash completamente diverso.
Il meccanismo della firma digitale
Sezione intitolata “Il meccanismo della firma digitale”La firma digitale combina hashing e crittografia asimmetrica per garantire:
- Autenticità: il documento proviene davvero dal firmatario
- Integrità: il documento non è stato alterato dopo la firma
Procedura di firma (eseguita dalla CA):
- Si calcola l’hash del certificato da firmare
- Si cifra l’hash con la chiave privata della CA
- L’hash cifrato diventa la firma digitale, allegata al certificato
Procedura di verifica (eseguita dal browser):
- Si calcola l’hash del certificato ricevuto
- Si decifra la firma allegata usando la chiave pubblica della CA, ottenendo l’hash originale
- Si confrontano i due hash: se coincidono, il certificato è autentico e integro
Se un attaccante modificasse anche un solo bit del certificato, l’hash ricalcolato dal browser sarebbe diverso da quello decifrato dalla firma, rivelando la manomissione.
La catena di trust e le Certification Authorities (CA)
Sezione intitolata “La catena di trust e le Certification Authorities (CA)”Una domanda sorge spontanea: come fa il browser a sapere che la chiave pubblica usata per verificare la firma appartiene davvero alla CA legittima? La risposta è la catena di trust (chain of trust).
Per motivi di sicurezza, le CA principali non firmano quasi mai direttamente i certificati dei server finali. Utilizzano invece una struttura gerarchica:
graph TD
RootCA[Root CA<br/> - Certificato auto-firmato<br/> preinstallato nel Browser/OS] -->|Firma| IntCA[Intermediate CA <br/>- Certificato Intermedio]
IntCA -->|Firma| ServerCert[Certificato del Server<br/>es. www.esempio.com]
classDef root fill:#f9f,stroke:#333,stroke-width:2px;
classDef int fill:#bbf,stroke:#333,stroke-width:2px;
classDef server fill:#bfb,stroke:#333,stroke-width:2px;
class RootCA root;
class IntCA int;
class ServerCert server;
- Root CA: Il browser o il sistema operativo contiene preinstallate le chiavi pubbliche delle Root CA (autorità di certificazione radice), organizzazioni altamente affidabili e rigorosamente controllate (come Let’s Encrypt, DigiCert, GlobalSign). Il loro certificato è auto-firmato.
- Intermediate CA: Le Root CA firmano i certificati delle Intermediate CA (autorità intermedie). Questo livello intermedio protegge la chiave privata della Root CA (che viene tenuta offline per massima sicurezza).
- Certificato del Server (Leaf Certificate): Le Intermediate CA a loro volta firmano i certificati finali per i server web.
Il browser verifica l’intera catena a ritroso: controlla che il certificato del server sia firmato da un’Intermediate CA valida, e poi verifica che il certificato di quest’ultima sia firmato da una Root CA presente nel proprio archivio di fiducia (trust store). Se l’intera catena è valida e riconduce a una Root CA fidata, il certificato del server è considerato trusted (fidato).
Verifica pratica con cURL
Sezione intitolata “Verifica pratica con cURL”Riferimenti:
Quando si utilizza cURL per accedere a un sito HTTPS, si pone il problema della verifica dell’autenticità del server remoto. Il client deve disporre dei certificati (o delle chiavi pubbliche) delle Certification Authorities (CA) che consentono di validare il certificato digitale presentato dal server, seguendo esattamente il meccanismo descritto sopra.
Ad esempio, quando un browser si connette al sito https://www.istitutogreppi.edu.it, esso verifica il certificato digitale del server:

Riepilogo della procedura di verifica:
- Il browser dispone delle chiavi pubbliche delle Root Certification Authorities (nel caso della scuola, ad esempio, Let’s Encrypt)
- La firma digitale sul certificato viene prodotta dalla CA emittente calcolando l’hash dei dati del certificato e cifrando tale hash con la chiave privata della CA
- Per verificare l’autenticità, il client:
- Ricalcola l’hash dei dati in chiaro del certificato ricevuto
- Decifra la firma digitale usando la chiave pubblica della CA, ottenendo l’hash originale
- Confronta i due hash: se coincidono, il certificato è integro (non manomesso) e l’identità del server è verificata
Ecco uno pseudocodice che illustra la logica di verifica:
// 1. Il client riceve il certificato dal serverCertificate cert = ReceiveCertificateFromServer();
// 2. Il client estrae i dati in chiaro e la firma digitale dal certificatobyte[] datiInChiaro = cert.GetDatiInChiaro();byte[] firmaDigitale = cert.GetFirmaDigitale();
// 3. Il client recupera la chiave pubblica della CA (dal proprio trust store)PublicKey chiavePubblicaCA = GetCAPublicKey(cert.Issuer);
// 4. Il client ricalcola l'hash dei dati in chiarobyte[] hashCalcolato = CalculateHash(datiInChiaro);
// 5. Il client decifra la firma digitale usando la chiave pubblica della CAbyte[] hashOriginale = DecryptSignature(firmaDigitale, chiavePubblicaCA);
// 6. Il client confronta i due hashif (hashCalcolato == hashOriginale){ Console.WriteLine("Certificato valido: integrità e autenticità confermate.");}else{ Console.WriteLine("Certificato NON valido: possibile manomissione o CA non fidata.");}Approfondimenti introduttivi:
Nel caso della versione di cURL preinstallata in Windows (che utilizza Schannel), in genere non si riscontrano problemi di validazione perché viene usato l’elenco delle CA trusted di Windows.
Se invece si utilizza una versione aggiornata di cURL, è necessario configurare opportunamente la sorgente dei certificati, come indicato in https://curl.se/docs/sslcerts.html.
Due alternative comuni sono:
-
utilizzare l’opzione
--ca-native(disponibile in alcune versioni recenti, ad esempio dalla 8.2.0 in poi) per usare sostanzialmente lo store di Windows: https://stackoverflow.com/a/37553616.Un esempio di POST in questo caso è:
Finestra del terminale curl --ca-native --request POST --location 'https://jsonplaceholder.typicode.com/users' `--header 'Content-Type: application/json' `--data '{"name": "Mario Rossi","username": "mrossi","email": "mario.rossi@example.com"}' -
utilizzare un file con i certificati delle CA (opzione
--cacert), ad esempio esportando l’insieme di CA da Mozilla, come indicato in https://curl.se/docs/caextract.html. In tal caso:Finestra del terminale curl --cacert path\to\cert\cacert.pemAd esempio, ipotizzando che
cacert.pemsia nella directory corrente:Finestra del terminale curl --cacert .\cacert.pem --request POST --location 'https://jsonplaceholder.typicode.com/users' `--header 'Content-Type: application/json' `--data '{"name": "Mario Rossi","username": "mrossi","email": "mario.rossi@example.com"}'{"name": "Mario Rossi","username": "mrossi","email": "mario.rossi@example.com","id": 11}Risorse aggiuntive su cURL:
REST Client per Visual Studio Code
Sezione intitolata “REST Client per Visual Studio Code”Cos’è REST Client
Sezione intitolata “Cos’è REST Client”REST Client è un’estensione per Visual Studio Code che permette di inviare richieste HTTP direttamente dall’editor, senza bisogno di strumenti esterni come Postman o cURL. Le richieste vengono scritte in file con estensione .http o .rest e possono essere eseguite con un semplice click.
Installazione
Sezione intitolata “Installazione”- Aprire Visual Studio Code
- Andare nella sezione Estensioni (icona con i quadrati sulla sinistra o
Ctrl+Shift+X) - Cercare “REST Client” di Huachao Mao
- Cliccare su Installa
Perché usare REST Client
Sezione intitolata “Perché usare REST Client”| Vantaggio | Descrizione |
|---|---|
| Integrato nell’IDE | Non serve uscire da VS Code per testare le API |
| File testuali | Le richieste sono codice, salvabile su Git e condivisibile |
| Sintassi semplice | Formato leggibile e immediato |
| Esecuzione rapida | Click destro → “Send Request” o shortcut Ctrl+Alt+R |
| Variabili | Supporta variabili per ambienti diversi (dev, staging, prod) |
| Generazione cURL | Può generare automaticamente il comando cURL equivalente |
Come funziona
Sezione intitolata “Come funziona”- Creare un file con estensione
.http(esempio:api-requests.http) - Scrivere le richieste usando la sintassi semplificata
- Eseguire posizionando il cursore sulla richiesta e premendo
Ctrl+Alt+R(o click destro → “Send Request”) - Visualizzare la risposta in una nuova tab a destra
Sintassi base
Sezione intitolata “Sintassi base”Di seguito esempi completi delle operazioni CRUD usando l’endpoint https://jsonplaceholder.typicode.com/users:
### GET - Recupera tutti gli utentiGET https://jsonplaceholder.typicode.com/users
### GET - Recupera un utente specificoGET https://jsonplaceholder.typicode.com/users/1
### POST - Crea un nuovo utentePOST https://jsonplaceholder.typicode.com/usersContent-Type: application/json
{ "name": "Mario Rossi", "username": "mrossi", "email": "mario.rossi@example.com", "phone": "123-456-7890", "website": "mariorossi.it"}
### PUT - Aggiorna completamente un utentePUT https://jsonplaceholder.typicode.com/users/1Content-Type: application/json
{ "id": 1, "name": "Leanne Graham Modificata", "username": "Bret", "email": "Sincere@april.biz", "phone": "1-770-736-8031 x56442", "website": "hildegard.org"}
### PATCH - Aggiorna parzialmente un utentePATCH https://jsonplaceholder.typicode.com/users/1Content-Type: application/json
{ "email": "nuova.email@example.com"}
### DELETE - Elimina un utenteDELETE https://jsonplaceholder.typicode.com/users/1Generazione di comandi cURL
Sezione intitolata “Generazione di comandi cURL”REST Client può convertire automaticamente qualsiasi richiesta HTTP in un comando cURL equivalente. Questa funzionalità è utile quando si necessita di eseguire la stessa richiesta da terminale o script.
Per generare il comando cURL:
- Posizionare il cursore sulla richiesta desiderata
- Premere
Ctrl+Alt+C(oCmd+Alt+Csu macOS) oppure click destro → “Copy as cURL” - Il comando cURL viene copiato negli appunti
Esempio di conversione:
La richiesta REST Client:
POST https://jsonplaceholder.typicode.com/usersContent-Type: application/json
{ "name": "Mario Rossi", "email": "mario@example.com"}Viene convertita in:
curl -X POST \ https://jsonplaceholder.typicode.com/users \ -H 'Content-Type: application/json' \ -d '{ "name": "Mario Rossi", "email": "mario@example.com"}'Variabili
Sezione intitolata “Variabili”È possibile definire variabili all’inizio del file per rendere le richieste più flessibili:
@baseUrl = https://jsonplaceholder.typicode.com@contentType = application/json
GET {{baseUrl}}/usersContent-Type: {{contentType}}Shortcut da tastiera
Sezione intitolata “Shortcut da tastiera”| Azione | Windows/Linux | macOS |
|---|---|---|
| Invia richiesta | Ctrl+Alt+R | Cmd+Alt+R |
| Invia richiesta e salva risposta | Ctrl+Alt+S | Cmd+Alt+S |
| Genera comando cURL | Ctrl+Alt+C | Cmd+Alt+C |
Documentazione ufficiale
Sezione intitolata “Documentazione ufficiale”- Repository GitHub: https://github.com/Huachao/vscode-restclient
- Marketplace VS Code: Cercare “REST Client” nell’apposita sezione
Utilizzo di un Mock Server per il testing di applicazioni client Importante
Sezione intitolata “Utilizzo di un Mock Server per il testing di applicazioni client ”Durante lo sviluppo di applicazioni client che consumano API REST, si presenta spesso la necessità di testare il codice in assenza di un backend reale funzionante. Un mock server consente di simulare le risposte di un’API, permettendo lo sviluppo parallelo del frontend senza dipendere dalla disponibilità del backend.
Avvio Rapido del Mock Server con json-server
Sezione intitolata “Avvio Rapido del Mock Server con json-server”Per facilitare l’apprendimento e il testing delle applicazioni client, viene fornita una cartella preconfigurata contenente tutti i file necessari per avviare un mock server REST. La cartella è scaricabile dal repository degli esempi: mock-server-tutorial. Per scaricare solo la cartella specifica, è possibile utilizzare un applicativo web come download-directory.github.io oppure il comando git sparse-checkoutper clonare solo la parte desiderata del repository, oppure utilizzare uno degli script ottimizzati indicati nella terza opzione di seguito.
Opzione 1: Scaricare la cartella specifica con la web app download-directory.github.io
Sezione intitolata “Opzione 1: Scaricare la cartella specifica con la web app download-directory.github.io”Se si preferisce non usare i comandi Git, è possibile scaricare solo la cartella specifica utilizzando uno strumento online:
- Visitare https://download-directory.github.io/
- Inserire l’URL della cartella desiderata:
https://github.com/malafronte/malafronte-doc-samples/tree/main/samples-quarta/api_client_server_demos/mock-server-tutorial
- Fare clic su “Download” per ottenere solo la cartella
mock-server-tutorialcome file ZIP
Opzione 2: Clonare solo la cartella specifica con Git Sparse-Checkout
Sezione intitolata “Opzione 2: Clonare solo la cartella specifica con Git Sparse-Checkout”git clone --filter=blob:none --no-checkout https://github.com/malafronte/malafronte-doc-samples.gitcd malafronte-doc-samplesgit sparse-checkout init --conegit sparse-checkout set samples-quarta/api_client_server_demos/mock-server-tutorialgit checkoutOpzione 3: Utilizzare script ottimizzati per scaricare solo la cartella specifica
Sezione intitolata “Opzione 3: Utilizzare script ottimizzati per scaricare solo la cartella specifica”Se si preferisce un approccio ancora semplice e sicuro, è possibile utilizzare uno script che automatizza il download della cartella specifica. Nella cartella tools-and-scripts del repository https://github.com/malafronte/malafronte-doc-samples sono riportati alcuni script sia bash che python che permettono di scaricare solo una cartella specifica da un repository GitHub.
- download-github-folder-optimized.py: Script Python ottimizzato per scaricare solo la cartella desiderata, con gestione dell’autenticazione e degli errori.
- download-github-folder-optimized.sh: Script bash ottimizzato per scaricare solo la cartella desiderata, con gestione dell’autenticazione e degli errori, ideale per ambienti Unix/Linux o Windows con WSL.
Per maggiori dettagli si può consultare la pagina README della cartella.
La cartella distribuita contiene:
package.json: File di configurazione del progetto Node.js con gli script predefinitidb.json: Database JSON contenente i dati mock da esporre tramite APIgenerate-data.js: Script per rigenerare il database con nuovi dati casuali
È importante notare che per iniziare a sviluppare applicazioni client non è necessario comprendere immediatamente i dettagli implementativi del server. La configurazione avanzata e la generazione personalizzata dei dati verranno affrontate nelle sezioni successive. In questa fase introduttiva, l’obiettivo è acquisire familiarità con l’installazione e l’utilizzo base del mock server.
Procedura di Installazione e Configurazione
Sezione intitolata “Procedura di Installazione e Configurazione”La procedura seguente guida l’utente attraverso i passaggi necessari per rendere operativo il mock server sul proprio ambiente di sviluppo locale.
Verifica dei Prerequisiti
Sezione intitolata “Verifica dei Prerequisiti”Prima di procedere, è necessario verificare che il sistema disponga di:
- Node.js installato (versione LTS raccomandata)
- npm (Node Package Manager, installato automaticamente con Node.js)
Per verificare l’installazione, è possibile eseguire in un terminale:
node --versionnpm --versionEntrambi i comandi dovrebbero restituire un numero di versione. In caso contrario, sarà necessario installare Node.js dal sito ufficiale: https://nodejs.org
Apertura della Cartella del Mock Server
Sezione intitolata “Apertura della Cartella del Mock Server”Una volta ottenuta la cartella del mock server, occorre:
- Posizionare la cartella in una location appropriata del file system
- Aprire un terminale (Command Prompt, PowerShell o terminale integrato di VS Code)
- Navigare all’interno della cartella utilizzando il comando
cd
Esempio:
cd C:\path\to\mock-serverIn alternativa, con Visual Studio Code:
- Aprire VS Code
- Selezionare File → Apri Cartella
- Selezionare la cartella del mock server
- Aprire il terminale integrato con Terminale → Nuovo Terminale (o
Ctrl + ò)
Installazione delle Dipendenze (necessario solo se si desidera rigenerare i dati)
Sezione intitolata “Installazione delle Dipendenze (necessario solo se si desidera rigenerare i dati)”Il primo passo consiste nell’installare le dipendenze necessarie. Dal terminale, all’interno della cartella del mock server, eseguire:
npm install json-server@0.17.4 @faker-js/faker --save-devQuesto comando:
- Installa json-server (versione 0.17.4), il tool che trasforma un file JSON in un server REST completo
- Installa @faker-js/faker, la libreria per generare dati casuali realistici
- Salva le dipendenze nella sezione
devDependenciesdel filepackage.json
L’installazione potrebbe richiedere alcuni minuti. Al termine, nella cartella sarà presente una nuova directory node_modules contenente tutte le librerie necessarie.
Durante l’installazione è normale visualizzare messaggi di avviso (warning). Non costituiscono errori critici e il server risulterà comunque funzionante.
Configurazione del proxy di npm (se necessario)
Sezione intitolata “Configurazione del proxy di npm (se necessario)”Nel caso in cui ci si trovi dietro un proxy, potrebbe essere necessario configurare npm per l’accesso a Internet. La configurazione di npm può essere eseguita tramite i comandi npm config.
Configurazione Standard (Senza Autenticazione - caso del proxy scolastico)
Sezione intitolata “Configurazione Standard (Senza Autenticazione - caso del proxy scolastico)”Se il proxy non richiede un nome utente e una password, come nel caso del proxy scolastico (se presente), aprire il terminale ed eseguire i seguenti comandi::
Bash
npm config set proxy http://proxy:3128npm config set https-proxy http://proxy:3128Si noti che per https-proxy, l’indirizzo inizia generalmente con http:// a meno che il proprio server proxy non richieda esplicitamente connessioni sicure).*
Configurazione con Autenticazione
Sezione intitolata “Configurazione con Autenticazione”Se il proxy richiede le anche le credenziali di accesso, occorre includere username e password nell’URL:
Bash
npm config set proxy http://username:password@indirizzo-proxy:portanpm config set https-proxy http://username:password@indirizzo-proxy:portaAttenzione ai caratteri speciali: Se la propria password contiene caratteri speciali (come
@,!,#, ecc.), occorre convertirli nel loro equivalente URL-encoded (ad esempio, la chiocciola@diventa%40).
Come verificare la configurazione
Sezione intitolata “Come verificare la configurazione”Per verificare che le impostazioni siano state salvate correttamente, si può eseguire:
Bash
npm config get proxynpm config get https-proxyOppure per visualizzare tutte le tue impostazioni npm attuali si può eseguire:
Bash
npm config listCome rimuovere il proxy di npm
Sezione intitolata “Come rimuovere il proxy di npm”Se ci si sposta su un’altra rete (ad esempio a casa) e non si ha più bisogno del proxy, occorre ricordarsi di rimuovere queste impostazioni per permettere a npm di funzionare di nuovo normalmente:
Bash
npm config rm proxynpm config rm https-proxy(Opzionale) Rigenerazione del Database
Sezione intitolata “(Opzionale) Rigenerazione del Database”Se si desidera rigenerare il database con nuovi dati casuali, è possibile eseguire lo script di generazione:
npm run generateQuesto comando esegue lo script generate-data.js che sovrascrive il file db.json con nuovi dati generati tramite Faker.js. Questa operazione è completamente opzionale: il file db.json fornito contiene già un dataset funzionante.
Il comando npm run generate sovrascrive completamente il contenuto del file db.json. Eventuali modifiche manuali ai dati andranno perse. Si consiglia di eseguire questo comando solo quando si desidera un reset completo dei dati.
Avvio del Mock Server
Sezione intitolata “Avvio del Mock Server”Per avviare il mock server ed esporre le API REST, eseguire:
npm startSe tutto è configurato correttamente, nel terminale apparirà un output simile al seguente:
\{^_^}/ hi!
Loading db.json Done
Resources http://localhost:3000/companies http://localhost:3000/products
Home http://localhost:3000
Type s + enter at any time to create a snapshot of the database Watching...Il server è ora in esecuzione sulla porta 3000 del localhost. Le risorse esposte sono accessibili tramite i seguenti endpoint:
http://localhost:3000/companies- Collezione delle aziendehttp://localhost:3000/products- Collezione dei prodotti
Verifica del Funzionamento
Sezione intitolata “Verifica del Funzionamento”Per verificare che il server risponda correttamente, è possibile:
Opzione 1: Browser Web
Aprire un browser e navigare all’indirizzo http://localhost:3000/products. Il browser dovrebbe visualizzare un array JSON contenente i dati dei prodotti.
Opzione 2: Postman
Creare una nuova richiesta GET con URL http://localhost:3000/products ed eseguirla. La risposta dovrebbe contenere l’elenco dei prodotti in formato JSON.
Opzione 3: curl (da un altro terminale)
curl http://localhost:3000/productsInterruzione del Server
Sezione intitolata “Interruzione del Server”Per arrestare il mock server, è sufficiente tornare al terminale dove è in esecuzione e premere:
Ctrl + C(su Windows/Linux)Cmd + C(su macOS)
Il server si arresterà immediatamente e non sarà più possibile accedere alle API fino a un nuovo avvio con npm start.
Riepilogo dei Comandi
Sezione intitolata “Riepilogo dei Comandi”| Comando | Descrizione |
|---|---|
npm install json-server@0.17.4 @faker-js/faker --save-dev | Prima installazione delle dipendenze (eseguire una sola volta) |
npm run generate | Rigenera il database con nuovi dati casuali (opzionale) |
npm start | Avvia il mock server sulla porta 3000 |
Ctrl + C | Arresta il mock server |
Funzionalità disponibili per il Client
Sezione intitolata “Funzionalità disponibili per il Client”json-server non si limita a servire dati statici; offre funzionalità REST complete che il client potrà gestire:
-
Filtri:
GET /products?price_gte=10&price_lte=50(Prodotti con prezzo tra 10 e 50). -
Paginazione:
GET /companies?_page=1&_limit=10(Recupera le prime 10 aziende). -
Ordinamento:
GET /products?_sort=price&_order=desc. (Ordina i prodotti per prezzo in ordine decrescente).GET /products?_sort=price&_order=desc&_start=20&_limit=10(Ordina i prodotti per prezzo in ordine decrescente, a partire dal 21° prodotto, limitando il risultato a 10 prodotti).
-
Relazioni:
GET /products?company_id=1(i prodotti dell’azienda 1). -
Full-text Search:
GET /companies?q=California.
L’elenco completo delle funzionalità è disponibile nella documentazione ufficiale: https://github.com/typicode/json-server/tree/v0.17.4
Collection di Postman per Testing
Sezione intitolata “Collection di Postman per Testing”Per facilitare il testing di un client, è possibile creare una collection di Postman che include richieste ed endpoint di test.
Postman Collection - JSON Server Mock API - CRUD Testing
{ "info": { "name": "JSON Server Mock API - CRUD Testing", "description": "Collection per testare le operazioni CRUD su json-server locale. Tutte le richieste usano la variabile {{baseUrl}}.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Products", "description": "Operazioni CRUD sulla collezione Products", "item": [ { "name": "Get All Products", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products", "host": ["{{baseUrl}}"], "path": ["products"] }, "description": "Recupera la lista completa dei prodotti (100 elementi generati)" } }, { "name": "Get Product by ID", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products/1", "host": ["{{baseUrl}}"], "path": ["products", "1"] }, "description": "Recupera il prodotto con ID = 1" } }, { "name": "Create Product", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"name\": \"New Product Sample\",\n \"price\": 149.99,\n \"company_id\": 5\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{baseUrl}}/products", "host": ["{{baseUrl}}"], "path": ["products"] }, "description": "Crea un nuovo prodotto. JSON Server assegnerà automaticamente l'ID incrementale." } }, { "name": "Update Product (PUT)", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"id\": 1,\n \"name\": \"Updated Product Name\",\n \"price\": 199.99,\n \"company_id\": 10\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{baseUrl}}/products/1", "host": ["{{baseUrl}}"], "path": ["products", "1"] }, "description": "Sostituzione completa della risorsa. ATTENZIONE: tutti i campi devono essere inclusi." } }, { "name": "Update Product (PATCH)", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"price\": 89.50\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{baseUrl}}/products/1", "host": ["{{baseUrl}}"], "path": ["products", "1"] }, "description": "Aggiornamento parziale: modifica solo il campo price mantenendo gli altri invariati." } }, { "name": "Delete Product", "request": { "method": "DELETE", "header": [], "url": { "raw": "{{baseUrl}}/products/101", "host": ["{{baseUrl}}"], "path": ["products", "101"] }, "description": "Elimina il prodotto con ID 101 (o ultimo creato). Modifica persistente su db.json." } }, { "name": "Filter Products by Company", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?company_id=5", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "company_id", "value": "5" } ] }, "description": "Filtra tutti i prodotti appartenenti alla compagnia con ID 5" } }, { "name": "Filter Products by Price Range", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?price_gte=100&price_lte=500", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "price_gte", "value": "100" }, { "key": "price_lte", "value": "500" } ] }, "description": "Prodotti con prezzo tra 100 e 500 (operators: gte=greater than equal, lte=less than equal)" } }, { "name": "Sort Products", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?_sort=price&_order=desc", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "_sort", "value": "price" }, { "key": "_order", "value": "desc" } ] }, "description": "Ordinamento per prezzo decrescente (asc per crescente)" } }, { "name": "Paginate Products", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?_page=2&_limit=10", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "_page", "value": "2" }, { "key": "_limit", "value": "10" } ] }, "description": "Paginazione: pagina 2 con 10 elementi per pagina. Header X-Total-Count indica il totale." } }, { "name": "Search Products by Name", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?name_like=Soap", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "name_like", "value": "Soap" } ] }, "description": "Ricerca case-insensitive nel campo name (supporta anche espressioni regolari basiche)" } } ] }, { "name": "Companies", "description": "Operazioni CRUD sulla collezione Companies con dati annidati", "item": [ { "name": "Get All Companies", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/companies", "host": ["{{baseUrl}}"], "path": ["companies"] } } }, { "name": "Get Company by ID", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/companies/1", "host": ["{{baseUrl}}"], "path": ["companies", "1"] } } }, { "name": "Get Company with Embedded Products", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/companies/1?_embed=products", "host": ["{{baseUrl}}"], "path": ["companies", "1"], "query": [ { "key": "_embed", "value": "products" } ] }, "description": "Recupera la compagnia 1 includendo tutti i prodotti associati (company_id = 1) annidati nell'array 'products'" } }, { "name": "Get Companies with Locations Filter", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/companies?locations.city=Jackson", "host": ["{{baseUrl}}"], "path": ["companies"], "query": [ { "key": "locations.city", "value": "Jackson" } ] }, "description": "Filtra compagnie che hanno una location nella città 'Jackson' (filtro su campo annidato)" } }, { "name": "Create Company", "request": { "method": "POST", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"name\": \"NewTech Solutions\",\n \"revenue\": 125000000.50,\n \"headquarter\": {\n \"lat\": 45.4642035,\n \"lon\": 9.189982\n },\n \"locations\": [\n {\n \"city\": \"Milano\",\n \"employee_number\": 450,\n \"state\": \"Lombardia\"\n },\n {\n \"city\": \"Roma\",\n \"employee_number\": 230,\n \"state\": \"Lazio\"\n }\n ]\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{baseUrl}}/companies", "host": ["{{baseUrl}}"], "path": ["companies"] }, "description": "Crea una nuova compagnia con struttura complessa annidata (headquarter e locations array)" } }, { "name": "Update Company Revenue", "request": { "method": "PATCH", "header": [ { "key": "Content-Type", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"revenue\": 150000000.00\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{baseUrl}}/companies/1", "host": ["{{baseUrl}}"], "path": ["companies", "1"] } } }, { "name": "Delete Company", "request": { "method": "DELETE", "header": [], "url": { "raw": "{{baseUrl}}/companies/101", "host": ["{{baseUrl}}"], "path": ["companies", "101"] }, "description": "Elimina compagnia. ATTENZIONE: non elimina i prodotti associati (operazione manuale richiesta o implementazione cascade custom)" } } ] }, { "name": "Advanced Queries & Utilities", "description": "Esempi di query complesse e endpoint di sistema", "item": [ { "name": "Get Full Database (Snapshot)", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/db", "host": ["{{baseUrl}}"], "path": ["db"] }, "description": "Restituisce l'intero database JSON (utile per debug)" } }, { "name": "Slice Data (Range)", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?_start=20&_end=30", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "_start", "value": "20" }, { "key": "_end", "value": "30" } ] }, "description": "Restituisce elementi dall'indice 20 al 30 (escluso). Header Content-Range indica la porzione." } }, { "name": "Full Text Search (All fields)", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products?q=Soap", "host": ["{{baseUrl}}"], "path": ["products"], "query": [ { "key": "q", "value": "Soap" } ] }, "description": "Ricerca full-text su tutti i campi (operatore 'q')" } }, { "name": "Get Products with Company Details (Expand)", "request": { "method": "GET", "header": [], "url": { "raw": "{{baseUrl}}/products/1?_expand=company", "host": ["{{baseUrl}}"], "path": ["products", "1"], "query": [ { "key": "_expand", "value": "company" } ] }, "description": "Recupera il prodotto espandendo il riferimento company_id con i dati completi della compagnia (populate/join)" } } ] } ], "variable": [ { "key": "baseUrl", "value": "http://localhost:3000", "type": "string" } ]}File HTTP di REST Client per il Testing
Sezione intitolata “File HTTP di REST Client per il Testing”Di seguito si riporta il file mock-api-requests.http pronto per l’uso con l’estensione REST Client di Visual Studio Code. Il formato è puramente testuale, leggibile e permette di eseguire le richieste direttamente dall’editor con il tasto destro → “Send Request” (o shortcut Ctrl+Alt+R / Cmd+Alt+R).
REST Client di Visual Studio Code - CRUD Testing
# ============================================================# JSON Server Mock API - REST Client Collection# Estensione VS Code: REST Client (humao.rest-client)# ============================================================
# Variabili di ambiente - modificare se il server gira su porta diversa@baseUrl = http://localhost:3000@contentType = application/json
# ============================================================# PRODUCTS - Operazioni CRUD e filtri avanzati# ============================================================
### 1. Get All Products# Recupera la lista completa dei prodotti (100 elementi generati)GET {{baseUrl}}/products
### 2. Get Product by ID# Recupera il prodotto con ID = 1GET {{baseUrl}}/products/1
### 3. Create Product# Crea un nuovo prodotto. JSON Server assegnerà automaticamente l'ID incrementale.POST {{baseUrl}}/productsContent-Type: {{contentType}}
{ "name": "New Product Sample", "price": 149.99, "company_id": 5}
### 4. Update Product (PUT)# Sostituzione completa della risorsa. ATTENZIONE: tutti i campi devono essere inclusi.PUT {{baseUrl}}/products/1Content-Type: {{contentType}}
{ "id": 1, "name": "Updated Product Name", "price": 199.99, "company_id": 10}
### 5. Update Product (PATCH)# Aggiornamento parziale: modifica solo il campo price mantenendo gli altri invariati.PATCH {{baseUrl}}/products/1Content-Type: {{contentType}}
{ "price": 89.50}
### 6. Delete Product# Elimina il prodotto con ID 101 (o ultimo creato). Modifica persistente su db.json.DELETE {{baseUrl}}/products/101
### 7. Filter Products by Company# Filtra tutti i prodotti appartenenti alla compagnia con ID 5GET {{baseUrl}}/products?company_id=5
### 8. Filter Products by Price Range# Prodotti con prezzo tra 100 e 500# Operators: gte=greater than equal, lte=less than equalGET {{baseUrl}}/products?price_gte=100&price_lte=500
### 9. Sort Products# Ordinamento per prezzo decrescente (usare asc per crescente)GET {{baseUrl}}/products?_sort=price&_order=desc
### 10. Paginate Products# Paginazione: pagina 2 con 10 elementi per pagina.# Header X-Total-Count indica il totale.GET {{baseUrl}}/products?_page=2&_limit=10
### 11. Search Products by Name# Ricerca case-insensitive nel campo name (supporta anche espressioni regolari basiche)GET {{baseUrl}}/products?name_like=Soap
### 12. Full Text Search (All fields)# Ricerca full-text su tutti i campi (operatore 'q')GET {{baseUrl}}/products?q=Soap
### 13. Get Products with Company Details (Expand)# Recupera il prodotto espandendo il riferimento company_id con i dati completi della compagniaGET {{baseUrl}}/products/1?_expand=company
### 14. Slice Data (Range)# Restituisce elementi dall'indice 20 al 30 (escluso).# Header Content-Range indica la porzione.GET {{baseUrl}}/products?_start=20&_end=30
# ============================================================# COMPANIES - Operazioni CRUD con dati annidati# ============================================================
### 15. Get All Companies# Recupera tutte le compagnie con struttura complessa (headquarter e locations)GET {{baseUrl}}/companies
### 16. Get Company by ID# Recupera la compagnia con ID = 1GET {{baseUrl}}/companies/1
### 17. Get Company with Embedded Products# Recupera la compagnia 1 includendo tutti i prodotti associati (company_id = 1)# annidati nell'array 'products'GET {{baseUrl}}/companies/1?_embed=products
### 18. Get Companies with Locations Filter# Filtra compagnie che hanno una location nella città 'Jackson'# (filtro su campo annidato)GET {{baseUrl}}/companies?locations.city=Jackson
### 19. Create Company# Crea una nuova compagnia con struttura complessa annidata# (headquarter e locations array)POST {{baseUrl}}/companiesContent-Type: {{contentType}}
{ "name": "NewTech Solutions", "revenue": 125000000.50, "headquarter": { "lat": 45.4642035, "lon": 9.189982 }, "locations": [ { "city": "Milano", "employee_number": 450, "state": "Lombardia" }, { "city": "Roma", "employee_number": 230, "state": "Lazio" } ]}
### 20. Update Company Revenue (PATCH)# Aggiornamento parziale del fatturatoPATCH {{baseUrl}}/companies/1Content-Type: {{contentType}}
{ "revenue": 150000000.00}
### 21. Delete Company# Elimina compagnia. ATTENZIONE: non elimina i prodotti associatiDELETE {{baseUrl}}/companies/101
# ============================================================# UTILITIES - Endpoint di sistema e debug# ============================================================
### 22. Get Full Database (Snapshot)# Restituisce l'intero database JSON (utile per debug e verifica stato)GET {{baseUrl}}/db
### 23. Health Check (se configurato nelle routes)# Endpoint di verifica stato server (richiede configurazione custom in routes.json)# GET {{baseUrl}}/healthIstruzioni per l’uso del file .http con REST Client
Sezione intitolata “Istruzioni per l’uso del file .http con REST Client”- Installazione: Installare l’estensione “REST Client” (
humao.rest-client) da VS Code Marketplace - Configurazione: Se il server gira su porta diversa dalla 3000, modificare la variabile
@baseUrlin cima al file - Utilizzo:
- Posizionare il cursore sulla richiesta desiderata
- Click destro → “Send Request” (oppure
Ctrl+Alt+R/Cmd+Alt+R) - La risposta appare in una nuova tab a destra con syntax highlighting JSON
- Variabili: Il file usa variabili (
{{baseUrl}}) per facilitare il cambio di ambiente (es. passare ahttp://localhost:3001modificando una sola riga)
Vantaggi rispetto a Postman per il contesto didattico
Sezione intitolata “Vantaggi rispetto a Postman per il contesto didattico”- Version Control: Il file
.httppuò essere committato in Git insieme al codice sorgente del client C# - Leggibilità: Si può vedere la struttura delle richieste senza navigare UI complesse
- Integrazione: Resta dentro l’IDE (VS Code) usato per sviluppare il client C#
- Curl generation: L’estensione permette di generare automaticamente il comando
curlequivalente per ogni richiesta (click su “Copy as cURL”)
Script con curl e jq per testing da terminale
Sezione intitolata “Script con curl e jq per testing da terminale”Di seguito si riporta il file mock-api-curl.sh (o commands.txt) contenente tutti i comandi curl equivalenti, pronti per essere eseguiti nel terminale. I comandi utilizzano la sintassi della versione stabile 0.17.4 (tutte le funzionalità _sort, _order, q, _embed, ecc. sono supportate).
Istruzioni per l’installazione di jq
Sezione intitolata “Istruzioni per l’installazione di jq”-
Pre-requisito: Installare
jqper la formattazione JSON colorata (opzionale ma consigliato):Finestra del terminale # Ubuntu/Debiansudo apt-get install jq# macOSbrew install jq# Windows con wingetwinget install jqlang.jq# Windows (con chocolatey)choco install jq -
Esecuzione singola: Copiare il comando desiderato e incollarlo nel terminale.
-
Senza jq: Se
jqnon è installato, rimuovere| jq .dalla fine di ogni comando per vedere il JSON “raw”. -
Visualizzazione headers: I comandi DELETE e Paginazione usano
-iper mostrare gli header HTTP (utili per vedereX-Total-CounteContent-Range). -
Windows: Su PowerShell usare il carattere backtick
`per il multiline invece di\, oppure eseguire i comandi in una singola riga. Su CMD usare^per il multiline.
Utilizzo di curl senza jq
Sezione intitolata “Utilizzo di curl senza jq”Se si preferisce non usare jq e si vuole un output compatto:
curl -s "http://localhost:3000/products?_sort=price&_order=desc"L’opzione -s (silent) nasconde la barra di progresso di curl.
Se non si vuole installare nulla, si può semplicemente rimuovere | jq . dai comandi curl mostrati precedentemente. Il JSON verrà restituito “sgranato” ma leggibile, oppure si può salvare su file:
curl -s "http://localhost:3000/products" > products.jsonE poi aprire products.json con VS Code o qualsiasi editor che supporti la formattazione JSON.
CRUD Testing con curl e jq
#!/bin/bash# ============================================================# JSON Server Mock API - Curl Commands Collection# Versione: json-server 0.17.x (stabile)# Esecuzione: copiare e incollare i singoli comandi nel terminale# ============================================================
# Variabile base URL (opzionale, per comodità)BASE_URL="http://localhost:3000"
# ============================================================# PRODUCTS - Operazioni CRUD e filtri avanzati# ============================================================
# 1. Get All Products# Recupera la lista completa dei prodotti (100 elementi generati)curl -X GET "${BASE_URL}/products" -H "Content-Type: application/json" | jq .
# 2. Get Product by ID# Recupera il prodotto con ID = 1curl -X GET "${BASE_URL}/products/1" -H "Content-Type: application/json" | jq .
# 3. Create Product# Crea un nuovo prodotto. JSON Server assegnerà automaticamente l'ID incrementale.curl -X POST "${BASE_URL}/products" \ -H "Content-Type: application/json" \ -d '{ "name": "New Product Sample", "price": 149.99, "company_id": 5 }' | jq .
# 4. Update Product (PUT)# Sostituzione completa della risorsa. ATTENZIONE: tutti i campi devono essere inclusi.curl -X PUT "${BASE_URL}/products/1" \ -H "Content-Type: application/json" \ -d '{ "id": 1, "name": "Updated Product Name", "price": 199.99, "company_id": 10 }' | jq .
# 5. Update Product (PATCH)# Aggiornamento parziale: modifica solo il campo price mantenendo gli altri invariati.curl -X PATCH "${BASE_URL}/products/1" \ -H "Content-Type: application/json" \ -d '{ "price": 89.50 }' | jq .
# 6. Delete Product# Elimina il prodotto con ID 101 (o ultimo creato). Modifica persistente su db.json.curl -X DELETE "${BASE_URL}/products/101" -H "Content-Type: application/json" -i
# 7. Filter Products by Company# Filtra tutti i prodotti appartenenti alla compagnia con ID 5curl -X GET "${BASE_URL}/products?company_id=5" -H "Content-Type: application/json" | jq .
# 8. Filter Products by Price Range# Prodotti con prezzo tra 100 e 500 (operators: gte=greater than equal, lte=less than equal)curl -X GET "${BASE_URL}/products?price_gte=100&price_lte=500" -H "Content-Type: application/json" | jq .
# 9. Sort Products# Ordinamento per prezzo decrescente (usare asc per crescente)curl -X GET "${BASE_URL}/products?_sort=price&_order=desc" -H "Content-Type: application/json" | jq .
# 10. Paginate Products# Paginazione: pagina 2 con 10 elementi per pagina. Header X-Total-Count indica il totale.curl -X GET "${BASE_URL}/products?_page=2&_limit=10" -H "Content-Type: application/json" -i
# 11. Search Products by Name# Ricerca case-insensitive nel campo name (supporta anche espressioni regolari basiche)curl -X GET "${BASE_URL}/products?name_like=Soap" -H "Content-Type: application/json" | jq .
# 12. Full Text Search (All fields)# Ricerca full-text su tutti i campi (operatore 'q')curl -X GET "${BASE_URL}/products?q=Soap" -H "Content-Type: application/json" | jq .
# 13. Get Products with Company Details (Expand)# Recupera il prodotto espandendo il riferimento company_id con i dati completi della compagniacurl -X GET "${BASE_URL}/products/1?_expand=company" -H "Content-Type: application/json" | jq .
# 14. Slice Data (Range)# Restituisce elementi dall'indice 20 al 30 (escluso). Header Content-Range indica la porzione.curl -X GET "${BASE_URL}/products?_start=20&_end=30" -H "Content-Type: application/json" -i
# ============================================================# COMPANIES - Operazioni CRUD con dati annidati# ============================================================
# 15. Get All Companies# Recupera tutte le compagnie con struttura complessa (headquarter e locations)curl -X GET "${BASE_URL}/companies" -H "Content-Type: application/json" | jq .
# 16. Get Company by ID# Recupera la compagnia con ID = 1curl -X GET "${BASE_URL}/companies/1" -H "Content-Type: application/json" | jq .
# 17. Get Company with Embedded Products# Recupera la compagnia 1 includendo tutti i prodotti associati annidati nell'array 'products'curl -X GET "${BASE_URL}/companies/1?_embed=products" -H "Content-Type: application/json" | jq .
# 18. Get Companies with Locations Filter# Filtra compagnie che hanno una location nella città 'Jackson' (filtro su campo annidato)curl -X GET "${BASE_URL}/companies?locations.city=Jackson" -H "Content-Type: application/json" | jq .
# 19. Create Company# Crea una nuova compagnia con struttura complessa annidata (headquarter e locations array)curl -X POST "${BASE_URL}/companies" \ -H "Content-Type: application/json" \ -d '{ "name": "NewTech Solutions", "revenue": 125000000.50, "headquarter": { "lat": 45.4642035, "lon": 9.189982 }, "locations": [ { "city": "Milano", "employee_number": 450, "state": "Lombardia" }, { "city": "Roma", "employee_number": 230, "state": "Lazio" } ] }' | jq .
# 20. Update Company Revenue (PATCH)# Aggiornamento parziale del fatturatocurl -X PATCH "${BASE_URL}/companies/1" \ -H "Content-Type: application/json" \ -d '{ "revenue": 150000000.00 }' | jq .
# 21. Delete Company# Elimina compagnia. ATTENZIONE: non elimina i prodotti associaticurl -X DELETE "${BASE_URL}/companies/101" -H "Content-Type: application/json" -i
# ============================================================# UTILITIES - Endpoint di sistema e debug# ============================================================
# 22. Get Full Database (Snapshot)# Restituisce l'intero database JSON (utile per debug e verifica stato)curl -X GET "${BASE_URL}/db" -H "Content-Type: application/json" | jq .
# 23. Health Check (se configurato nelle routes)# Endpoint di verifica stato server (richiede configurazione custom in routes.json)# curl -X GET "${BASE_URL}/health" -H "Content-Type: application/json"Sviluppo dell’Applicazione Client
Sezione intitolata “Sviluppo dell’Applicazione Client”Una volta avviato il mock server, è possibile iniziare immediatamente lo sviluppo dell’applicazione client. Gli endpoint REST sono pienamente funzionali e supportano tutte le operazioni CRUD standard:
- GET
/products- Recupera tutti i prodotti - GET
/products/1- Recupera il prodotto con ID 1 - POST
/products- Crea un nuovo prodotto - PUT
/products/1- Aggiorna completamente il prodotto con ID 1 - PATCH
/products/1- Aggiorna parzialmente il prodotto con ID 1 - DELETE
/products/1- Elimina il prodotto con ID 1
Lo stesso vale per la risorsa /companies.
Il mock server opera esclusivamente in locale (localhost) e non è accessibile dall’esterno della propria macchina. Ciò lo rende ideale per lo sviluppo e il testing in sicurezza, senza rischi di esposizione o conflitti con servizi di produzione.
Con il mock server in esecuzione, è ora possibile concentrarsi completamente sull’implementazione della logica client, sulla gestione delle richieste HTTP e sulla manipolazione dei dati ricevuti, senza preoccuparsi dell’infrastruttura backend.
Esempio Completo di Client Console in C#
Sezione intitolata “Esempio Completo di Client Console in C#”Di seguito viene presentata un’implementazione completa di un’applicazione console C# che interagisce con il mock server tramite HttpClient. L’esempio dimostra tutte le operazioni CRUD fondamentali e l’utilizzo di System.Text.Json per la serializzazione e deserializzazione degli oggetti.
Definizione delle Classi Modello
Sezione intitolata “Definizione delle Classi Modello”Le classi modello rappresentano la struttura dei dati esposti dal mock server. È fondamentale che i nomi delle proprietà corrispondano esattamente ai campi JSON (o utilizzare attributi [JsonPropertyName] per il mapping).
//file DataModels.csnamespace RestApiClient;
using System.Text.Json.Serialization;
// Classe che rappresenta un prodottopublic class Product{ [JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
[JsonPropertyName("price")] public decimal Price { get; set; }
[JsonPropertyName("company_id")] public int CompanyId { get; set; }
public override string ToString() { return $"[{Id}] {Name} - €{Price:F2} (Company: {CompanyId})"; }}
// Classe che rappresenta le coordinate geografiche della sede principalepublic class Headquarter{ [JsonPropertyName("lat")] public double Lat { get; set; }
[JsonPropertyName("lon")] public double Lon { get; set; }
public override string ToString() { return $"Lat: {Lat:F6}, Lon: {Lon:F6}"; }}
// Classe che rappresenta una location/filialepublic class Location{ [JsonPropertyName("city")] public string City { get; set; } = string.Empty;
[JsonPropertyName("employee_number")] public int EmployeeNumber { get; set; }
[JsonPropertyName("state")] public string State { get; set; } = string.Empty;
public override string ToString() { return $"{City}, {State} ({EmployeeNumber} dipendenti)"; }}
// Classe che rappresenta un'aziendapublic class Company{ [JsonPropertyName("id")] public int Id { get; set; }
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
[JsonPropertyName("revenue")] public decimal Revenue { get; set; }
[JsonPropertyName("headquarter")] public Headquarter Headquarter { get; set; } = new();
[JsonPropertyName("locations")] public List<Location> Locations { get; set; } = new();
public override string ToString() { return $"[{Id}] {Name} - Revenue: €{Revenue:N2}"; }}Implementazione del Client REST
Sezione intitolata “Implementazione del Client REST”La classe ApiClient incapsula la logica di comunicazione con il mock server, fornendo metodi asincroni per tutte le operazioni CRUD.
//file ApiClient.csusing System.Net.Http.Json;using System.Text;using System.Text.Json;
namespace RestApiClient;
public class ApiClient{ private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly JsonSerializerOptions _jsonOptions;
public ApiClient(string baseUrl = "http://localhost:3000") { _baseUrl = baseUrl; _httpClient = new HttpClient { BaseAddress = new Uri(_baseUrl) };
// Configurazione opzioni JSON con formattazione leggibile _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = true }; }
// ==================== OPERAZIONI SUI PRODOTTI ====================
/// <summary> /// Recupera tutti i prodotti dal server /// </summary> public async Task<List<Product>?> GetAllProductsAsync() { try { var response = await _httpClient.GetAsync("/products"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Product>>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore HTTP: {ex.Message}"); return null; } }
/// <summary> /// Recupera un prodotto specifico tramite ID /// </summary> public async Task<Product?> GetProductByIdAsync(int id) { try { return await _httpClient.GetFromJsonAsync<Product>($"/products/{id}", _jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante il recupero del prodotto {id}: {ex.Message}"); return null; } }
/// <summary> /// Crea un nuovo prodotto sul server /// </summary> public async Task<Product?> CreateProductAsync(Product product) { try { var response = await _httpClient.PostAsJsonAsync("/products", product, _jsonOptions); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<Product>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante la creazione del prodotto: {ex.Message}"); return null; } }
/// <summary> /// Aggiorna completamente un prodotto esistente (PUT) /// </summary> public async Task<Product?> UpdateProductAsync(int id, Product product) { try { var response = await _httpClient.PutAsJsonAsync($"/products/{id}", product, _jsonOptions); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<Product>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante l'aggiornamento del prodotto {id}: {ex.Message}"); return null; } }
/// <summary> /// Aggiorna parzialmente un prodotto (PATCH) /// </summary> public async Task<Product?> PatchProductAsync(int id, object partialUpdate) { try { var json = JsonSerializer.Serialize(partialUpdate, _jsonOptions); var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PatchAsync($"/products/{id}", content); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<Product>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante l'aggiornamento parziale del prodotto {id}: {ex.Message}"); return null; } }
/// <summary> /// Elimina un prodotto dal server /// </summary> public async Task<bool> DeleteProductAsync(int id) { try { var response = await _httpClient.DeleteAsync($"/products/{id}"); return response.IsSuccessStatusCode; } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante l'eliminazione del prodotto {id}: {ex.Message}"); return false; } }
// ==================== OPERAZIONI SULLE COMPAGNIE ====================
/// <summary> /// Recupera tutte le compagnie dal server /// </summary> public async Task<List<Company>?> GetAllCompaniesAsync() { try { var response = await _httpClient.GetAsync("/companies"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Company>>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore HTTP: {ex.Message}"); return null; } }
/// <summary> /// Recupera una compagnia specifica tramite ID /// </summary> public async Task<Company?> GetCompanyByIdAsync(int id) { try { return await _httpClient.GetFromJsonAsync<Company>($"/companies/{id}", _jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante il recupero della compagnia {id}: {ex.Message}"); return null; } }
/// <summary> /// Recupera i prodotti filtrati per compagnia /// </summary> public async Task<List<Product>?> GetProductsByCompanyAsync(int companyId) { try { var response = await _httpClient.GetAsync($"/products?company_id={companyId}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Product>>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante il recupero dei prodotti della compagnia {companyId}: {ex.Message}"); return null; } }
// ==================== OPERAZIONI AVANZATE ====================
/// <summary> /// Ricerca prodotti per nome (full-text search) /// </summary> public async Task<List<Product>?> SearchProductsAsync(string searchTerm) { try { var response = await _httpClient.GetAsync($"/products?q={Uri.EscapeDataString(searchTerm)}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Product>>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante la ricerca: {ex.Message}"); return null; } }
/// <summary> /// Recupera prodotti con paginazione /// </summary> public async Task<List<Product>?> GetProductsPaginatedAsync(int page, int limit) { try { var response = await _httpClient.GetAsync($"/products?_page={page}&_limit={limit}"); response.EnsureSuccessStatusCode();
// L'header X-Total-Count contiene il numero totale di elementi if (response.Headers.TryGetValues("X-Total-Count", out var values)) { Console.WriteLine($"Totale prodotti disponibili: {values.First()}"); }
return await response.Content.ReadFromJsonAsync<List<Product>>(_jsonOptions); } catch (HttpRequestException ex) { Console.WriteLine($"Errore durante il recupero paginato: {ex.Message}"); return null; } }}Programma di Esempio con Utilizzo del Client
Sezione intitolata “Programma di Esempio con Utilizzo del Client”Il seguente programma principale dimostra l’utilizzo pratico di tutte le funzionalità del client REST.
//file Program.csusing System.Text;using RestApiClient;class Program{ static async Task Main(string[] args) { Console.OutputEncoding= Encoding.UTF8; Console.WriteLine("=== Client REST per Mock Server JSON-Server ===\n");
var client = new ApiClient();
// ========== GET: Recupera tutti i prodotti ========== Console.WriteLine("1. RECUPERO DI TUTTI I PRODOTTI"); Console.WriteLine("".PadRight(50, '-')); var products = await client.GetAllProductsAsync(); if (products != null && products.Count != 0) { Console.WriteLine($"Recuperati {products.Count} prodotti:"); foreach (var product in products.Take(5)) // Mostra solo i primi 5 { Console.WriteLine($" {product}"); } Console.WriteLine($" ... (altri {products.Count - 5} prodotti omessi)\n"); }
// ========== GET by ID: Recupera un prodotto specifico ========== Console.WriteLine("2. RECUPERO PRODOTTO SPECIFICO (ID=1)"); Console.WriteLine("".PadRight(50, '-')); var product1 = await client.GetProductByIdAsync(1); if (product1 != null) { Console.WriteLine($" {product1}\n"); }
// ========== POST: Crea un nuovo prodotto ========== Console.WriteLine("3. CREAZIONE DI UN NUOVO PRODOTTO"); Console.WriteLine("".PadRight(50, '-')); var newProduct = new Product { Name = "Tastiera Meccanica RGB", Price = 129.99m, CompanyId = 5 }; var createdProduct = await client.CreateProductAsync(newProduct); if (createdProduct != null) { Console.WriteLine($" Prodotto creato: {createdProduct}\n"); }
// ========== PUT: Aggiorna completamente un prodotto ========== Console.WriteLine("4. AGGIORNAMENTO COMPLETO PRODOTTO (PUT)"); Console.WriteLine("".PadRight(50, '-')); if (createdProduct != null) { createdProduct.Name = "Tastiera Meccanica RGB Pro"; createdProduct.Price = 159.99m; var updatedProduct = await client.UpdateProductAsync(createdProduct.Id, createdProduct); if (updatedProduct != null) { Console.WriteLine($" Prodotto aggiornato: {updatedProduct}\n"); } }
// ========== PATCH: Aggiorna parzialmente un prodotto ========== Console.WriteLine("5. AGGIORNAMENTO PARZIALE PRODOTTO (PATCH)"); Console.WriteLine("".PadRight(50, '-')); if (createdProduct != null) { var partialUpdate = new { price = 139.99m }; var patchedProduct = await client.PatchProductAsync(createdProduct.Id, partialUpdate); if (patchedProduct != null) { Console.WriteLine($" Prodotto aggiornato (PATCH): {patchedProduct}\n"); } }
// ========== GET: Recupera compagnie ========== Console.WriteLine("6. RECUPERO DELLE COMPAGNIE"); Console.WriteLine("".PadRight(50, '-')); var companies = await client.GetAllCompaniesAsync(); if (companies != null && companies.Any()) { Console.WriteLine($"Recuperate {companies.Count} compagnie:"); foreach (var company in companies.Take(3)) { Console.WriteLine($" {company}"); Console.WriteLine($" Sede: {company.Headquarter}"); Console.WriteLine($" Locations: {company.Locations.Count}"); } Console.WriteLine(); }
// ========== GET: Recupera prodotti per compagnia ========== Console.WriteLine("7. PRODOTTI DELLA COMPAGNIA ID=5"); Console.WriteLine("".PadRight(50, '-')); var companyProducts = await client.GetProductsByCompanyAsync(5); if (companyProducts != null) { Console.WriteLine($"Trovati {companyProducts.Count} prodotti:"); foreach (var prod in companyProducts.Take(3)) { Console.WriteLine($" {prod}"); } Console.WriteLine(); }
// ========== SEARCH: Ricerca full-text ========== Console.WriteLine("8. RICERCA PRODOTTI (full-text: 'Soap')"); Console.WriteLine("".PadRight(50, '-')); var searchResults = await client.SearchProductsAsync("Soap"); if (searchResults != null) { Console.WriteLine($"Trovati {searchResults.Count} risultati:"); foreach (var prod in searchResults.Take(3)) { Console.WriteLine($" {prod}"); } Console.WriteLine(); }
// ========== PAGINATION: Recupera con paginazione ========== Console.WriteLine("9. PAGINAZIONE (Pagina 2, 10 elementi)"); Console.WriteLine("".PadRight(50, '-')); var paginatedProducts = await client.GetProductsPaginatedAsync(2, 10); if (paginatedProducts != null) { Console.WriteLine($"Prodotti della pagina 2:"); foreach (var prod in paginatedProducts) { Console.WriteLine($" {prod}"); } Console.WriteLine(); }
// ========== DELETE: Elimina il prodotto creato ========== Console.WriteLine("10. ELIMINAZIONE PRODOTTO CREATO"); Console.WriteLine("".PadRight(50, '-')); if (createdProduct != null) { var deleted = await client.DeleteProductAsync(createdProduct.Id); Console.WriteLine(deleted ? $" Prodotto {createdProduct.Id} eliminato con successo\n" : $" Errore nell'eliminazione del prodotto {createdProduct.Id}\n"); }
Console.WriteLine("=== Test completato ==="); Console.WriteLine("\nPremere un tasto per terminare..."); Console.ReadKey(); }}Note Implementative
Sezione intitolata “Note Implementative”Gestione degli Errori: Il client implementa una gestione base delle eccezioni HTTP. In un contesto di produzione, sarebbe opportuno implementare:
- Retry policies con Polly: Quando si consumano API REST, errori temporanei come timeout di rete, errori 503 (Service Unavailable) o 429 (Too Many Requests) sono comuni. Una retry policy (politica di ritentativo) definisce automaticamente quante volte ripetere una richiesta fallita e con quale strategia di attesa (es. attesa fissa, attesa esponenziale crescente). Ad esempio, si può configurare Polly per riprovare fino a 3 volte con un delay crescente (1s, 2s, 4s) quando si riceve un errore 5xx, aumentando la probabilità di successo senza interrompere l’applicazione.
- Circuit breaker per resilienza: Il pattern Circuit Breaker (interruttore automatico) previene il sovraccarico di un servizio già in difficoltà. Quando un numero configurato di richieste consecutive fallisce (es. 5 errori), il circuito si “apre” e le richieste successive vengono bloccate immediatamente per un periodo di tempo (es. 30 secondi), senza nemmeno tentare di contattare il servizio. Dopo il periodo di “cooldown”, il circuito entra in stato “half-open” e permette una richiesta di test: se ha successo, il circuito si “chiude” e il flusso normale riprende. Questo protegge sia il client (evitando attese inutili) sia il server (dando tempo di recupero).
- Logging strutturato: A differenza del logging tradizionale basato su stringhe di testo libero (es.
Console.WriteLine($"Errore: {ex.Message}")), il logging strutturato utilizza librerie come Serilog o NLog per registrare eventi come oggetti dati con proprietà chiave-valore. Ogni log entry include campi standardizzati come timestamp, livello (Debug/Info/Warning/Error), nome dell’applicazione, e metadati specifici dell’evento (es.HttpMethod,Url,StatusCode,ResponseTimeMs). Questo permette di filtrare, aggregare e analizzare i log in modo efficiente con strumenti come Elasticsearch, Splunk o Azure Application Insights, facilitando il debug e il monitoraggio in produzione.
Serializzazione JSON: L’utilizzo di System.Text.Json garantisce prestazioni ottimali e integrazione nativa con .NET moderno. Gli attributi [JsonPropertyName] permettono il mapping tra la convenzione snake_case di JSON e PascalCase di C#.
Pattern Asincrono: Tutte le operazioni di I/O sono asincrone (async/await), rispettando le best practices per applicazioni scalabili e responsive.
HttpClient: L’istanza di HttpClient viene riutilizzata per tutte le richieste, evitando l’esaurimento delle socket (socket exhaustion) che si verificherebbe creando un nuovo client per ogni richiesta.
Prima di eseguire il client C#, assicurarsi che il mock server sia in esecuzione con npm start. Il client tenterà di connettersi a http://localhost:3000 per impostazione predefinita.
Configurazione Avanzata degli Header HTTP
Sezione intitolata “Configurazione Avanzata degli Header HTTP”Nella sezione precedente, l’implementazione del client REST non include una configurazione esplicita degli header HTTP personalizzati. Questa scelta è stata adottata intenzionalmente per motivi didattici e per la natura permissiva del mock server json-server. Tuttavia, quando si interagisce con API reali pubbliche o aziendali, la corretta configurazione degli header HTTP diventa fondamentale e spesso obbligatoria per il funzionamento dell’applicazione.
Perché l’Esempio Base Funziona Senza Header Personalizzati
Sezione intitolata “Perché l’Esempio Base Funziona Senza Header Personalizzati”Il mock server json-server è stato progettato per facilitare lo sviluppo e il testing, pertanto accetta richieste HTTP anche senza header specifici. Quando non vengono configurati esplicitamente, HttpClient utilizza i seguenti valori predefiniti:
GET /products HTTP/1.1Host: localhost:3000Connection: keep-aliveQuesti header minimi sono sufficienti per json-server, che:
- Non richiede autenticazione
- Non verifica l’header
Acceptper il content negotiation - Non richiede identificazione del client tramite
User-Agent - Risponde sempre con
application/jsonindipendentemente dalla richiesta
Questa permissività è ideale per l’apprendimento iniziale, consentendo di concentrarsi sulla logica HTTP (GET, POST, PUT, DELETE) senza la complessità aggiuntiva della gestione degli header.
Quando È Necessario Configurare Gli Header
Sezione intitolata “Quando È Necessario Configurare Gli Header”Quasi tutte le API pubbliche moderne richiedono header aggiuntivi per motivi di sicurezza, versioning, identificazione e ottimizzazione. Ecco i casi più comuni:
| Scenario | Header Richiesto | Esempio Pratico |
|---|---|---|
| Autenticazione | Authorization o X-API-Key | GitHub API, OpenWeather, Google Maps API |
| Content Negotiation | Accept | API che supportano XML e JSON, versionamento (es. application/vnd.github.v3+json) |
| Identificazione Client | User-Agent | GitHub API (rifiuta richieste senza User-Agent) |
| CORS e sicurezza | Origin, Referer | API chiamate da browser web |
| Rate Limiting | X-RateLimit-* | Molte API restituiscono questi header per informare sui limiti |
| Compressione | Accept-Encoding | Riduce il traffico di rete (gzip, deflate, br) |
Tipi di Header in HttpClient
Sezione intitolata “Tipi di Header in HttpClient”HttpClient permette di configurare gli header in due modalità distinte:
1. DefaultRequestHeaders - Header Globali
Gli header configurati in DefaultRequestHeaders vengono automaticamente aggiunti a tutte le richieste effettuate da quell’istanza di HttpClient. Sono ideali per:
- Token di autenticazione utilizzati in tutte le chiamate
- User-Agent costante per l’applicazione
- Accept header che non varia tra le richieste
2. Header Per Singola Richiesta
È possibile aggiungere header specifici a una singola richiesta HTTP, utile quando:
- Solo alcune chiamate richiedono autenticazione speciale
- Si vuole sovrascrivere temporaneamente un header globale
- Ogni richiesta necessita di un timestamp o token univoco
Esempio 1: Configurazione Base con GitHub API
Sezione intitolata “Esempio 1: Configurazione Base con GitHub API”L’API di GitHub rappresenta un esempio reale che richiede obbligatoriamente header specifici. Senza di essi, le richieste vengono rifiutate con errore 403 Forbidden.
using System.Net.Http.Headers;
public class GitHubApiClient{ private readonly HttpClient _httpClient;
public GitHubApiClient() { _httpClient = new HttpClient { BaseAddress = new Uri("https://api.github.com") };
// 1. Header Accept per specificare la versione API e il formato _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/vnd.github.v3+json"));
// 2. User-Agent è OBBLIGATORIO per GitHub API // Formato raccomandato: NomeApp/Versione _httpClient.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
// 3. (Opzionale) Token di autenticazione per accesso autenticato // Aumenta il rate limit da 60 a 5000 richieste/ora // _httpClient.DefaultRequestHeaders.Authorization = // new AuthenticationHeaderValue("Bearer", "YOUR_GITHUB_TOKEN"); }
public async Task<string?> GetUserProfileAsync(string username) { try { var response = await _httpClient.GetAsync($"/users/{username}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } catch (HttpRequestException ex) { Console.WriteLine($"Errore: {ex.Message}"); return null; } }}Spiegazione degli Header GitHub:
-
Accept: application/vnd.github.v3+json: Indica al server di utilizzare la versione 3 dell’API GitHub e di rispondere con JSON. Il prefissovnd.(vendor) identifica un media type proprietario. -
User-Agent: MyApp/1.0: Identifica l’applicazione client. GitHub traccia l’utilizzo delle API tramite questo header e rifiuta richieste senza User-Agent per prevenire abusi. -
Authorization: Bearer TOKEN: Opzionale per endpoint pubblici, obbligatorio per operazioni autenticate (es. creazione repository, accesso a dati privati).
Esempio 2: API con Chiave Personalizzata (Custom API Key)
Sezione intitolata “Esempio 2: API con Chiave Personalizzata (Custom API Key)”Molte API commerciali utilizzano header personalizzati per l’autenticazione invece dello standard Authorization. Questo pattern è comune in servizi come OpenWeather, Postman Mock API, e molti servizi enterprise.
public class CustomApiClient{ private readonly HttpClient _httpClient; private readonly string _apiKey;
public CustomApiClient(string baseUrl, string apiKey) { _apiKey = apiKey; _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
// Header personalizzato per API Key _httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey);
// Accept standard per JSON _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json"));
// User-Agent per identificazione _httpClient.DefaultRequestHeaders.Add("User-Agent", "CustomApiClient/2.0"); }
public async Task<T?> GetAsync<T>(string endpoint) { try { var response = await _httpClient.GetAsync(endpoint); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<T>(); } catch (HttpRequestException ex) { Console.WriteLine($"Errore: {ex.Message}"); return default; } }}
// Utilizzovar client = new CustomApiClient( "https://api.example.com", "your-secret-api-key-here");Esempio 3: Header Per Singola Richiesta
Sezione intitolata “Esempio 3: Header Per Singola Richiesta”In alcuni scenari è necessario aggiungere o sovrascrivere header solo per una specifica chiamata, ad esempio quando si caricano file o si invia un formato diverso.
public class FlexibleApiClient{ private readonly HttpClient _httpClient;
public FlexibleApiClient(string baseUrl) { _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
// Configurazione base per JSON _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); }
/// <summary> /// POST standard con JSON /// </summary> public async Task<Product?> CreateProductAsync(Product product) { var response = await _httpClient.PostAsJsonAsync("/products", product); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<Product>(); }
/// <summary> /// POST con XML (header specifico per questa richiesta) /// </summary> public async Task<string?> SendXmlDataAsync(string xmlContent) { var content = new StringContent(xmlContent, Encoding.UTF8, "application/xml");
// Crea una richiesta con header personalizzato var request = new HttpRequestMessage(HttpMethod.Post, "/data") { Content = content };
// Aggiunge header specifico solo per questa richiesta request.Headers.Add("X-Custom-Header", "special-value");
var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }
/// <summary> /// Richiesta con token temporaneo (non salvato in DefaultRequestHeaders) /// </summary> public async Task<string?> GetSecureDataAsync(string temporaryToken) { var request = new HttpRequestMessage(HttpMethod.Get, "/secure/data"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", temporaryToken);
var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }}Esempio 4: Integrazione con Mock Server e Header Personalizzati
Sezione intitolata “Esempio 4: Integrazione con Mock Server e Header Personalizzati”Per preparare il client all’integrazione con API reali mantenendo la compatibilità con json-server, è possibile adottare un approccio ibrido:
public class ProductionReadyApiClient{ private readonly HttpClient _httpClient; private readonly bool _isProduction;
public ProductionReadyApiClient(string baseUrl, string? apiKey = null, bool isProduction = false) { _isProduction = isProduction; _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
// Header comuni sempre presenti _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Add( "User-Agent", "ProductApiClient/1.0");
// Header di autenticazione solo in produzione if (_isProduction && !string.IsNullOrEmpty(apiKey)) { _httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey); Console.WriteLine("Client configurato per ambiente di PRODUZIONE"); } else { Console.WriteLine("Client configurato per ambiente di SVILUPPO (Mock Server)"); } }
public async Task<List<Product>?> GetProductsAsync() { try { var response = await _httpClient.GetAsync("/products");
// Log degli header di risposta (utile per debugging) if (_isProduction) { LogResponseHeaders(response); }
response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Product>>(); } catch (HttpRequestException ex) { Console.WriteLine($"Errore HTTP: {ex.Message}"); return null; } }
private void LogResponseHeaders(HttpResponseMessage response) { Console.WriteLine("\n--- Response Headers ---"); foreach (var header in response.Headers) { Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}"); }
// Header comuni per rate limiting if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var remaining)) { Console.WriteLine($"Rate Limit Remaining: {remaining.First()}"); } }}
// Utilizzo in sviluppo (con json-server locale)var devClient = new ProductionReadyApiClient("http://localhost:3000");
// Utilizzo in produzione (con API reale)var prodClient = new ProductionReadyApiClient( "https://api.production.com", apiKey: "your-production-key", isProduction: true);Best Practices per la Gestione degli Header
Sezione intitolata “Best Practices per la Gestione degli Header”-
Non codificare mai API Key nel codice sorgente: Utilizzare variabili d’ambiente, file di configurazione esterni o Azure Key Vault.
var apiKey = Environment.GetEnvironmentVariable("API_KEY")?? throw new InvalidOperationException("API_KEY non configurata"); -
Riutilizzare l’istanza di HttpClient: Come già implementato negli esempi, una singola istanza va riutilizzata per tutta l’applicazione.
-
Verificare sempre gli header di risposta: Molte API forniscono informazioni utili negli header di risposta (rate limiting, pagination, deprecation warnings).
-
Documentare gli header obbligatori: Quando si sviluppa un client per API di terze parti, documentare quali header sono obbligatori e quali opzionali.
-
Testare con e senza autenticazione: Verificare che l’applicazione gestisca correttamente errori 401 (Unauthorized) e 403 (Forbidden).
Confronto: Mock Server vs API Reali
Sezione intitolata “Confronto: Mock Server vs API Reali”| Aspetto | json-server (Mock) | API Reali |
|---|---|---|
| Header Accept | Opzionale, risponde sempre JSON | Spesso obbligatorio per versioning |
| User-Agent | Ignorato | Spesso obbligatorio (GitHub, molte API pubbliche) |
| Autenticazione | Non richiesta | Quasi sempre obbligatoria (API Key, OAuth, JWT) |
| Rate Limiting | Assente | Presente con header X-RateLimit-* |
| CORS | Permissivo | Restrittivo per sicurezza |
| Certificati HTTPS | Non necessari (HTTP) | Obbligatori, validazione del certificato |
Una strategia consigliata è sviluppare inizialmente contro il mock server senza header complessi, poi introdurre gradualmente la configurazione degli header quando si passa a testare contro un ambiente di staging o produzione. Questo approccio incrementale facilita il debugging e la comprensione dei requisiti specifici dell’API target.
Risorse per Approfondimento
Sezione intitolata “Risorse per Approfondimento”- Documentazione Microsoft HttpClient: HttpClient Class
- HTTP Headers Reference: MDN Web Docs - HTTP Headers
- GitHub API Documentation: GitHub REST API
- Best Practices HttpClient: HttpClient Guidelines
Con questa comprensione della configurazione degli header HTTP, il client REST diventa pronto per interagire con qualsiasi API professionale, mantenendo al contempo la flessibilità necessaria per ambienti di sviluppo e testing.
Un esempio completo di client C# con backend mock JSON-Server e supporto all’autenticazione mediante token Importante
Sezione intitolata “Un esempio completo di client C# con backend mock JSON-Server e supporto all’autenticazione mediante token ”L’esempio proposto illustra una progressione didattica nell’integrazione di client C# con API REST, partendo da un’implementazione base fino a raggiungere un sistema con autenticazione JWT:
- Nella prima fase, il progetto mock-server-products implementa un server mock basato su json-server che espone un’API RESTful completa per la gestione di prodotti. Questo server supporta tutte le operazioni CRUD (GET, POST, PUT, PATCH, DELETE) sulla risorsa
/products, utilizza dati iniziali provenienti da un file JSON e include risorse multimediali locali (immagini, miniature e QR code) servite dalla cartellapublic/assets/products_media. Il server si avvia sulla porta 3000 e offre anche la possibilità di generare dataset casuali tramiteFaker.js. L’applicazione ApiClient rappresenta il client C# che interagisce con questo server, dimostrando come effettuare richieste HTTP e gestire le risposte RESTful in un ambiente di testing controllato. - Passando alla seconda fase, il progetto mock-server-auth estende il server mock introducendo un sistema di autenticazione basato su JWT (JSON Web Token). In questa configurazione, le operazioni di lettura (GET) rimangono pubblicamente accessibili, mentre tutte le operazioni di modifica (POST, PUT, PATCH, DELETE) richiedono un token JWT valido nell’header
Authorization. Il server espone un endpoint/loginche, ricevute credenziali valide (email e password), restituisce un token JWT da utilizzare nelle richieste successive. Il client ApiClientAuth implementa la logica necessaria per gestire questo flusso di autenticazione: effettua il login inviando le credenziali, estrae e memorizza il token JWT ricevuto, configura l’istanza diHttpClientper includere automaticamente il token nell’headerAuthorization(con schema Bearer) e procede con l’esecuzione delle operazioni CRUD sugli endpoint protetti. Questa progressione permette di comprendere prima le basi della comunicazione HTTP con un mock server, per poi affrontare gli aspetti di sicurezza tipici delle API RESTful professionali, dove l’autenticazione tramite token rappresenta lo standard de facto per proteggere le risorse sensibili.
Creazione di Mock Server REST con json-server e faker.js Approfondimento
Sezione intitolata “Creazione di Mock Server REST con json-server e faker.js ”Approfondimento: https://it.wikipedia.org/wiki/Mock_object
Nel contesto della programmazione orientata agli oggetti, un mock object (oggetto simulato) riproduce in modo controllato il comportamento di un oggetto reale. Il mock consente di testare componenti che dipendono da risorse non disponibili, inaccessibili oppure non ancora implementate; in tali casi l’oggetto reale viene sostituito dal mock.
Introduzione e Contesto Operativo
Sezione intitolata “Introduzione e Contesto Operativo”Nell’ambito dello sviluppo software moderno, la separazione tra lo sviluppo del backend e del frontend rende necessaria l’adozione di strategie di mocking per permettere ai team di lavorare in parallelo senza dipendenze bloccanti. Il tool json-server rappresenta una soluzione elegante e immediata per la prototipazione rapida di API RESTful, permettendo di simulare un server backend completo a partire da un semplice file JSON. Il software Faker.js consente invece di generare dati realistici e variabili, arricchendo l’esperienza di testing e sviluppo con dataset che riflettono scenari reali.
Questa guida illustra il processo di installazione, configurazione e popolamento dati di un mock server, con particolare attenzione alla generazione di dataset realistici mediante la libreria Faker.js. Verrà inoltre presentato un esempio completo di client C# per l’interazione con le risorse esposte.
Prerequisiti
Sezione intitolata “Prerequisiti”Prima di iniziare, è necessario assicurarsi che l’ambiente di sviluppo soddisfi i seguenti requisiti:
- Node.js: È richiesto il runtime Node.js (versione raccomandata LTS) per eseguire il server e gli script di generazione dati.
- npm (Node Package Manager): Viene installato automaticamente insieme a Node.js.
- Un editor di codice (si consiglia Visual Studio Code).
Configurazione del Progetto
Sezione intitolata “Configurazione del Progetto”Per mantenere ordine nel lavoro, si consiglia di creare una cartella dedicata al progetto di mock server.
- Creare una nuova cartella denominata
mock-server-tutorial. - Aprire il terminale e posizionarsi all’interno della cartella.
- Inizializzare un nuovo progetto
Node.jsper gestire le dipendenze:
mkdir mock-server-tutorialcd mock-server-tutorialnpm init -yQuesto comando genererà un file package.json di base.
Installazione delle Dipendenze
Sezione intitolata “Installazione delle Dipendenze”Per questo ambiente di test sono necessari due pacchetti npm:
-
json-server: Il motore che trasformerà un file JSON in un server REST completo.
-
@faker-js/faker: La libreria utilizzata per generare dati casuali ma verosimili (nomi di aziende, prezzi, coordinate geografiche, ecc.).
Eseguire il seguente comando nella cartella del progetto per installarli:
npm install json-server@0.17.4 @faker-js/faker --save-devLa versione 0.17.x rappresenta il ramo stabile più documentato. La versione 1.0.0-beta introduce modifiche architetturali significative (middleware-based) che, sebbene promettenti, possono risultare destabilizzanti in un contesto educativo.
È fondamentale utilizzare il pacchetto @faker-js/faker e non il deprecato faker.*
Uso di --save-dev
Sezione intitolata “Uso di --save-dev”L’opzione --save-dev (o la sua forma abbreviata -D) è un parametro fondamentale del comando npm install e serve a categorizzare le dipendenze del proprio progetto. Aggiunge i pacchetti installati alla sezione "devDependencies" del file package.json, invece che alla sezione standard "dependencies". Serve a distinguere tra le librerie necessarie per far funzionare l’applicazione in produzione e quelle necessarie solo durante la fase di sviluppo.
-
Dependencies (senza flag): Librerie essenziali per l’esecuzione del programma (es. React, Angular, o driver di database).
-
DevDependencies (
--save-dev): Strumenti usati dagli sviluppatori ma non necessari all’utente finale (es. strumenti di test, compilatori, linter, e nel nostro caso generatori di dati mock e server di sviluppo).
Si usa --save-dev perché json-server e faker sono strumenti di supporto per il testing; non fanno parte del codice che verrà distribuito ai clienti finali.
Installazione globale (opzionale e sconsigliata)
Sezione intitolata “Installazione globale (opzionale e sconsigliata)”È anche possibile installare json-server globalmente sulla propria macchina per utilizzarlo rapidamente in qualsiasi cartella senza dover configurare un progetto npm.
Il comando è:
npm install -g json-serverSe si sceglie questa strada, per avviare il server basterà digitare nel terminale:
json-server --watch db.jsonTuttavia in un contesto didattico si consiglia l’installazione locale e l’utilizzo dell’opzione --save-dev descritta sopra. Questo garantisce che tutti gli studenti utilizzino la stessa versione del server e che il progetto sia auto-contenuto e facilmente condivisibile.
Generazione Automatica dei Dati (Data Seeding)
Sezione intitolata “Generazione Automatica dei Dati (Data Seeding)”Invece di scrivere manualmente migliaia di righe di JSON, si creerà uno script per generare tali dati con l’uso della libreria @faker-js/faker. Si crei un file denominato generate-database.js con il contenuto specificato di seguito. Lo script implementa la logica di generazione per le due collezioni, una di companies e una di products, garantendo la coerenza referenziale tra prodotti e compagnie (il campo company_id nei prodotti referenzierà sempre ID esistenti nella collezione companies).
Creare un file denominato generate_data.js nella radice del progetto. Questo script avrà il compito di creare due collezioni:
-
Companies: 100 aziende con dettagli su fatturato, sede centrale e filiali.
-
Products: 100 prodotti, ciascuno collegato a un’azienda tramite
company_id.
const fs = require("fs");const { faker } = require("@faker-js/faker");
// Configurazioneconst PRODUCTS_COUNT = 100;const COMPANIES_COUNT = 100;
console.log("Inizio generazione dati mock...");
// Funzione helper per generare Locationsconst generateLocations = () => { const locationsCount = faker.number.int({ min: 1, max: 5 }); const locations = [];
for (let i = 0; i < locationsCount; i++) { locations.push({ city: faker.location.city(), employee_number: faker.number.int({ min: 10, max: 10000 }), state: faker.location.state(), }); } return locations;};
// 1. Generazione Companiesconst companies = [];for (let i = 1; i <= COMPANIES_COUNT; i++) { const company = { id: i, name: faker.company.name(), revenue: parseFloat( faker.finance.amount({ min: 1000000, max: 100000000, dec: 2 }), ), headquarter: { lat: faker.location.latitude(), lon: faker.location.longitude(), }, locations: generateLocations(), }; companies.push(company);}
// 2. Generazione Productsconst products = [];for (let i = 1; i <= PRODUCTS_COUNT; i++) { const product = { id: i, name: faker.commerce.productName(), price: parseFloat(faker.commerce.price({ min: 1, max: 1000, dec: 2 })), // Assegna casualmente un company_id esistente (da 1 a 100) company_id: faker.number.int({ min: 1, max: COMPANIES_COUNT }), }; products.push(product);}
// Struttura finale del DBconst database = { products: products, companies: companies,};
// Scrittura su file db.jsonconst jsonString = JSON.stringify(database, null, 2);fs.writeFileSync("db.json", jsonString);
console.log(`Generazione completata!`);console.log(`- Creati ${products.length} prodotti.`);console.log(`- Create ${companies.length} compagnie.`);console.log(`Il file 'db.json' è pronto per essere servito.`);Per eseguire lo script e generare il database, modificare il file package.json, nella sezione “scripts” come riportato di seguito:
{ "name": "mock-server-tutorial", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "generate": "node generate_data.js", "start": "json-server --watch db.json --port 3000", "start:alt": "json-server --watch db.json --port 3001" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs", "devDependencies": { "@faker-js/faker": "^10.3.0", "json-server": "0.17.4" }}Se si preferisce non modificare il file package.json, è possibile eseguire i comandi direttamente da terminale:
-
Per generare i dati: Eseguire
node generate_data.js. -
Per avviare il server: Poiché
json-serverè installato come dipendenza locale (e non globale), il terminale non troverà il comando direttojson-server. È necessario utilizzarenpx(Node Package Execute), uno strumento incluso in Node.js che cerca ed esegue i binari presenti nella cartella localenode_modulessenza richiedere installazioni globali:Finestra del terminale npx json-server db.json --port 3000
Tornando al metodo principale, ora è possibile generare i dati eseguendo:
npm run generateQuesto comando creerà un file db.json contenente 100 prodotti e 100 aziende con dati coerenti.
Avvio del Server e Analisi degli Endpoint
Sezione intitolata “Avvio del Server e Analisi degli Endpoint”Una volta generato il file db.json, il server è pronto per essere avviato. Utilizzare il comando configurato precedentemente:
npm startIl terminale mostrerà un output simile al seguente:
$ npm start
> mock-server-tutorial@1.0.0 start> json-server --watch db.json --port 3000
\{^_^}/ hi!
Loading db.json Done
Resources http://localhost:3000/products http://localhost:3000/companies
Home http://localhost:3000
Type s + enter at any time to create a snapshot of the database Watching...Troubleshooting: Errore “Port 3000 already in use”
Sezione intitolata “Troubleshooting: Errore “Port 3000 already in use””Se all’avvio compare l’errore Error: listen EADDRINUSE: address already in use :::3000, significa che un altro programma sta usando la porta 3000. Hai due opzioni:
-
Identificare e chiudere il processo:
-
Windows:
netstat -ano | findstr :3000per trovare il PID, poitaskkill /PID <PID> /F. -
Mac/Linux:
lsof -i :3000per trovare il PID, poikill -9 <PID>.
-
-
Cambiare porta: Abbiamo predisposto uno script alternativo che usa la porta 3001. Eseguire:
Finestra del terminale npm run start:alt
Configurazione avanzata di json-server: architettura e implementazione
Sezione intitolata “Configurazione avanzata di json-server: architettura e implementazione”La configurazione avanzata si avvale di tre file distinti che operano a livelli diversi dell’architettura:
-
json-server.json: Configurazione runtime del server -
routes.json: Riscrittura e mapping delle URL (URL rewriting) -
Middleware personalizzati: Interceptors per logica di business (auth, CORS, delay)
File di Configurazione Principale (json-server.json)
Sezione intitolata “File di Configurazione Principale (json-server.json)”Questo file JSON centrale consente di sostituire la maggior parte dei flag da riga di comando con una configurazione dichiarativa persistente. Deve essere posizionato nella root del progetto.
{ "port": 3000, "host": "localhost", "watch": true, "quiet": false, "no-cors": false, "no-gzip": false, "snapshots": "./snapshots", "routes": "./routes.json", "middlewares": [ "./middleware/cors.js", "./middleware/auth.js", "./middleware/delay.js" ], "id": "id", "foreignKeySuffix": "_id", "bodyParser": true}Analisi dei Parametri Critici
Sezione intitolata “Analisi dei Parametri Critici”| Parametro | Valore Default | Descrizione Tecnica |
|---|---|---|
host | "localhost" | Per permettere l’accesso da altre macchine nella rete locale (utile per testare client C# su dispositivi mobili o VM), impostare "0.0.0.0". |
snapshots | null | Directory dove salvare lo stato del database via endpoint POST /__snapshots__. Utile per catturare stati specifici durante il testing del client C#. |
id | "id" | Nome della chiave primaria. Se un client C# si aspetta "Id" (PascalCase per convenzione .NET), modificare questo valore. |
foreignKeySuffix | "Id" | Suffisso per le chiavi esterne. Standardizza la convenzione nominale tra collezioni. |
Routing Avanzato (routes.json)
Sezione intitolata “Routing Avanzato (routes.json)”Il file di routing permette di definire endpoint personalizzati che non corrispondono direttamente alle chiavi del file JSON, implementando pattern RESTful complessi richiesti dal client C#.
{ "/api/v1/*": "/$1", "/api/products/search/:term": "/products?name_like=:term", "/api/companies/:id/full": "/companies/:id?_embed=products", "/api/dashboard/stats": "/stats", "/health": "/health-check"}Pattern di riscrittura implementati
Sezione intitolata “Pattern di riscrittura implementati”-
Namespace Versioning: Il pattern
/api/v1/*rimappa tutte le chiamate da/api/v1/productsa/products, permettendo al client di implementare versionamento API senza modificare la struttura del mock. -
Query Semplificate: L’endpoint
/api/companies/:id/fullespande automaticamente la relazione, restituendo la compagnia con i prodotti annidati (operazione che altrimenti richiederebbe due chiamate HTTP dal client). -
Endpoint Sintetici: È possibile creare risorse virtuali come
/statsche aggregano dati da multiple collezioni mediante middleware (vedi sezione successiva).
Middleware Personalizzati
Sezione intitolata “Middleware Personalizzati”I middleware sono funzioni JavaScript che intercettano la richiesta prima che raggiunga il router di json-server. Per il client sono essenziali tre middleware: CORS, Autenticazione simulata, e Latenza di rete.
Middleware CORS per Client (middleware/cors.js)
Sezione intitolata “Middleware CORS per Client (middleware/cors.js)”Quando il client (specialmente se eseguito in un browser) tenta di contattare localhost:3000, il browser blocca le richieste per violazione della Same-Origin Policy. Questo middleware risolve il problema:
module.exports = (req, res, next) => { // Header necessari per CORS preflight res.header("Access-Control-Allow-Origin", "*"); res.header( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS", ); res.header( "Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization", );
// Gestione esplicita delle richieste OPTIONS (preflight) if (req.method === "OPTIONS") { res.sendStatus(200); } else { next(); }};Middleware di Autenticazione Simulata (middleware/auth.js)
Sezione intitolata “Middleware di Autenticazione Simulata (middleware/auth.js)”Per testare il client in scenari dove le API richiedono autenticazione JWT:
module.exports = (req, res, next) => { // Esclude le rotte pubbliche const publicPaths = ["/auth/login", "/health"]; if (publicPaths.includes(req.path)) return next();
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "Missing or invalid authorization header", code: "AUTH_REQUIRED", }); }
const token = authHeader.split(" ")[1];
// Validazione fittizia: il token deve essere "fake-jwt-token" if (token !== "fake-jwt-token") { return res.status(403).json({ error: "Invalid token", code: "AUTH_FORBIDDEN", }); }
// Aggiunge info utente alla request per uso nei controller req.user = { id: 1, role: "admin" }; next();};Middleware di Simulazione Rete (middleware/delay.js)
Sezione intitolata “Middleware di Simulazione Rete (middleware/delay.js)”Fondamentale per testare la responsiveness del client in condizioni di rete reali:
module.exports = (req, res, next) => { // Simula latenza variabile tra 200ms e 1500ms const delay = Math.floor(Math.random() * 1300) + 200;
// Aggiunge header di diagnostica visibile dal client res.header("X-Simulated-Delay", `${delay}ms`); res.header("X-Server-Time", new Date().toISOString());
setTimeout(next, delay);};Middleware di Logging Avanzato (middleware/logger.js)
Sezione intitolata “Middleware di Logging Avanzato (middleware/logger.js)”Utile per il debugging durante lo sviluppo del client:
module.exports = (req, res, next) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${req.method}${req.path}`);
if (Object.keys(req.body).length > 0) { console.log(" Body:", JSON.stringify(req.body, null, 2)); }
// Intercetta la risposta per loggare lo status const originalSend = res.send; res.send = function (body) { console.log(` Response Status: ${res.statusCode}`); originalSend.call(this, body); };
next();};Configurazione HTTPS per Client sicuri
Sezione intitolata “Configurazione HTTPS per Client sicuri”Se il client richiede endpoint HTTPS, è necessario generare certificati self-signed:
// https-config.jsconst fs = require("fs");const path = require("path");
module.exports = { cert: fs.readFileSync(path.join(__dirname, "ssl", "cert.pem")), key: fs.readFileSync(path.join(__dirname, "ssl", "key.pem")),};E aggiornare json-server.json:
{ "port": 3001, "https": true, "ssl": { "cert": "./ssl/cert.pem", "key": "./ssl/key.pem" }}Esempio di Setup Completo
Sezione intitolata “Esempio di Setup Completo”Struttura directory raccomandata:
mock-server/├── package.json├── json-server.json├── db.json (generato)├── routes.json└── middleware/ ├── 01-cors.js (prefisso numerico per ordinamento) ├── 02-delay.js ├── 03-auth.js └── 04-logger.jspackage.json configurato per la didattica:
{ "name": "mock-api-for-csharp-client", "version": "1.0.0", "scripts": { "start": "json-server --config json-server.json", "start:secure": "json-server --config json-server.json --https true", "generate": "node generate-database.js", "reset": "npm run generate && npm start" }, "devDependencies": { "@faker-js/faker": "^10.3.0", "json-server": "0.17.4" }}Endpoint Speciali di Amministrazione
Sezione intitolata “Endpoint Speciali di Amministrazione”json-server espone automaticamente endpoint utili per il debugging del client:
-
GET /__rules- Visualizza tutte le regole di routing attive -
GET /db- Restituisce l’intero database (utile per verificare stato) -
POST /__snapshots__- Salva lo stato attuale in un file timestampato -
PUT /__snapshots__/:id- Ripristina uno stato precedente
L’adozione di questa configurazione avanzata trasforma il mock server in un ambiente di testing realistico, permettendo agli studenti di affrontare problematiche tipiche dello sviluppo professionale: gestione dei token JWT, handling di timeout di rete, parsing di header custom, e versionamento delle API, il tutto prima ancora che il backend reale sia disponibile.
Implementazione dell’Autenticazione X-API-KEY
Sezione intitolata “Implementazione dell’Autenticazione X-API-KEY”Architettura del Middleware di Autenticazione
Sezione intitolata “Architettura del Middleware di Autenticazione”La soluzione prevede l’implementazione di un middleware Node.js che intercetti ogni richiesta HTTP in ingresso, estragga l’header X-API-KEY e ne verifichi la validità contro un elenco di chiavi autorizzate. In caso di esito negativo, il middleware restituirà un errore HTTP 401 (Unauthorized) o 403 (Forbidden) senza propagare la richiesta al router di json-server.
Si crei il file middleware/api-key-auth.js:
/** * Middleware per autenticazione via X-API-KEY * Supporta multiple API keys e rotte pubbliche escluse */module.exports = (req, res, next) => { // Definizione delle chiavi API valide (in produzione usare variabili d'ambiente) const validApiKeys = [ "dev-key-12345-students", "test-key-67890-demo", "csharp-client-key-2024", ];
// Rotte pubbliche che non richiedono autenticazione const publicRoutes = ["/health", "/auth/login", "/public/products"];
// Verifica se la rotta corrente è pubblica const isPublic = publicRoutes.some( (route) => req.path === route || req.path.startsWith(route + "/"), );
if (isPublic) { return next(); }
// Estrazione dell'header X-API-KEY (case-insensitive per RFC 2616) const apiKey = req.headers["x-api-key"] || req.headers["X-API-KEY"];
// Validazione presenza if (!apiKey) { return res.status(401).json({ error: "Authentication required", message: "Missing X-API-KEY header", code: "API_KEY_MISSING", timestamp: new Date().toISOString(), }); }
// Validazione formato (opzionale: regex per validazione struttura) const apiKeyPattern = /^[a-zA-Z0-9-]{10,50}$/; if (!apiKeyPattern.test(apiKey)) { return res.status(400).json({ error: "Bad Request", message: "Invalid API key format", code: "API_KEY_MALFORMED", }); }
// Verifica validità if (!validApiKeys.includes(apiKey)) { // Logging per audit (visibile solo nel terminale del server) console.warn( `[AUTH] Tentativo accesso con API key invalida: ${apiKey.substring(0, 5)}... da IP ${req.ip}`, );
return res.status(403).json({ error: "Forbidden", message: "Invalid API key provided", code: "API_KEY_INVALID", }); }
// Autenticazione riuscita: arricchimento della request con metadata req.authenticated = true; req.apiKeyId = apiKey.substring(0, 8) + "..."; // Identificativo parziale per logging req.clientType = apiKey.startsWith("csharp") ? "dotnet-client" : "generic";
// Header di risposta informativi (utile per debug client C#) res.setHeader("X-RateLimit-Limit", "1000"); res.setHeader("X-RateLimit-Remaining", "999"); // Placeholder per futura implementazione rate limiting res.setHeader("X-Authenticated-Client", req.clientType);
console.log( `[AUTH] Accesso autorizzato: ${req.method}${req.path} - Client: ${req.clientType}`, );
next();};Configurazione del Server
Sezione intitolata “Configurazione del Server”Aggiornare il file json-server.json per includere il middleware nell’array di elaborazione:
{ "port": 3000, "watch": true, "routes": "./routes.json", "middlewares": [ "./middleware/api-key-auth.js", "./middleware/cors.js", "./middleware/delay.js" ], "no-cors": false}L’ordine dei middleware è significativo. Il middleware di autenticazione deve essere posizionato prima di quello CORS se si desidera che la verifica dell’API key avvenga prima della gestione delle richieste preflight OPTIONS.
Implementazione di un Client C#
Sezione intitolata “Implementazione di un Client C#”Di seguito l’implementazione C# completa che gestisce l’iniezione dell’header X-API-KEY in tutte le richieste HTTP, con gestione degli errori di autenticazione:
using System;using System.Net.Http;using System.Net.Http.Json;using System.Net.Http.Headers;using System.Collections.Generic;using System.Threading.Tasks;
namespace SecureMockApiClient{ public class ApiClient { private readonly HttpClient _httpClient; private const string API_KEY = "csharp-client-key-2024"; private const string BASE_URL = "http://localhost:3000/";
public ApiClient() { _httpClient = new HttpClient { BaseAddress = new Uri(BASE_URL), Timeout = TimeSpan.FromSeconds(30) };
// Configurazione header di default per tutte le richieste _httpClient.DefaultRequestHeaders.Add("X-API-KEY", API_KEY); _httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
// Header custom opzionali per tracciamento _httpClient.DefaultRequestHeaders.Add("X-Client-Version", "1.0.0"); }
public async Task<List<Product>> GetProductsAsync() { try { var response = await _httpClient.GetAsync("products");
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { var error = await response.Content.ReadFromJsonAsync<ApiError>(); Console.WriteLine($"Errore di autenticazione: {error?.message}"); return null; }
if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) { Console.WriteLine("API Key invalida o scaduta. Verificare le credenziali."); return null; }
response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<List<Product>>(); } catch (HttpRequestException ex) { Console.WriteLine($"Errore di connessione: {ex.Message}"); throw; } }
public async Task<Company> GetCompanyAsync(int id) { // Esempio con espansione relazione (include prodotti) var response = await _httpClient.GetAsync($"companies/{id}?_embed=products"); return await response.Content.ReadFromJsonAsync<Company>(); } }
// Modello per deserializzazione errori public class ApiError { public string error { get; set; } public string message { get; set; } public string code { get; set; } public string timestamp { get; set; } }
// Programma di test class Program { static async Task Main(string[] args) { var client = new ApiClient();
Console.WriteLine("=== Test Accesso Autenticato ===\n");
// Test 1: Accesso autorizzato var products = await client.GetProductsAsync(); Console.WriteLine($"Prodotti recuperati: {products?.Count ?? 0}");
// Test 2: Verifica header di risposta using (var httpClient = new HttpClient()) { httpClient.DefaultRequestHeaders.Add("X-API-KEY", "csharp-client-key-2024"); var response = await httpClient.GetAsync("http://localhost:3000/products/1");
Console.WriteLine("\nHeader di risposta:"); if (response.Headers.Contains("X-Authenticated-Client")) { var clientType = response.Headers.GetValues("X-Authenticated-Client"); Console.WriteLine($" Client Type: {string.Join(", ", clientType)}"); } } } }}Testing e Verifica tramite CLI
Sezione intitolata “Testing e Verifica tramite CLI”Prima di eseguire il client, si consiglia di verificare il comportamento del middleware tramite strumenti da riga di comando:
Richiesta senza API Key (deve fallire con 401)
Sezione intitolata “Richiesta senza API Key (deve fallire con 401)”curl -i http://localhost:3000/productsOutput atteso:
HTTP/1.1 401 UnauthorizedContent-Type: application/json{ "error": "Authentication required", "message": "Missing X-API-KEY header", "code": "API_KEY_MISSING", "timestamp": "2024-01-15T10:30:00.000Z"}Richiesta con API Key valida
Sezione intitolata “Richiesta con API Key valida”curl -i -H "X-API-KEY: csharp-client-key-2024" http://localhost:3000/products/1Output atteso:
HTTP/1.1 200 OKX-Authenticated-Client: dotnet-clientX-RateLimit-Limit: 1000Content-Type: application/json{ "id": 1, "name": "Onions - Vidalia", "price": 37.54, "company_id": 94}Richiesta con API Key invalida (deve fallire con 403)
Sezione intitolata “Richiesta con API Key invalida (deve fallire con 403)”curl -i -H "X-API-KEY: chiave-sbagliata" http://localhost:3000/productsPattern Avanzati per Contesti Educativi
Sezione intitolata “Pattern Avanzati per Contesti Educativi”Rotte con Livelli di Autorizzazione Differenziati
Sezione intitolata “Rotte con Livelli di Autorizzazione Differenziati”Per simulare un sistema RBAC (Role-Based Access Control), si può estendere il middleware per verificare permessi specifici:
// Estensione del middleware per ruoliconst keyPermissions = { "dev-key-12345-students": ["read", "write"], "csharp-client-key-2024": ["read"], // Solo lettura per il client C# esercitazione};
// Nel middleware, dopo la validazione:const permissions = keyPermissions[apiKey];if (req.method !== "GET" && !permissions.includes("write")) { return res.status(403).json({ error: "Insufficient permissions", message: "API key valida ma senza permessi di scrittura", code: "FORBIDDEN_WRITE", });}Simulazione di Rate Limiting per API Key
Sezione intitolata “Simulazione di Rate Limiting per API Key”Per testare la robustezza del client C# in scenari di throttling:
const requestCounts = new Map();
module.exports = (req, res, next) => { const apiKey = req.headers["x-api-key"];
// ... validazione chiave ...
// Rate limiting semplice (10 req/min per chiave) const now = Date.now(); const windowStart = now - 60000; const requests = requestCounts.get(apiKey) || []; const recentRequests = requests.filter((t) => t > windowStart);
if (recentRequests.length >= 10) { res.setHeader("Retry-After", "60"); return res.status(429).json({ error: "Too Many Requests", message: "Rate limit exceeded: 10 requests per minute", code: "RATE_LIMIT_EXCEEDED", }); }
recentRequests.push(now); requestCounts.set(apiKey, recentRequests); res.setHeader("X-RateLimit-Remaining", 10 - recentRequests.length);
next();};Considerazioni sulla Sicurezza
Sezione intitolata “Considerazioni sulla Sicurezza”Quando si utilizza questo pattern in ambiente didattico, è opportuno evidenziare alcune best practice di sicurezza che si dovrebbero adottare quando si implementa l’autenticazione basata su API key in scenari reali:
-
Trasporto Sicuro: In produzione, l’header
X-API-KEYdeve transitare esclusivamente su connessioni HTTPS. Il client C# deve validare il certificato SSL/TLS del server. -
Storage della Chiave: Nel codice C# esemplificativo, la chiave è hardcoded per semplicità didattica. In applicazioni reali, la chiave deve essere archiviata in:
-
appsettings.jsoncon sezioneUser Secrets(sviluppo locale) -
Variabili d’ambiente
-
Azure Key Vault / AWS Secrets Manager (produzione)
-
-
Header vs Query String: Si sconsiglia di passare l’API key come query parameter (es.
?api_key=xyz) poiché i parametri URL vengono loggati da proxy e server web, esponendo potenzialmente le credenziali. -
Revoca: Il sistema implementato è stateless. Per permettere la revoca di una chiave compromessa senza riavviare il server, sarebbe necessario implementare un meccanismo di blacklist in memoria o su file esterno controllato periodicamente.
Funzionalità avanzate - uso di certificati SSL self-signed
Sezione intitolata “Funzionalità avanzate - uso di certificati SSL self-signed”Ecco la procedura completa per generare certificati SSL self-signed su Windows e configurare json-server 0.17.4 in modalità HTTPS, inclusa la configurazione del client C# per l’ambiente di sviluppo.
Prerequisiti: OpenSSL su Windows
Sezione intitolata “Prerequisiti: OpenSSL su Windows”Poiché OpenSSL non è incluso nativamente in Windows, installarlo tramite uno dei seguenti metodi:
Opzione A - Git Bash (Consigliata): Se si ha Git per Windows installato, Git Bash include già OpenSSL. aprire Git Bash nella cartella del progetto.
Opzione B - WSL (Windows Subsystem for Linux):
wsl --install# Poi da terminale WSL Ubuntu procedere con i comandi LinuxOpzione C - Download manuale: Scarica OpenSSL per Windows da https://slproweb.com/products/Win32OpenSSL.html (versione “Win64 OpenSSL v3.x”).
Generazione dei Certificati
Sezione intitolata “Generazione dei Certificati”Dalla root del progetto mock-server-tutorial, eseguire:
# Crea la directory per i certificatimkdir ssl
# Genera la chiave privata (2048 bit)openssl genrsa -out ssl/key.pem 2048
# Genera il certificato self-signed valido 365 giorniopenssl req -new -x509 -sha256 -key ssl/key.pem -out ssl/cert.pem -days 365 -subj "/C=IT/ST=Lombardia/L=Milano/O=DevLocal/CN=localhost"Output atteso:
ssl/key.pem- Chiave privata del serverssl/cert.pem- Certificato pubblico autofirmato
Configurazione di JSON Server (Versione 0.17.4)
Sezione intitolata “Configurazione di JSON Server (Versione 0.17.4)”Metodo A: Avvio via CLI (Più rapido)
Sezione intitolata “Metodo A: Avvio via CLI (Più rapido)”Modificare lo script nel package.json:
{ "scripts": { "generate": "node generate_data.js", "start": "json-server db.json --port 3000", "start:secure": "json-server db.json --port 3001 --ssl --cert ./ssl/cert.pem --key ./ssl/key.pem" }}Avvio:
npm run start:secureMetodo B: Configurazione programmatica con https-config.js
Sezione intitolata “Metodo B: Configurazione programmatica con https-config.js”Se si preferisce il controllo programmatico (per aggiungere middleware custom), creare il file server.js:
const jsonServer = require("json-server");const https = require("https");const fs = require("fs");const path = require("path");
// Import della configurazione SSL separataconst sslOptions = { key: fs.readFileSync(path.join(__dirname, "ssl", "key.pem")), cert: fs.readFileSync(path.join(__dirname, "ssl", "cert.pem")),};
const server = jsonServer.create();const router = jsonServer.router("db.json");const middlewares = jsonServer.defaults();
server.use(middlewares);server.use(router);
https.createServer(sslOptions, server).listen(3001, () => { console.log("✓ JSON Server HTTPS running at https://localhost:3001"); console.log( " ⚠️ Il certificato è self-signed: accettare l'eccezione nel browser", );});E aggiornare package.json:
{ "scripts": { "start:secure": "node server.js" }}Configurazione del Client C#
Sezione intitolata “Configurazione del Client C#”Quando il client C# tenta di connettersi a https://localhost:3001, riceverà un errore di certificato non attendibile. Per l’ambiente di sviluppo, configurare HttpClient per bypassare la validazione:
using System;using System.Net.Http;using System.Net.Security;using System.Security.Cryptography.X509Certificates;
public class SecureApiClient{ private readonly HttpClient _httpClient; private const string BaseUrl = "https://localhost:3001/";
public SecureApiClient() { // Handler che accetta qualsiasi certificato (SOLO per sviluppo locale!) var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { // In produzione: validare thumbprint o issuer // In sviluppo: accettare tutto return true; } };
_httpClient = new HttpClient(handler) { BaseAddress = new Uri(BaseUrl) };
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); }
// ... metodi CRUD come in precedenza}⚠️ Avviso di sicurezza: Il callback return true disabilita completamente la validazione SSL. Utilizzare esclusivamente in ambiente di sviluppo locale mai in produzione.
Verifica e Test
Sezione intitolata “Verifica e Test”Test via curl (ignorando certificato)
Sezione intitolata “Test via curl (ignorando certificato)”curl -k https://localhost:3001/products# oppurecurl --insecure https://localhost:3001/productsTest via browser
Sezione intitolata “Test via browser”- Aprire
https://localhost:3001/products - Il browser mostrerà un avviso “La connessione non è privata” (Chrome) o “Warning: Potential Security Risk” (Firefox)
- Cliccare su “Avanzate” → “Procedi su localhost (non sicuro)“
Importazione certificato (Opzionale - per eliminare warning)
Sezione intitolata “Importazione certificato (Opzionale - per eliminare warning)”Per aggiungere il certificato ai “Trusted Root” di Windows (utile per test con browser):
# Da PowerShell come Amministratorecertutil -addstore -f "ROOT" .\ssl\cert.pemRiepilogo struttura files
Sezione intitolata “Riepilogo struttura files”mock-server-tutorial/├── ssl/│ ├── key.pem # Chiave privata (NON committare in git!)│ └── cert.pem # Certificato pubblico├── db.json # Database generato├── server.js # (Opzionale) Server HTTPS programmatico├── https-config.js # Modulo config SSL richiesto dall'utente└── package.jsonNota: Aggiungere la cartella ssl/ al .gitignore per evitare di committare chiavi private:
# .gitignoressl/*.pemFunzionalità avanzate - Validazione del certificato self signed
Sezione intitolata “Funzionalità avanzate - Validazione del certificato self signed”Per far sì che il client C# consideri attendibile esclusivamente il certificato cert.pem generato localmente, si implementa una tecnica di Certificate Pinning (o “fingerprint validation”). Invece di disabilitare la validazione SSL, il client verifica che l’impronta digitale (thumbprint) del certificato ricevuto corrisponda esattamente a quella del file locale.
Di seguito la procedura completa.
Estrazione dell’impronta digitale (Thumbprint)
Sezione intitolata “Estrazione dell’impronta digitale (Thumbprint)”Dalla directory del progetto, si esegue il comando OpenSSL per ottenere l’hash SHA-1 (o SHA-256) del certificato:
openssl x509 -in ssl/cert.pem -noout -fingerprint -sha1Output esempio:
SHA1 Fingerprint=A1:B2:C3:D4:E5:F6:...Si copia la stringa esadecimale rimuovendo i due punti (:). Il valore da inserire nel codice C# sarà:
A1B2C3D4E5F6...Nota: Se si preferisce SHA-256, sostituire -sha1 con -sha256 nel comando OpenSSL.
Implementazione del Client C# con Validazione
Sezione intitolata “Implementazione del Client C# con Validazione”Il HttpClientHandler viene configurato per confrontare l’impronta del certificato server con quella attesa. Se non coincidono, la connessione viene rifiutata.
using System;using System.Net.Http;using System.Net.Security;using System.Security.Cryptography.X509Certificates;using System.Threading.Tasks;
public class SecurePinnedApiClient{ private readonly HttpClient _httpClient; private const string BaseUrl = "https://localhost:3001/";
// Impronta del certificato self-signed (senza due punti, maiuscole o minuscole indifferenti) private const string ExpectedCertificateThumbprint = "A1B2C3D4E5F678..."; // <-- Inserire qui il valore
public SecurePinnedApiClient() { var handler = new HttpClientHandler { // Validazione custom: accetta SOLO il certificato con thumbprint specifico ServerCertificateCustomValidationCallback = ValidateCertificate };
_httpClient = new HttpClient(handler) { BaseAddress = new Uri(BaseUrl), Timeout = TimeSpan.FromSeconds(30) };
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); }
/// <summary> /// Valida che il certificato presentato dal server sia esattamente quello generato localmente. /// </summary> private bool ValidateCertificate( HttpRequestMessage request, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (certificate == null) { Console.WriteLine("Errore: Nessun certificato presentato dal server."); return false; }
// Ottiene l'impronta SHA-1 del certificato ricevuto string actualThumbprint = certificate.Thumbprint;
// Confronto case-insensitive bool isValid = actualThumbprint.Equals( ExpectedCertificateThumbprint, StringComparison.OrdinalIgnoreCase);
if (!isValid) { Console.WriteLine($"ATTENZIONE: Tentativo di connessione con certificato non autorizzato."); Console.WriteLine($" Atteso: {ExpectedCertificateThumbprint}"); Console.WriteLine($" Ricevuto: {actualThumbprint}"); return false; }
Console.WriteLine("Certificato validato con successo (pinning corretto)."); return true; }
public async Task<string> GetProductsAsync() { try { var response = await _httpClient.GetAsync("products"); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } catch (HttpRequestException ex) { Console.WriteLine($"Errore richiesta: {ex.Message}"); throw; } }}Alternativa: Caricamento del file .pem nel Client
Sezione intitolata “Alternativa: Caricamento del file .pem nel Client”Un approccio più manutenibile consiste nel copiare il file cert.pem nel progetto C# (cartella Assets/ o radice) e validare confrontando direttamente i byte del certificato, senza dover copiare manualmente l’hash.
Configurazione progetto C#:
- Aggiungere
cert.pemal progetto in Visual Studio. - Impostare Proprietà → Copia nella directory di output = Copia sempre (o Copia se più recente).
using System.IO;using System.Reflection;
public class PinnedCertFromFileClient{ private readonly string _expectedThumbprint;
public PinnedCertFromFileClient() { // Carica il certificato embedded o da file var assembly = Assembly.GetExecutingAssembly(); var certPath = Path.Combine( Path.GetDirectoryName(assembly.Location)!, "cert.pem"); // Assicurarsi che cert.pem sia in output dir
if (!File.Exists(certPath)) throw new FileNotFoundException("Certificato non trovato", certPath);
var localCert = new X509Certificate2(certPath); _expectedThumbprint = localCert.Thumbprint;
Console.WriteLine($"Certificato locale caricato. Thumbprint: {_expectedThumbprint}"); }
// Usare _expectedThumbprint nel callback ValidateCertificate // come mostrato nella sezione precedente}Considerazioni di Sicurezza
Sezione intitolata “Considerazioni di Sicurezza”- Man-in-the-Middle (MITM): Questa tecnica previene attacchi MITM anche con certificati self-signed, poiché un eventuale attaccante dovrebbe possedere esattamente la coppia di chiavi generata localmente, non un qualsiasi altro certificato.
- Rinnovo/Rigenerazione: Se si rigenera il certificato (
openssl reqnuovo), il thumbprint cambia. È necessario aggiornare la costanteExpectedCertificateThumbprintnel codice C# e ridistribuire il client (o il filecert.pem). - Produzione: In ambiente di produzione non si utilizzano certificati self-signed. Si utilizza invece un certificato rilasciato da una CA pubblica (Let’s Encrypt, etc.) e si rimuove il callback custom, lasciando che il sistema operativo validi la catena di fiducia (
SslPolicyErrors.None).
Postman Mock Server Approfondimento
Sezione intitolata “Postman Mock Server ”Video introduttivi:
Postman può essere utilizzato anche per realizzare un mock di endpoint REST. Il primo passo consiste nel creare un Mock Server:

Per creare un Mock Server è necessario definire almeno una rotta con un esempio. Un esempio descrive una richiesta (con un determinato valore) e la corrispondente risposta attesa.
Nel seguito si realizza un Mock Server ad accesso libero che consente di effettuare una GET per ottenere l’elenco dei prodotti su un endpoint API.
Creazione del Mock Server
Sezione intitolata “Creazione del Mock Server”Per creare un mock server è necessario inserire almeno un esempio di richiesta. Ad esempio, si inserisce una GET /products che produce come risposta iniziale un oggetto vuoto {}. Successivamente si dettagliano meglio richiesta e risposta.

Dopo aver selezionato “Next” si definisce il nome del Mock Server:

Si assegna al server il nome Product Mocking Server e si mantiene attiva l’opzione che salva la URL del server in una variabile d’ambiente. Postman crea quindi il Mock Server e mostra la relativa pagina iniziale:

Postman crea automaticamente:
- un ambiente (Environment) con lo stesso nome del Mock Server;
- una collection, anch’essa con lo stesso nome.

Una volta creato il Mock Server, è possibile modificare l’esempio relativo alla GET [object Object]/products e aggiungere ulteriori endpoint con i rispettivi esempi.
Definizione degli esempi di risposta
Sezione intitolata “Definizione degli esempi di risposta”Per fare in modo che, quando si effettua una richiesta al Mock Server, si ottenga come risposta un elenco di prodotti in formato JSON, si modifica la risposta dell’esempio “default” incollando dati JSON (ad esempio generati con Mockaroo).
È inoltre necessario configurare l’header di risposta, impostando Content-Type a application/json.

Selezionare “Save” per salvare l’esempio relativo alla rotta /products.
Per testare l’esempio, selezionare la richiesta e usare “Send”: Postman mostrerà la risposta coerente con l’esempio definito.
È possibile aggiungere un secondo esempio sulla rotta /products per ottenere un prodotto specifico (ad esempio con rotta /products/:id). Procedere con la creazione di ulteriori esempi sulla stessa richiesta:

È possibile aggiungere anche la rotta relativa alle aziende, procedendo in modo analogo a quanto fatto per products. Si ottiene dunque la rotta /companies con dati analogamente generati:

Utilizzo di un Postman Mock Server per il testing di un’applicazione client
Sezione intitolata “Utilizzo di un Postman Mock Server per il testing di un’applicazione client”Un Mock Server può essere utilizzato anche al di fuori dell’ambiente Postman, ad esempio per testare un’applicazione client in sviluppo.
Nel caso di un server ad accesso libero, esso può essere interrogato anche da browser. Dalle proprietà del Mock Server, selezionando “View Documentation”, è possibile individuare la URL del server e delle rotte:

Incollando la URL nel browser si ottiene una risposta dal Mock Server:

Questo consente di sviluppare un’applicazione client utilizzando il Mock Server prima che sia pronto il server REST reale.
Un mock server non è destinato all’uso in produzione: simula un comportamento, ma non implementa realmente persistenza e logica applicativa (ad esempio, una POST/PUT può non salvare dati in modo permanente).
Postman Mock Server privato
Sezione intitolata “Postman Mock Server privato”Postman consente anche di creare Mock Server privati, accessibili solo tramite autenticazione (ad esempio mediante una chiave).
Per creare un Product Secure Mocking Server si può procedere come segue:
- creare un mock server come nei paragrafi precedenti e, nelle opzioni, selezionare l’opzione di server privato:

-
inserire una Postman API Key (generata come descritto in precedenza) nell’header delle richieste HTTP, ad esempio come campo
x-api-key; -
creare le richieste GET, POST, PUT, DELETE analogamente agli esempi già realizzati.
Esempi:

Negli esempi sono stati inseriti:
- la GET di un prodotto con
id = 4; - la GET di una compagnia con
id = 73corrispondente alcompany_iddel prodotto conid = 4.
Documentation Page del server Product Secure Mocking Server
Sezione intitolata “Documentation Page del server Product Secure Mocking Server”https://documenter.getpostman.com/view/1643371/2s93JnT6Bz

Testing di un server API con cURL
Sezione intitolata “Testing di un server API con cURL”Si propone di testare il server privato creato con cURL. Dalla pagina di documentazione del Mock Server è possibile consultare le rotte definite e alcuni esempi di comandi.
Selezionando “cURL” nel menù in alto della pagina di documentazione, Postman propone esempi di comandi per testare le rotte.

Ad esempio, per eseguire la GET su products la documentazione suggerisce:
curl --location 'https://3ca86ebb-ff5d-4d05-aae4-117d2ea114ed.mock.pstmn.io/products' \--header 'Accept: application/json'Questo comando, così come riportato, non funziona in PowerShell per diversi motivi:
-
è necessario inserire una chiave privata di Postman;
-
la URL deve essere quella del proprio mock server;
-
in PowerShell il carattere
(che in bash indica la prosecuzione su riga successiva) non viene interpretato correttamente: in PowerShell il carattere per la prosecuzione è ```(ALT+96). In CMD si usa^; -
il certificato digitale del server potrebbe non risultare valido, a meno di usare la versione di cURL preinstallata in Windows (Schannel) o opzioni adeguate.
Il comando cURL per PowerShell può essere riscritto come segue (a scuola è necessario impostare le variabili d’ambiente della shell per il proxy):
curl --ca-native --location 'https://id_proprio_server.mock.pstmn.io/products' `--header 'Accept: application/json' --header 'X-API-Key: valore_chiave_di_postman'In realtà non è strettamente necessario impostare --location (-L, follow redirects). L’istruzione può essere:
curl 'https://id_proprio_server.mock.pstmn.io/products' `-H 'Accept: application/json' -H 'X-API-Key: valore_chiave_di_postman'È ammesso anche l’uso dei doppi apici al posto dei singoli apici con cURL.
Per finalità didattiche è utile aggiungere --verbose (o -v) per visualizzare lo scambio di messaggi tra client e server. I campi preceduti da > sono inviati da cURL verso il server; quelli preceduti da < provengono dal server.

Test di una POST con cURL:
curl --ca-native --request POST --location 'https://id_proprio_server.mock.pstmn.io/products' `--header 'Content-Type: application/json' `--header 'X-API-Key: valore_chiave_di_postman' `--data '{ "name": "Honey - Comb", "price": 26.68, "company_id": 73}'Oppure, usando le opzioni in forma abbreviata:
curl --ca-native -X POST -L 'https://id_proprio_server.mock.pstmn.io/products' `-H 'Content-Type: application/json' `-H 'X-API-Key: valore_chiave_di_postman' `-d '{ "name": "Honey - Comb", "price": 26.68, "company_id": 73}'Oppure è possibile non specificare il tipo di richiesta e sfruttare i default (GET senza dati, POST con dati):
curl --ca-native 'https://id_proprio_server.mock.pstmn.io/products' `-H 'Content-Type: application/json' `-H 'X-API-Key: valore_chiave_di_postman' `-d '{ "name": "Honey - Comb", "price": 26.68, "company_id": 73}'
Test di una PUT con cURL (nel caso di PUT occorre specificare l’id):
curl --ca-native --request PUT --location 'https://id_proprio_server.mock.pstmn.io/products/4' `--header 'Content-Type: application/json' `--header 'X-API-Key: valore_chiave_di_postman' `--data '{ "id": 4, "name": "Honey - Comb", "price": 40.0, "company_id": 73 }'oppure in forma abbreviata:
curl --ca-native -X PUT -L 'https://id_proprio_server.mock.pstmn.io/products/4' `-H 'Content-Type: application/json' `-H 'X-API-Key: valore_chiave_di_postman' `-d '{ "id": 4, "name": "Honey - Comb", "price": 40.0, "company_id": 73 }'
Test di una DELETE con cURL (occorre specificare l’id):
curl --ca-native --request DELETE --location 'https://id_proprio_server.mock.pstmn.io/products/4' `--header 'X-API-Key: valore_chiave_di_postman'oppure in forma abbreviata:
curl --ca-native -X DELETE -L 'https://id_proprio_server.mock.pstmn.io/products/4' `--header 'X-API-Key: valore_chiave_di_postman'
In bash (Linux) i comandi rimangono sostanzialmente gli stessi, ma per indicare la prosecuzione su riga successiva si usa “. Ad esempio:
curl --ca-native --location 'https://id_proprio_server.mock.pstmn.io/products/1' \-H 'Accept: application/json' -H 'X-API-Key: valore_chiave_di_postman'curl --ca-native -X POST -L 'https://id_proprio_server.mock.pstmn.io/products' \-H 'Content-Type: application/json' \-H 'X-API-Key: valore_chiave_di_postman' \-d '{ "name": "Honey - Comb", "price": 26.68, "company_id": 73}'curl --ca-native -X PUT -L 'https://id_proprio_server.mock.pstmn.io/products/4' \-H 'Content-Type: application/json' \-H 'X-API-Key: valore_chiave_di_postman' \-d '{ "id": 4, "name": "Honey - Comb", "price": 40.0, "company_id": 73 }'curl --ca-native -X DELETE -L 'https://id_proprio_server.mock.pstmn.io/products/4' \--header 'X-API-Key: valore_chiave_di_postman'Scrittura di un client console per le Mock API di Postman
Sezione intitolata “Scrittura di un client console per le Mock API di Postman”Riferimenti:
In questa sezione si definisce un esempio di client console che interroga un mock server Postman, costruendo i model a partire dai dati JSON.
L’esempio seguente mostra un client console che interroga un mock server Postman, con autenticazione tramite chiave API e gestione di GET e POST. I model Company e Product sono definiti in base alla struttura dei dati JSON restituiti dal mock server. Per poter interrogare il mock server, è necessario inserire la chiave API e la URL del server in un file JSON (ad esempio MyPostmanStore.json) che viene letto all’avvio del client.
Se si vuole testare un mock server privato, è necessario inserire la chiave API e la URL del server nel file MyPostmanStore.json e assicurarsi che il client C# legga correttamente questi dati per configurare l’header delle richieste HTTP.
Se si vuole testare il client senza una chiave API, è possibile creare un mock server ad accesso libero e inserire la URL del server nel file MyPostmanStore.json, lasciando vuoto il campo della chiave API. In questo modo, il client C# non invierà l’header X-API-Key nelle richieste HTTP, e il mock server ad accesso libero risponderà comunque alle richieste.
Pet l’esempio di codice seguente è possibile usare il Mock Server:
- URL:
https://f656ed25-0808-4eec-b895-d3e84f046cee.mock.pstmn.io - GET
/productsrestituisce un elenco di prodotti in formato JSON - GET
/companiesrestituisce un elenco di aziende in formato JSON
//file Company.csusing System.Text.Json.Serialization;namespace APIClientForPostmanMockServer.Model{ public class Company { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("revenue")] public float Revenue { get; set; } [JsonPropertyName("headquarter")] public Headquarter? Headquarter { get; set; } [JsonPropertyName("locations")] public Location[]? Locations { get; set; } } public class Headquarter { [JsonPropertyName("lat")] public float Lat { get; set; } [JsonPropertyName("lon")] public float Lon { get; set; } } public class Location { [JsonPropertyName("city")] public string? City { get; set; } [JsonPropertyName("employee_number")] public int EmployeeNumber { get; set; } [JsonPropertyName("state")] public string? State { get; set; } }}//file Product.csusing System.Text.Json.Serialization;namespace APIClientForPostmanMockServer.Model{ public class Product { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("price")] public decimal Price { get; set; } [JsonPropertyName("company_id")] public int CompanyId { get; set; } }}//file Program.csusing HttpProxyControl;using System.Net.Http.Headers;using System.Net.Http.Json;using System.Net;using System.Text;using System.Text.Json;using System.Text.Json.Serialization;using APIClientForPostmanMockServer.Model;namespace APIClientForPostmanMockServer{ public class PostmanStore { [JsonPropertyName("api_key")] public string APIKeyValue { get; set; } = string.Empty; [JsonPropertyName("base_address")] public string BaseAddress { get; set; } = string.Empty; } class Program { const string APIKeyName = "X-API-Key"; static readonly PostmanStore postmanStore = GetDataFromStore(); static readonly string APIKeyValue = postmanStore.APIKeyValue; static readonly string baseAddress = postmanStore.BaseAddress; static readonly string acceptedMediaType = "application/json"; static readonly HttpClient _client = HttpProxyHelper.CreateHttpClient(setProxy: true); /// <summary> /// Recupera i secrets da file /// </summary> /// <returns></returns> static PostmanStore GetDataFromStore() { //il file è nella cartella che contiene la soluzione corrente //il file contiene un oggetto JSON del tipo: //{ // "api_key":"api_key_value", // "base_address": "base_address_value" //} string keyStorePath = "../../../../../../MyPostmanStore.json"; string store = File.ReadAllText(keyStorePath); PostmanStore? postmanStore = JsonSerializer.Deserialize<PostmanStore>(store); return postmanStore ?? new PostmanStore(); } /// <summary> /// Configura i parametri dell'header per effettuare le richieste all'endpoint remoto /// Il base address del client viene configurato con il base URL del server /// https://swagger.io/docs/specification/2-0/api-host-and-base-path/ /// L'header viene resettato e vengono aggiunti il media type e l'X-API-Key /// </summary> static void ConfigureHttpClientHeaders() { _client.BaseAddress = new Uri(baseAddress); _client.DefaultRequestHeaders.Accept.Clear(); _client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue(acceptedMediaType)); //https://stackoverflow.com/questions/53551361/how-to-add-api-key-in-request-header-using-web-api //https://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient //l'istruzione seguente scrive nello header del messaggio http di richiesta la x-api-key _client.DefaultRequestHeaders.Add(APIKeyName, APIKeyValue); } static async Task Main(string[] args) { // https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.getawaiter //GetAwaiter is intended for compiler use rather than use directly in code. //RunAsync().GetAwaiter().GetResult(); await RunAsync(); } static async Task RunAsync() { ConfigureHttpClientHeaders(); try { Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Test di POST"); Console.WriteLine("Simuliamo la POST per inserire un prodotto sull'endpoint remoto"); // Create a new product Product? product = new() { Name = "Insalata verde", Price = 1.55m, CompanyId = 34 }; //la POST dovrebbe restituire l'uri dell'oggetto creato e il suo id Uri? url = await PostProductAsync(product, _client); if (url != null) { Console.WriteLine($"Created at {url}"); // Get the product //con l'uri della risorsa creata sul server remoto recuperiamo l'oggetto Console.WriteLine("Il prodotto caricato sul server mediante la POST non è realmente salvato, \n" + "quindi quando recuperiamo il valore tramite la sua url otteniamo un valore diverso"); product = await GetProductAsync(url.PathAndQuery, _client); if (product != null) { ShowProduct(product); // Update the product Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Test di PUT"); Console.WriteLine("Updating price..."); product.Price = 80; Product? updatedProduct = await PutProductAsync(product, _client); // Get the updated product //Console.WriteLine("Il prodotto aggiornato"); //product = await GetProductAsync(url.PathAndQuery); if (updatedProduct != null) { ShowProduct(updatedProduct); } // Delete the product Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Test di DELETE"); Console.WriteLine($"Eliminiamo il prodotto con id = {product.Id}"); var statusCode = await DeleteProductAsync(product.Id, _client); Console.WriteLine($"Deleted (HTTP Status = {(int)statusCode})"); } } //otteniamo tutti i prodotti Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Test di GET di tutti i prodotti"); Console.WriteLine("Elenco dei prodotti"); var products = await GetAllProductsAsync("products", _client); if (products != null) { ShowProducts(products); } //dato un prodotto, ottenere i dati dell'azienda che produce tale prodotto Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Dato un prodotto, ottenere i dati dell'azienda che produce tale prodotto"); //Console.Write("Inserisci l'id di un prodotto: "); //int productId = int.Parse(Console.ReadLine() ?? "0"); int productId = 4; Console.WriteLine($"Recuperiamo il prodotto con id = {productId}: "); string productUri = $"products/{productId}"; product = await GetProductAsync(productUri, _client); if (product != null) { JsonSerializerOptions options = new() { WriteIndented = true }; Console.WriteLine("il prodotto corrispondente all'id inserito è:"); Console.WriteLine(JsonSerializer.Serialize(product, options)); //si noti che il prodotto ottenuto non corrisponde a quello riportato con //lo stesso id nell'elenco dei prodotti, poiché viene generato a caso ogni volta string companyUri = $"companies/{product.CompanyId}"; Company? company = await GetCompanyAsync(companyUri, _client); if (company != null) { Console.WriteLine("L'azienda che produce il prodotto scelto è:"); Console.WriteLine(JsonSerializer.Serialize(company, options)); } } Console.WriteLine("\n\n****************************************************************"); Console.WriteLine("Elenco di tutte le compagnie"); var companies = await GetAllCompaniesAsync("companies", _client); if (companies != null) { JsonSerializerOptions options = new() { WriteIndented = true }; companies.ForEach(c => Console.WriteLine(JsonSerializer.Serialize(c, options) + "\n")); } Console.WriteLine("Test completato"); } catch (HttpRequestException e) { Console.WriteLine("C'è stato un errore con la richiesta effettuata"); Console.WriteLine(e.Message); } catch (Exception e) { Console.WriteLine(e.Message); } Console.ReadLine(); } static void ShowProduct(Product product) { Console.WriteLine($"Id: {product.Id} Name: {product.Name,-50} Price: " + $"{product.Price}\tCompanyId: {product.CompanyId}"); } static void ShowProducts(List<Product> products) { products.ForEach(p => ShowProduct(p)); } #region REST_Methods_Products /// <summary> /// Effettua la POST di un nuovo prodotto sul server /// </summary> /// <param name="product">il prodotto da creare sul server</param> /// <returns>l'URI della risorsa creata, oppure null se l'uri non è fornito dal server</returns> /// <exception cref="HttpRequestException">Se lo status code restituito dall'endpoint corrisponde ad un errore</exception> static async Task<Uri?> PostProductAsync(Product product, HttpClient client) { HttpResponseMessage response = await client.PostAsJsonAsync( "products", product); //verifico che l'operazione abbia avuto successo response.EnsureSuccessStatusCode(); //sull'oggetto response posso fare diverse elaborazioni //se il server restituisce l'oggetto creato: //1)posso recuperarlo dal body della risposta Product? prodottoCreato = await response.Content.ReadFromJsonAsync<Product>(); if (prodottoCreato != null) { Console.WriteLine("Prodotto creato:\n" + JsonSerializer.Serialize(prodottoCreato)); } //2)oppure posso leggere il body della risposta come stringa e poi elaborarla string responseContentAsstring = await response.Content.ReadAsStringAsync(); Product? prodotto = JsonSerializer.Deserialize<Product>(responseContentAsstring); if (prodotto != null) { Console.WriteLine("Prodotto creato come stringa\n" + responseContentAsstring); } //se il server restituisce l'uri della risorsa // return URI of the created resource. Uri? resourceUri = response.Headers.Location; if (resourceUri != null) { Console.WriteLine("Headers location: " + response.Headers.Location); return response.Headers.Location; } else { return null; } } //effettua la POST utilizzando il metodo PostAsync e l'oggetto StringContent static async Task<Uri?> PostProductAsync2(Product product, HttpClient client) { //trasformo l'oggetto in formato JSON string data = JsonSerializer.Serialize(product); //trasformo il JSON in uno StringContent var content = new StringContent(data, Encoding.UTF8, "application/json"); //faccio la Post asincrona dello StringContent var response = await client.PostAsync("products", content); //verifico che l'operazione sia andata a buon fine response.EnsureSuccessStatusCode(); //sull'oggetto response posso fare diverse elaborazioni //se il server restituisce l'oggetto creato, posso recuperarlo dal body della risposta Product? prodottoCreato = await response.Content.ReadFromJsonAsync<Product>(); Console.WriteLine("Prodotto creato:\n" + JsonSerializer.Serialize(prodottoCreato)); //oppure posso leggere il body della risposta come stringa e poi elaborarla string prodottoCreatoAsString = await response.Content.ReadAsStringAsync(); Product? prodotto = JsonSerializer.Deserialize<Product>(prodottoCreatoAsString); Console.WriteLine("Prodotto creato come stringa\n" + prodottoCreatoAsString); // return URI of the created resource. Console.WriteLine("Headers location: " + response.Headers.Location); return response.Headers.Location; } /// <summary> /// Effettua una GET sul server per recuperare un oggetto di cui è specificato l'id nella rotta /// </summary> /// <param name="path"></param> /// <returns>Il prodotto </returns> static async Task<Product?> GetProductAsync(string path, HttpClient client) { Product? product = null; HttpResponseMessage response = await client.GetAsync(path); if (response.IsSuccessStatusCode) { product = await response.Content.ReadFromJsonAsync<Product?>(); } return product; } /// <summary> /// Effettua una GET sul server per richiedere tutti i prodotti. /// In questo caso si utilizza il metodo GetStreamAsync di HttpClient in abbinamento al DeserializeAsync /// di JsonSerializer /// </summary> /// <param name="path"></param> /// <returns></returns> static async Task<List<Product>?> GetAllProductsAsync(string path, HttpClient client) { await using Stream stream = await client.GetStreamAsync(path); var products = await JsonSerializer.DeserializeAsync<List<Product>>(stream); return products; } static async Task<Product?> PutProductAsync(Product product, HttpClient client) { HttpResponseMessage response = await client.PutAsJsonAsync( $"products/{product.Id}", product); response.EnsureSuccessStatusCode(); //Console.WriteLine("Aggiornamento effettuato"); // Deserialize the updated product from the response body. Product? updatedProduct = await response.Content.ReadFromJsonAsync<Product?>(); //Console.WriteLine("Prodotto aggiornato sul server:\n" + JsonSerializer.Serialize(updatedProduct)); return updatedProduct; } static async Task<HttpStatusCode> DeleteProductAsync(int id, HttpClient client) { HttpResponseMessage response = await client.DeleteAsync( $"products/{id}"); return response.StatusCode; } #endregion REST_Methods_Products #region REST_Methods_Companies static async Task<Company?> GetCompanyAsync(string path, HttpClient client) { return await client.GetFromJsonAsync<Company>(path); } static async Task<List<Company></Company>?> GetAllCompaniesAsync(string path, HttpClient client) { await using Stream stream = await client.GetStreamAsync(path); var companies = await JsonSerializer.DeserializeAsync<List<Company>>(stream); return companies; } static async Task<List<Company>?> GetAllCompaniesAsync2(string path, HttpClient client) { HttpResponseMessage response = await client.GetAsync(path); string? companies = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<List<Company>?>(companies); } static async Task<List<Company>?> GetAllCompaniesAsync3(string path, HttpClient client) => await client.GetFromJsonAsync<List<Company>>(path); #endregion REST_Methods_Companies }}