Progettazione API RESTful con NodeJS e Restify

Cosa starai creando

L'API RESTful comprende due concetti principali: Risorsa, e Rappresentazione. La risorsa può essere qualsiasi oggetto associato ai dati o identificato con un URI (più di un URI può fare riferimento alla stessa risorsa) e può essere gestito utilizzando i metodi HTTP. La rappresentazione è il modo in cui visualizzi la risorsa. In questo tutorial tratteremo alcune informazioni teoriche sulla progettazione dell'API RESTful e implementeremo un'API di applicazioni di blogging di esempio utilizzando NodeJS.

Risorsa

La scelta delle risorse corrette per un'API RESTful è una sezione importante della progettazione. Prima di tutto, devi analizzare il tuo dominio aziendale e poi decidere quanti e quali tipi di risorse saranno utilizzati per le tue esigenze aziendali. Se stai progettando un'API di blogging, probabilmente la userai Articolo, Utente, e Commento. Quelli sono i nomi delle risorse e i dati associati sono la risorsa stessa:

"title": "Come progettare RESTful API", "content": "RESTful API design è un caso molto importante nel mondo dello sviluppo software.", "author": "huseyinbabal", "tags": ["tecnologia" , "nodejs", "node-restify"] "category": "NodeJS"

Verbi delle risorse

È possibile procedere con un'operazione di risorsa dopo aver deciso le risorse richieste. L'operazione qui si riferisce ai metodi HTTP. Ad esempio, per creare un articolo, puoi effettuare la seguente richiesta:

POST / articoli Host HTTP / 1.1: localhost: 3000 Content-Type: application / json "title": "RESTful API Design con Restify", "slug": "restful-api-design-with-restify", "content" : "Pellentesque habitant morbi tristique senectus et netus et maleuada fames ac turpis egestas.", "Author": "huseyinbabal"

Allo stesso modo, puoi visualizzare un articolo esistente inviando la seguente richiesta:

GET / articles / 123456789012 Host HTTP / 1.1: localhost: 3000 Content-Type: application / json

Che ne pensi di aggiornare un articolo esistente? Posso sentire che stai dicendo:

Posso fare un'altra richiesta POST a / articles / update / 123456789012 con il payload.

Forse preferibile, ma l'URI sta diventando più complesso. Come detto in precedenza, le operazioni possono fare riferimento ai metodi HTTP. Questo significa, dichiarare il aggiornare operazione nel metodo HTTP invece di metterlo nell'URI. Per esempio:

PUT / articoli / 123456789012 Host HTTP / 1.1: localhost: 3000 Content-Type: application / json "title": "Aggiornato come progettare API RESTful", "content": "La progettazione dell'API RESTful aggiornata è un caso molto importante nel software development world. "," author ":" huseyinbabal "," tags ": [" tecnologia "," nodejs "," restify "," un altro tag "]" categoria ":" NodeJS "

A proposito, in questo esempio vedi i tag e i campi delle categorie. Quelli non devono essere campi obbligatori. Puoi lasciarli vuoti e impostarli in futuro. 

A volte, è necessario eliminare un articolo quando è obsoleto. In tal caso puoi usare a ELIMINA Richiesta HTTP a / articoli / 123456789012.

I metodi HTTP sono concetti standard. Se li usi come operazione, avrai degli URI semplici e questo tipo di semplice API ti aiuterà a guadagnare consumatori felici.

Cosa succede se si desidera inserire un commento per un articolo? È possibile selezionare l'articolo e aggiungere un nuovo commento all'articolo selezionato. Utilizzando questa dichiarazione, è possibile utilizzare la seguente richiesta:

POST / articoli / 123456789012 / commenti Host HTTP / 1.1: localhost: 3000 Content-Type: application / json "text": "Wow! Questo è un buon tutorial", "author": "john doe"

La precedente forma di risorsa è chiamata come sub-risorsa. Commento è una sotto risorsa di Articolo. Il Commento il carico utile sopra verrà inserito nel database come figlio di Articolo. A volte, un diverso URI si riferisce alla stessa risorsa. Ad esempio, per visualizzare un commento specifico, puoi utilizzare:

GET / articles / 123456789012 / comments / 123 Host HTTP / 1.1: localhost: 3000 Content-Type: application / json 

o:

GET / comments / 123456789012 Host HTTP / 1.1: localhost: 3000 Content-Type: application / json

versioning

