Tempo di lettura: 18 minuti
Nella prima parte di questo tutorial siamo riusciti a collegare la nostra applicazione React con NodeJs ed Express, quindi abbiamo realizzato un sistema di backend per la web App Heroes.
React con NodeJs: REST
Espandiamo ora la nostra applicazione in modo che ci sia l’API HTTP RESTful come con il server json .
Il trasferimento di stato rappresentativo, alias REST, è stato introdotto nel 2000 nella tesi di Roy Fielding . REST è uno stile architettonico pensato per la creazione di applicazioni web scalabili.
Non approfondiremo la definizione di Fielding di REST ma ci concentreremo solamente sulle API RESTful le quali vengono generalmente utilizzate nelle applicazioni web.
Abbiamo accennato nella parte precedente che gli elementi singolari, come gli Heroe nel caso della nostra applicazione, sono chiamate risorse secondo la filosofia RESTful. Ogni risorsa ha un URL associato che è l’indirizzo univoco della risorsa.
Una convenzione consiste nel creare l’indirizzo univoco per le risorse combinando il nome del tipo di risorsa con l’identificatore univoco della risorsa.
Supponiamo che l’URL principale del nostro servizio sia www.example.com/api .
L’URL per l’intera raccolta di tutte le risorse degli Heroes è www.example.com/api/heroes .
L’indirizzo della singola risorsa , ad esempio , con un identificatore univoco pari ad 10 sarà www.example.com/api/heroes/10.
React con NodeJs: Il verbo HTTP
Possiamo eseguire diverse operazioni sulle risorse. L’operazione da eseguire è definita dal verbo HTTP :
URL | verb | functionality | |
---|---|---|---|
heroes/10 | GET | recupera una singola risorsa | |
heroes | GET | recupera tutte le risorse nella raccolta | |
heroes | POST | crea una nuova risorsa in base ai dati della richiesta | |
heroes/10 | DELETE | rimuove la risorsa identificata | |
heroes/10 | PUT |
|
|
heroes/10 | PATCH | sostituisce una parte della risorsa identificata con i dati della richiesta |
In questo modo riusciamo a definire approssimativamente ciò a cui REST si riferisce come un’interfaccia uniforme , il che significa un modo coerente di definire le interfacce che rende possibile la cooperazione dei sistemi.
Questo sistema di interpretare il REST rientra nel secondo livello di maturità RESTful nel Richardson Maturity Model. Secondo la definizione fornita da Roy Fielding, non abbiamo effettivamente definito un’API REST . In effetti, la grande maggioranza delle presunte API “REST” del mondo non soddisfa i criteri originali di Fielding delineati nella sua dissertazione.
In alcuni punti (vedi ad esempio Richardson, Ruby: RESTful Web Services ) vedrai il nostro modello per una semplice API CRUD , che viene indicata come un esempio di architettura orientata alle risorse invece di REST. Eviteremo di rimanere bloccati a discutere di semantica e invece torneremo a lavorare sulla nostra applicazione.
Recuperare una singola risorsa
Espandiamo la nostra applicazione in modo che offra un’interfaccia REST per operare su singoli eroi. Per prima cosa creiamo un percorso per recuperare una singola risorsa.
L’indirizzo univoco che utilizzeremo per un singolo heroe è del modulo heroes/10 , dove il numero alla fine si riferisce al numero ID univoco dell’eroe.
Possiamo definire i parametri per le rotte in express usando la sintassi dei due punti:
index.js
app.get('/api/heroes/:id', (request, response) => { const id = request.params.id const hero = heroes.find(hero => hero.id === id) response.json(hero) })
Ora app.get('/api/heroes/:id', ...)
gestirà tutte le richieste HTTP GET, che sono nella forma / api / heroes/ QUALCOSA , dove QUALCOSA è una stringa arbitraria.
Il parametro id nel percorso di una richiesta, è accessibile tramite l’ oggetto request :
const id = request.params.id
Il metodo find degli array viene utilizzato per trovare l’eroe con id corrispondente al parametro. L’eroe viene quindi restituito al mittente della request.
Aprite il file index.js e modificate gli id degli eroi, da stringhe a numeri:
... let heroes = [ { "id": "hero_j1m8m4b", "name": "Captain America" }, { "id": "hero_j1m8oq0c", "name": "Iron Man" }, { "id": "hero_j1m8p3fd", "name": "Hulk" } ] ...
a così:
... let heroes = [ { "id": 1, "name": "Captain America" }, { "id": 2, "name": "Iron Man" }, { "id": 3, "name": "Hulk" } ] ...
considerate che al momento questi sono mock dati ma nella realtà gli id restituiti sono quasi sempre valori numerici incrementali.
Ora se, avviate l’applicazione con npm run dev e la testate all’indirizzo: http://localhost:3001/api/heroes/1 noterete che questa restituisce una pagina bianca.
E’ il caso di eseguire il debug e scoprire il motivo, aprite il file index.js:
... app.get('/api/heroes/:id', (request, response) => { const id = request.params.id console.log(id) const hero = heroes.find(hero => hero.id === id) console.log(hero) response.json(hero) }) ...
basta aggiungere il buon vecchio console.log() 🙂 🙂 , quindi aprite il terminale e vi trovate :
[nodemon] restarting due to changes... [nodemon] starting `node index.js` Server running on port 3001 [nodemon] restarting due to changes... [nodemon] starting `node index.js` Server running on port 3001 1 undefined
Il parametro id dal percorso viene passato alla nostra applicazione, ma il metodo find non trova un eroe corrispondente.
Modifica applicazione
Per approfondire la nostra indagine, aggiungiamo anche un log della console all’interno della funzione di confronto passata al metodo find . Per fare ciò, dobbiamo eliminare la sintassi della funzione freccia compatta hero=> hero.id === id e utilizzare la sintassi con un’istruzione return esplicita:
app.get('/api/heroes/:id', (request, response) => { const id = request.params.id const hero = heroes.find(hero =>{ console.log(hero.id, typeof hero.id, id, typeof id, hero.id === id) return hero.id === id }) console.log(hero) response.json(hero) })
Ora l’output del terminale ina volta avviato il server è questo:
1 number 1 string false 2 number 1 string false 3 number 1 string false
La causa del bug ora è più chiara. La variabile id contiene una stringa “1”, mentre gli id delle note sono numeri interi. In JavaScript, il confronto “triplo uguale” === considera tutti i valori di diversi tipi non uguali per impostazione predefinita, il che significa che 1 non è “1”.
Risolvere il bug
Risolviamo il problema convertendo il parametro id da tipo stringa a tipo numero:
app.get('/api/heroes/:id', (request, response) => { const id = Number(request.params.id) const hero = heroes.find(hero => hero.id === id) response.json(hero) })
provate ora:
Tuttavia vi è ancora un problema, difatti se cambiassimo il parametro id con un altro numero non esistente, il server non darebbe errore ma mostrerebbe una pagina bianca:
Il codice di stato HTTP restituito è 200, il che significa che la risposta è riuscita. Non vengono restituiti dati con la risposta, poiché il valore dell’intestazione della lunghezza del contenuto è 0 e lo stesso può essere verificato dal browser.
La ragione di questo comportamento è che la variabile hero è impostata su undefined se non viene trovato alcun eroe corrispondente. La situazione deve essere gestita sul server in un modo migliore. Se non viene trovato l’eroe, il server dovrebbe rispondere con il codice di stato 404 non trovato invece di 200.
Apportiamo la seguente modifica al nostro codice:
app.get('/api/heroes/:id', (request, response) => { const id = Number(request.params.id) const hero = heroes.find(hero => hero.id === id) if(hero){ response.json(hero) } else { response.status(404).end() } })
Poiché nessun dato è allegato alla risposta, utilizziamo il metodo dello status per impostare lo stato e il metodo end per rispondere alla richiesta senza inviare alcun dato.
La condizione if sfrutta il fatto che tutti gli oggetti JavaScript sono true, il che significa che vengono valutati come veri in un’operazione di confronto. Tuttavia, undefined è false, nel senso che verrà valutato come falso.
La nostra applicazione funziona e invia il codice di stato dell’errore se non viene trovato alcun eroe. Tuttavia, l’applicazione non restituisce nulla da mostrare all’utente, come fanno normalmente le applicazioni web quando visitiamo una pagina che non esiste. In realtà non è necessario visualizzare nulla nel browser perché le API REST sono interfacce destinate all’uso programmatico e il codice di stato dell’errore è tutto ciò che è necessario.
Eliminare una risorsa
Andiamo avanti con l’implementare l’eliminazione di un eroe dalla nostra lista:
app.delete('/api/heroes/:id', (request, response)=> { const id = Number(request.params.id) const hero = heroes.filter(hero => hero.id !== id) response.status(204).end() })
Se l’eliminazione della risorsa ha esito positivo, il che significa che l’eroe esiste e viene rimosso, rispondiamo alla richiesta con il codice di stato 204 nessun contenuto e non restituiamo alcun dato con la risposta.
Non c’è consenso su quale codice di stato dovrebbe essere restituito a una richiesta DELETE se la risorsa non esiste. In realtà, le uniche due opzioni sono 204 e 404. Per semplicità la nostra applicazione risponderà con 204 in entrambi i casi.
React con NodeJs: Postman
Come testiamo l’operazione di cancellazione? Le richieste HTTP GET sono facili da effettuare dal browser. Potremmo scrivere un po ‘di JavaScript per testare l’eliminazione, ma scrivere codice di test non è sempre la soluzione migliore in ogni situazione.
Esistono molti strumenti per rendere più facile il test dei backend. Uno di questi è un programma da riga di comando curl . Tuttavia, invece di curl, daremo un’occhiata all’utilizzo di Postman per testare l’applicazione.
Installiamo Postman e proviamolo:
Usare Postman è abbastanza facile in questa situazione. È sufficiente definire l’URL e quindi selezionare il tipo di richiesta corretto (DELETE).
Il server di backend sembra rispondere correttamente. Facendo una richiesta HTTP GET a http://localhost/api/heroes/1 vediamo che l’eroe con id 1 non è più nell’elenco, il che indica che l’eliminazione è avvenuta con successo.
Poiché gli eroi nell’applicazione vengono salvati solo in memoria, l’elenco degli eroi tornerà al suo stato originale quando riavviamo l’applicazione.
Utilizzare il plugin REST client di Visual Studio Code
Se utilizzi Visual Studio Code puoi installare il plugin Rest client al posto di Postman. Una volta installato questi è molto semplice da utilizzare. Nella radice principale del progetto crea una directory e chiamala request.
Salviamo tutte le richieste del client REST nella directory come file che terminano con l’ estensione .rest .
Creiamo un nuovo file get_all_heroes.rest e definiamo la richiesta che recupera tutti gli eroi:
quindi fai click su Send request:
Come potete notare il client REST esegue la richiesta HTTP e la risposta dal server verrà aperta nell’editor.
Aggiungere Erori nella lista
L’aggiunta di nuovi eroi nella lista avviene mediante richiesta HTTP POST all’indirizzo http://localhost/api/heroes ed inviando tutte le informazioni al body della pagina in formato JSON.
Per poter poi accedere a tali dati abbiamo bisogno di json-parser di express da implementare ,quindi, nel file index.js:
const express = require('express') const app = express(); app.use(express.json()) ...
ora scriviamo il codice relativo alla richiesta POST:
... app.post('/api/heroes', (request, response) => { const hero = request.body console.log(hero) response.json(hero) }) ...
La funzione del gestore eventi può accedere ai dati dalla proprietà body dell’oggetto request.
Senza json-parser, la proprietà body non sarebbe definita. Il json-parser funziona in modo da prendere i dati JSON di una richiesta, trasformarli in un oggetto JavaScript e quindi collegarli alla proprietà body dell’oggetto request prima che venga chiamato il gestore della rotta.
Per il momento, l’applicazione non fa nulla con i dati ricevuti oltre a stamparli sulla console e rimandarli indietro nella response.
Prima di implementare il resto della logica dell’applicazione, verifichiamo con Postman che i dati siano effettivamente ricevuti dal server. Oltre a definire l’URL e il tipo di richiesta in Postman, dobbiamo anche definire i dati inviati nel corpo :
se fate click su send ed aprite il terminale noterete:
[nodemon] restarting due to changes... [nodemon] starting `node index.js` Server running on port 3001 { name: 'Batman', important: true }
che l’applicazione stampa i dati che abbiamo inviato nella richiesta alla console.
NB Il mio consiglio è quello di mantenere sempre visibile il terminale che esegue l’applicazione quando si lavora sul backend. Grazie a Nodemon qualsiasi modifica apportata al codice riavvierà l’applicazione. Se presti attenzione alla console, sarai immediatamente in grado di rilevare gli errori che si verificano nell’applicazione
Allo stesso modo, è utile controllare la console per assicurarsi che il backend si comporti come ci aspettiamo in diverse situazioni, come quando inviamo dati con una richiesta HTTP POST. Naturalmente, è una buona idea aggiungere molti comandi console.log al codice mentre l’applicazione è ancora in fase di sviluppo.
Una potenziale causa di problemi è un’intestazione Content-Type impostata in modo errato nelle richieste. Questo può accadere con Postman se il tipo di corpo non è definito correttamente:
L’ intestazione Content-Type è impostata su text, il server sembra ricevere un oggetto vuoto:
Il server non sarà in grado di analizzare i dati correttamente senza il valore corretto nell’intestazione. Non proverà nemmeno a indovinare il formato dei dati, poiché esiste un’enorme quantità di potenziali tipi di contenuto .
Aggiunta dati all’applicazione
Una volta che sappiamo che l’applicazione riceve i dati correttamente, è il momento di finalizzare la gestione della richiesta:
app.post('/api/heroes', (request, response) => { const maxId = heroes.length > 0 ? Math.max(...heroes.map(h => h.id)) : 0 const hero = request.body hero.id = maxId + 1 heroes = heroes.concat(hero) response.json(hero) })
Abbiamo bisogno di un ID univoco per l’eroe. Innanzitutto, troviamo il numero di ID più grande nell’elenco corrente e lo assegniamo alla variabile maxId . L’id del nuovo eroe viene quindi definito come maxId + 1 . Questo metodo in effetti non è consigliato, ma per il momemto accontentiamoci, in seguito cambieremo.
Questa versione ha ancora il problema che la richiesta HTTP POST può essere utilizzata per aggiungere oggetti con proprietà arbitrarie. Miglioriamo l’applicazione definendo che la proprietà del contenuto non può essere vuota. Le proprietà important e date avranno valori predefiniti. Tutte le altre proprietà vengono eliminate:
const generateId =() => { const maxId = heroes.length > 0 ? Math.max(...heroes.map(h => h.id)): 0 return maxId + 1 } app.post('/api/heroes', (request, response) => { const body = request.body if(!body.name){ return response.status(404).json({ error: 'Contenuto vuoto' }) } const hero = { name: body.name, important: body.important || false, date: new Date(), id: generateId(), } heroes = heroes.concat(hero) response.json(hero) })
In questo caso la logica per la generazione di un nuovo Id è stata definita nel metodo , separato, generateId:
const generateId =() => { const maxId = heroes.length > 0 ? Math.max(...heroes.map(h => h.id)): 0 return maxId + 1 }
se nei dati generati mancasse il valore del name, il server risponderà alle request con un codice di stato pari a 404, contenuto inesistente:
if(!body.name){ return response.status(404).json({ error: 'Contenuto vuoto' })
la chiamata return è molto importante in quanto, se mancasse, il codice verrebbe eseguito fino alla fine e l’eroe mancante verrebbe salvato nella lista.
Se mancasse la proprietà important, questi verrebbe definita , di default, come false:
important: body.important || false,
Trovi tutto il codice relativo al tutorial nel mio repository Git e nello specifico al branch backend:
Importante: PER CLONARE , DA GIT HUB, UN BRANCH SPECIFICO ,COME IN QUESTO CASO, DIGITA –branch <nome-branch> prima dell’url del repository, quindi inserisci il nome della directory che verrà creata nel tuo pc, nello specifico:
git clone –branch backend https://github.com/freewebsolution/React2.0.git backend
i file verranno salvati all’interno della cartella backend 🙂 🙂
Una volta clonato il repository da remoto ricordati di eseguire il comando npm install prima di npm run dev 😉
Prima di concludere
Analiziamo il metodo che genera gli ID :
const generateId =() => { const maxId = heroes.length > 0 ? Math.max(...heroes.map(h => h.id)): 0 return maxId + 1 }
il corpo della funzione contiene:
Math.max(...heroes.map(h => h.id))
heroes.map (h => h.id) crea un nuovo array che contiene tutti gli ID degli eroi. Il metodo Math.max restituisce il valore massimo dei numeri che vengono passati. Visto che heroes.map (h => h.id) è un array , non può essere passato ,direttamente, come parametro a Math.max quindi quindi lo trasformo in numeri individuali grazie allo spreed operator …
Informazioni sui tipi di richiesta HTTP
Lo standard HTTP parla di due proprietà relative ai tipi di richiesta, sicurezza e idempotenza .
La richiesta HTTP GET dovrebbe essere sicura :
In particolare, è stata stabilita la convenzione che i metodi GET e HEAD NON DOVREBBERO avere il significato di intraprendere un’azione diversa dal recupero. Questi metodi dovrebbero essere considerati “sicuri”.
Sicurezza significa che la richiesta in esecuzione non deve causare effetti collaterali nel server. Per effetti collaterali si intende che lo stato del database non deve cambiare a seguito della richiesta e la risposta deve restituire solo dati già esistenti sul server.
Niente può mai garantire che una richiesta GET sia effettivamente sicura , questa è in realtà solo una raccomandazione definita nello standard HTTP. Aderendo ai principi RESTful nella nostra API, le richieste GET vengono infatti sempre utilizzate in modo sicuro .
Lo standard HTTP definisce anche il tipo di richiesta HEAD , che dovrebbe essere sicuro. In pratica HEAD dovrebbe funzionare esattamente come GET ma non restituisce altro che il codice di stato e le intestazioni di risposta. Il corpo della risposta non verrà restituito quando effettui una richiesta HEAD.
Tutte le richieste HTTP eccetto POST dovrebbero essere idempotenti :
I metodi possono anche avere la proprietà di “idempotenza” in quanto (a parte errori o problemi di scadenza) gli effetti collaterali di N> 0 richieste identiche sono gli stessi di una singola richiesta. I metodi GET, HEAD, PUT e DELETE condividono questa proprietà
Ciò significa che se una richiesta ha effetti collaterali, il risultato dovrebbe essere lo stesso indipendentemente da quante volte la richiesta viene inviata.
Se facciamo una richiesta HTTP PUT all’url / api / heroes/ 10 e con la richiesta inviamo i dati {content: “no side effects!”, Important: true} , il risultato è lo stesso indipendentemente da quante volte il richiesta viene inviata.
Come la sicurezza per la richiesta GET, anche l’ idempotenza è solo una raccomandazione nello standard HTTP e non qualcosa che può essere garantito semplicemente in base al tipo di richiesta. Tuttavia, quando la nostra API aderisce ai principi RESTful, le richieste GET, HEAD, PUT e DELETE vengono utilizzate in modo tale da risultare idempotenti.
POST è l’unico tipo di richiesta HTTP che non è né sicuro né idempotente . Se inviamo 5 diverse richieste HTTP POST a / api / heroes con un corpo di {content: “many same”, important: true} , i 5 eroi risultanti sul server avranno tutte lo stesso contenuto.
Middleware
L’express json-parser che abbiamo utilizzato in precedenza è un cosiddetto middleware .
I middleware sono funzioni che possono essere utilizzate per la gestione di oggetti richiesta e risposta .
Il json-parser prende i dati grezzi dalle richieste archiviate nell’oggetto request, li analizza in un oggetto JavaScript e lo assegna all’oggetto request come un nuovo corpo di proprietà .
In pratica è possibile utilizzare più middleware contemporaneamente. Quando ne hai più di uno, vengono eseguiti uno per uno nell’ordine in cui sono stati utilizzati in express.
Implementiamo il nostro middleware che stampa le informazioni su ogni richiesta inviata al server.
Il middleware è una funzione che riceve tre parametri:
const requestLogger = (request, response, next) => { console.log('Method:', request.method) console.log('Path: ', request.path) console.log('Body: ', request.body) console.log('---') next() }
Alla fine del corpo della funzione viene chiamata la funzione successiva che è stata passata come parametro. La funzione successiva cede il controllo al middleware successivo.
Il middleware viene utilizzato in questo modo:
app.use(requestLogger)
Le funzioni middleware vengono chiamate nell’ordine in cui vengono utilizzate con il metodo di utilizzo dell’oggetto server Express . Si noti che json-parser viene utilizzato prima del middleware requestLogger , perché altrimenti request.body non verrà inizializzato quando viene eseguito il logger!
Le funzioni middleware devono essere utilizzate prima delle rotte se vogliamo che vengano eseguite prima che vengano chiamati i gestori di eventi della rotta. Ci sono anche situazioni in cui vogliamo definire le funzioni middleware dopo le rotte. In pratica, questo significa che stiamo definendo funzioni middleware che vengono chiamate solo se nessuna rotta gestisce la richiesta HTTP.
Aggiungiamo il seguente middleware dopo le nostre rotte, che viene utilizzato per catturare le richieste fatte a rotte inesistenti. Per queste richieste, il middleware restituirà un messaggio di errore in formato JSON.
const unknownEndpoint = (request, response) => { response.status(404).send({ error: 'unknown endpoint' }) } app.use(requestLogger)
Puoi trovare il codice aggiornato nel branch di questo repository.
Se volete Approfondire le conoscenze su Git e GIT hub potete seguire il corso completo su Udemy sempre scontato a 9.99/12.99 € , inoltre, nel momento in cui non foste soddisfatti, avete la possibilità di ottenere il rimborso completo dello stesso entro 30 giorni dalla data di acquisto!