In generale, le funzionalità dell'API cambiano di frequente al fine di fornire nuove funzionalità ai consumatori. In tal caso, possono esistere due versioni della stessa API contemporaneamente. Per separare queste due funzioni, puoi utilizzare il controllo delle versioni. Esistono due forme di controllo delle versioni

  1. Versione in URI: È possibile fornire il numero di versione nell'URI. Per esempio, /v1.1/articles/123456789012.
  2. Versione nell'intestazione: Fornire il numero di versione nell'intestazione e non modificare mai l'URI.Per esempio:
GET / articles / 123456789012 Host HTTP / 1.1: localhost: 3000 Accept-Version: 1.0

In realtà, la versione cambia solo la rappresentazione della risorsa, non il concetto della risorsa. Quindi, non è necessario modificare la struttura dell'URI. In v1.1, forse un nuovo campo è stato aggiunto all'articolo. Tuttavia, restituisce ancora un articolo. Nella seconda opzione, l'URI è ancora semplice e gli utenti non hanno bisogno di modificare l'URI nelle implementazioni lato client. 

È importante progettare una strategia per situazioni in cui il consumatore non fornisce un numero di versione. È possibile generare un errore quando la versione non viene fornita oppure è possibile restituire una risposta utilizzando la prima versione. Se si utilizza l'ultima versione stabile come predefinita, i consumatori possono ottenere molti errori per le loro implementazioni sul lato client.

Rappresentazione

La rappresentazione è il modo in cui un'API visualizza la risorsa. Quando chiami un endpoint API, ti verrà restituita una risorsa. Questa risorsa può essere in qualsiasi formato come XML, JSON, ecc. JSON è preferibile se stai progettando una nuova API. Tuttavia, se si aggiorna un'API esistente utilizzata per restituire una risposta XML, è possibile fornire un'altra versione per una risposta JSON. 

Sono sufficienti informazioni teoriche sulla progettazione dell'API RESTful. Diamo un'occhiata all'utilizzo della vita reale progettando e implementando un'API di Blogging usando Restify.

API REST di Blogging

Design

Per progettare un'API RESTful, dobbiamo analizzare il dominio aziendale. Quindi possiamo definire le nostre risorse. In un'API di Blogging, abbiamo bisogno di:

  • Crea, Aggiorna, Elimina, Visualizza Articolo
  • Crea un commento per uno specifico Articolo, Aggiorna, Elimina, Visualizza, Commento
  • Crea, Aggiorna, Elimina, Visualizza Utente

In questa API, non illustrerò come autenticare un utente per creare un articolo o un commento. Per la parte di autenticazione, è possibile fare riferimento all'esercitazione basata su token con AngularJS & NodeJS tutorial. 

I nostri nomi delle risorse sono pronti. Le operazioni sulle risorse sono semplicemente CRUD. È possibile fare riferimento alla seguente tabella per una presentazione generale dell'API.

Nome della risorsa Verbi HTTP Metodi HTTP
Articolo creare articolo
aggiorna l'articolo
elimina l'articolo
vedi articolo
POST / articoli con payload
PUT / articoli / 123 con Payload
CANCELLA / articoli / 123
GET / article / 123
Commento crea un commento
aggiorna Coment
elimina commento
vedi Commento
POST / articoli / 123 / commenti con payload
PUT / comments / 123 con Payload
CANCELLA / commenti / 123
OTTIENI / commenti / 123
Utente creare un utente
aggiorna utente
cancella utente
guarda Utente
POST / utenti con payload
PUT / users / 123 con Payload
ELIMINA / utenti / 123
GET / users / 123

Impostazione del progetto

In questo progetto useremo NodeJS con Restify. Le risorse saranno salvate nel MongoDB Banca dati. Prima di tutto, possiamo definire le risorse come modelli in Restify.

Articolo

var mongoose = require ("mangusta"); var Schema = mongoose.Schema; var ArticleSchema = new Schema (title: String, slug: String, content: String, author: type: String, ref: "User"); mongoose.model ('Article', ArticleSchema);

Commento

var mongoose = require ("mangusta"); var Schema = mongoose.Schema; var CommentSchema = new Schema (text: String, article: type: String, ref: "Article", author: type: String, ref: "User"); mongoose.model ('Comment', CommentSchema);

Utente

Non ci sarà alcuna operazione per la risorsa Utente. Assumeremo che conosciamo già l'utente corrente che sarà in grado di operare su articoli o commenti.

Puoi chiedere da dove viene questo modulo mangusta. È il framework ORM più popolare per MongoDB scritto come modulo NodeJS. Questo modulo è incluso nel progetto all'interno di un altro file di configurazione. 

Ora possiamo definire i nostri verbi HTTP per le risorse di cui sopra. Puoi vedere quanto segue:

var restify = require ('restify'), fs = require ('fs') var controller = , controllers_path = process.cwd () + '/ app / controller' fs.readdirSync (controllers_path) .forEach (funzione (file ) if (file.indexOf ('. js')! = -1) controller [file.split ('.') [0]] = richiede (percorso_controls + '/' + file)) var server = restify.createServer (); server .use (restify.fullResponse ()) .use (restify.bodyParser ()) // Article Avvia server.post ("/ articles", controllers.article.createArticle) server.put ("/ articles /: id", controller.article.updateArticle) server.del ("/ articles /: id", controllers.article.deleteArticle) server.get (percorso: "/ articles /: id", versione: "1.0.0", controller. article.viewArticle) server.get (percorso: "/ articles /: id", versione: "2.0.0", controllers.article.viewArticle_v2) // Articolo Fine // Commento Avvia server.post ("/ comments" , controllers.comment.createComment) server.put ("/ commenti /: id", controllers.comment.viewComment) server.del ("/ commenti /: id", controllers.comment.deleteComment) server.get ("/ comments /: id ", controllers.comment.viewComment) // Comment End var port = process.env.PORT || 3000; server.listen (port, function (err) if (err) console.error (err) else console.log ('L'app è pronta a:' + port)) if (process.env.environment == 'production') ) process.on ('uncaughtException', function (err) console.error (JSON.parse (JSON.stringify (err, ['stack', 'message', 'inner'], 2))))

In questo frammento di codice, prima di tutto i file controller che contengono i metodi controller vengono iterati e tutti i controller sono inizializzati per eseguire una richiesta specifica all'URI. Successivamente, gli URI per operazioni specifiche sono definiti per le operazioni CRUD di base. C'è anche il controllo delle versioni per una delle operazioni sull'articolo. 

Ad esempio, se si specifica la versione come 2 nell'intestazione Accept-Version, viewArticle_v2 sarà eseguito. viewArticle e viewArticle_v2 entrambi fanno lo stesso lavoro, mostrando la risorsa, ma mostrano la risorsa articolo in un formato diverso, come puoi vedere nel titolo campo sottostante. Infine, il server viene avviato su una porta specifica e vengono applicati alcuni controlli di segnalazione degli errori. Possiamo procedere con i metodi del controller per le operazioni HTTP sulle risorse.

article.js

var mongoose = require ('mangusta'), Article = mongoose.model ("Articolo"), ObjectId = mongoose.Types.ObjectId exports.createArticle = function (req, res, next) var articleModel = new Article (req.body ); articleModel.save (function (err, article) if (err) res.status (500); res.json (type: false, data: "Errore occorso:" + err) else res.json ( type: true, data: article)) exports.viewArticle = function (req, res, next) Article.findById (new ObjectId (req.params.id), function (err, article) if ( err) res.status (500); res.json (tipo: falso, dati: "Errore occorso:" + err) else if (articolo) res.json (tipo: true, dati: articolo ) else res.json (type: false, data: "Articolo:" + req.params.id + "non trovato") export.viewArticle_v2 = function (req, res, next) Article.findById (new ObjectId (req.params.id), function (err, article) if (err) res.status (500); res.json (type: false, data: "Errore occorso:" + err) else if (article) article.title = article.title + "v2" res.json (tipo: vero, dati: articolo) else res.json (tipo: falso, dati : "Articolo:" + req.params.id + "non trovato")) exports.updateArticle = function (req, res, next) var updatedArticleModel = new Article (req.body); Article.findByIdAndUpdate (new ObjectId (req.params.id), updatedArticleModel, function (err, article) if (err) res.status (500); res.json (type: false, data: "Errore occorso: "+ err) else if (article) res.json (type: true, data: article) else res.json (type: false, data:" Articolo: "+ req.params. id + "not found")) exports.deleteArticle = function (req, res, next) Article.findByIdAndRemove (new Object (req.params.id), function (err, article) if (err ) res.status (500); res.json (tipo: falso, dati: "Errore occorso:" + err) else res.json (tipo: vero, dati: "Articolo:" + req. params.id + "cancellato con successo")) 

Puoi trovare una spiegazione delle operazioni CRUD di base sul lato Mongoose qui sotto:

  • Createarticle: Questo è semplice salvare operazione su articleModel inviato dal corpo della richiesta. Un nuovo modello può essere creato passando il corpo della richiesta come costruttore a un modello come var articleModel = new Article (req.body)
  • viewArticle: Per visualizzare i dettagli dell'articolo, è necessario un ID articolo nel parametro URL. trova uno con un parametro ID è sufficiente per restituire i dettagli dell'articolo.
  • updateArticle: L'aggiornamento dell'articolo è una semplice query di ricerca e alcune manipolazioni di dati sull'articolo restituito. Infine, il modello aggiornato deve essere salvato nel database emettendo a salvare comando.
  • deleteArticle: findByIdAndRemove è il modo migliore per eliminare un articolo fornendo l'ID dell'articolo.

I comandi di Mongoose menzionati sopra sono semplicemente metodi statici come l'oggetto Article che è anche un riferimento allo schema di Mongoose.

comment.js

var mongoose = require ('mangusta'), Comment = mongoose.model ("Comment"), Article = mongoose.model ("Article"), ObjectId = mongoose.Types.ObjectId exports.viewComment = function (req, res)  Article.findOne ("comments._id": nuovo ObjectId (req.params.id), "commenti. $": 1, funzione (err, commento) if (err) res.status (500) ; res.json (type: false, data: "Errore occorso:" + err) else if (commento) res.json (tipo: vero, dati: nuovo Commento (comment.comments [0]) ) else res.json (tipo: falso, dati: "Commento:" + req.params.id + "non trovato") exports.updateComment = function (req, res, next) var updatedCommentModel = new Comment (req.body); console.log (updatedCommentModel) Article.update ("comments._id": nuovo ObjectId (req.params.id), "$ set": "commenti. $. testo": updatedCommentModel.text, "commenti. $ .author ": updatedCommentModel.author, function (err) if (err) res.status (500); res.json (type: false, data:" Errore occorso: "+ err) else res.json (type: true, data: "Commento:" + req.params.id + "updated")) exports.deleteComment = function (req, res, next) Article.findOneAndUpdate ( "comments._id": nuovo ObjectId (req.params.id), "$ pull": "commenti": "_id": nuovo ObjectId (req.params.id), funzione (err, articolo) if (err) res.status (500); res.json (type: false, data: "Errore occorso:" + err) else if (article) res.json (type: true, data: article) else res.json (type: false, data: "Commento:" + req.params.id + "not found")

Quando si effettua una richiesta a uno degli URI delle risorse, verrà eseguita la relativa funzione indicata nel controller. Ogni funzione all'interno dei file del controller può utilizzare req e res oggetti. Il commento la risorsa qui è una sotto-risorsa di Articolo. Tutte le operazioni di interrogazione vengono effettuate tramite il modello Articolo per trovare un sottodocumento e apportare l'aggiornamento necessario. Tuttavia, ogni volta che provi a visualizzare una risorsa di commento, ne vedrai una anche se non ci sono raccolte in MongoDB.  

Altri suggerimenti di design

  • Seleziona risorse di facile comprensione per offrire un facile utilizzo ai consumatori.
  • Lascia che la logica aziendale sia implementata dai consumatori. Ad esempio, la risorsa Articolo ha un campo chiamato lumaca. I consumatori non devono inviare questo dettaglio all'API REST. Questa strategia di slug dovrebbe essere gestita dal lato REST API per ridurre l'accoppiamento tra API e utenti. I consumatori devono solo inviare i dettagli del titolo ed è possibile generare lo slug in base alle proprie esigenze aziendali sul lato REST API.
  • Implementa un livello di autorizzazione per i tuoi endpoint API. I consumatori non autorizzati possono accedere a dati riservati appartenenti a un altro utente. In questa esercitazione, non abbiamo trattato la risorsa Utente, ma puoi fare riferimento all'autenticazione basata su token con AngularJS e NodeJS per ulteriori informazioni sulle autenticazioni API.
  • URI utente anziché stringa di query. / articoli / 123  (Buono), / Articoli? Id = 123 (Male).
  • Non mantenere lo stato; usa sempre input / output istantanei.
  • Usa il nome per le tue risorse. È possibile utilizzare i metodi HTTP per operare sulle risorse.

Infine, se si progetta un'API RESTful seguendo queste regole fondamentali, si avrà sempre un sistema flessibile, manutenibile e facilmente comprensibile.