Fermer

mai 30, 2018

Création d'applications et de services avec le framework Hapi.js –


Hapi.js est décrit comme "un framework riche pour construire des applications et des services". Les paramètres par défaut intelligents de Hapi simplifient la création d'API JSON, et sa conception modulaire et son système de plugin vous permettent d'étendre ou de modifier son comportement facilement.

La version récente de version 17.0 a pleinement adopté async et attendent donc vous allez écrire du code qui semble synchrone mais qui n'est pas bloquant et évite les rappels d'enfer. Win-win.

Le projet

Dans cet article, nous allons créer l'API suivante pour un blog classique à partir de rien:

 # Actions RESTful pour extraire, créer, mettre à jour et supprimer des articles
GET / articles articles # index
GET / articles /: id articles # show
POST / articles articles # créer
PUT / articles /: id articles # mise à jour
SUPPRIMER / articles /: id articles # destroy

# Itinéraires imbriqués pour créer et supprimer des commentaires
POST / articles /: id / commentaires commentaires # créer
SUPPRIMER / articles /: id / commentaires commentaires # détruire

# Authentification avec des jetons Web JSON (JWT)
Authentifications POST / authentifications # créer

L'article couvrira:

  • API principale de Hapi: routage, requête et réponse
  • modèles et persistance dans une base de données relationnelle
  • routes et actions pour articles et commentaires
  • testant une API REST avec HTTPie [19659008] authentification avec JWT et sécurisation des routes
  • validation
  • affichage HTML et mise en page pour la route racine / .

Le point de départ

Assurez-vous d'avoir une version récente de Node.js installé; node -v devrait retourner 8.9.0 ou supérieur.

Télécharger le code de départ d'ici avec git:

 git clone https://github.com/markbrown4/ hapi-api.git
cd hapi-api
npm installer

Ouvrez package.json et vous verrez que le script "start" s'exécute server.js avec nodemon . Cela prendra soin de redémarrer le serveur pour nous quand nous changeons un fichier.

Exécutez npm start et ouvrez http: // localhost: 3000 / :

 [{ "so": "hapi!" }]

Regardons la source:

 // server.js
const Hapi = require ('hapi')

// Configure l'instance du serveur
serveur const = Hapi.server ({
  hôte: 'localhost',
  port: 3000
})

// Ajouter des routes
server.route ({
  méthode: 'GET',
  chemin: '/',
  gestionnaire: () = & gt; {
    retour [{ so: 'hapi!' }]
  }
})

// Aller!
server.start (). then (() = & gt; {
  console.log ('Serveur fonctionnant sous:', server.info.uri)
}). catch (err = & gt; {
  console.log (err)
  process.exit (1)
})

The Route Handler

Le gestionnaire d'itinéraire est la partie la plus intéressante de ce code. Remplacez-le par le code ci-dessous, commentez les lignes de retour une par une et testez la réponse dans votre navigateur.

 server.route ({
  méthode: 'GET',
  chemin: '/',
  gestionnaire: () = & gt; {
    // return [{ so: 'hapi!' }]
    retour 123
    renvoie «& lt; h1 & gt; & lt; marquee & gt; HTML & lt; em & gt; rules & lt; / em & gt; & lt; / marquee & gt; & lt; / h1 & gt;`
    retourner null
    retourne une nouvelle erreur ('Boom')
    return Promise.resolve ({whoa: true})
    return require ('fs'). createReadStream ('index.html')
  }
})

Pour envoyer une réponse, vous devez simplement renvoyer une valeur et Hapi enverra le corps et les en-têtes appropriés.

  • Un objet répondra avec du contenu JSON et -Type: application / json
  • Chaîne les valeurs seront Type de contenu: text / html
  • Vous pouvez également renvoyer une promesse ou Stream .

La fonction gestionnaire est souvent faite async pour un flux de contrôle plus propre avec Promises:

 server.route ({
  méthode: 'GET',
  chemin: '/',
  gestionnaire: async () = & gt; {
    laissez html = attendre Promise.resolve (`& lt; h1 & gt; Google & lt; h1 & gt;`)
    html = html.replace ('Google', 'Hapi')

    retour html
  }
})

Ce n'est pas toujours plus propre avec async cependant. Parfois, le renvoi d'une promesse est plus simple:

 handler: () = & gt; {
  return Promise.resolve ("& lt; h1 & gt; Google & lt; h1 & gt;")
    .then (html = & html.replace ('Google', 'Hapi'))
}

Nous verrons de meilleurs exemples de la façon dont async nous aide lorsque nous commençons à interagir avec la base de données.

Le calque de modèle

Comme le populaire Express.js framework, Hapi est un framework minimal qui ne fournit aucune recommandation pour la couche Model ou la persistance. Vous pouvez choisir n'importe quelle base de données et ORM que vous souhaitez, ou aucune – c'est à vous de choisir. Nous allons utiliser SQLite et Sequelize ORM dans ce tutoriel pour fournir une API propre pour interagir avec la base de données.

SQLite est pré-installé sur macOS et la plupart des distributions Linux . Vous pouvez vérifier s'il est installé avec sqlite -v . Sinon, vous pouvez trouver des instructions d'installation sur le site Web SQLite .

Sequelize fonctionne avec de nombreuses bases de données relationnelles populaires comme Postgres ou MySQL, vous devrez donc installer séquencer et l'adaptateur sqlite3 :

 npm install --save sequelize sqlite3

Connectez-vous à notre base de données et écrivez notre première définition de table pour articles :

 // models.js
const path = require ('chemin')
const Sequelize = require ('sequelize')

// configure la connexion à l'hôte db, l'utilisateur, passe - pas nécessaire pour SQLite
const sequelize = new Sequelize (null, null, null, {
  dialecte: 'sqlite',
  storage: path.join ('tmp', 'db.sqlite') // SQLite conserve ses données directement dans le fichier
})

// Ici, nous définissons notre modèle d'article avec un attribut title de type string, et un attribut body de type text. Par défaut, toutes les tables obtiennent également des colonnes pour id, createdAt, updatedAt.
const Article = sequelize.define ('article', {
  titre: Sequelize.STRING,
  corps: Sequelize.TEXT
})

// Créer une table
Article.sync ()

module.exports = {
  Article
}

Testons notre nouveau modèle en l'important et en remplaçant notre gestionnaire d'itinéraire par ce qui suit:

 // server.js
const {Article} = require ('./ models')

server.route ({
  méthode: 'GET',
  chemin: '/',
  gestionnaire: () = & gt; {
    // essayez de commenter ces lignes une à la fois
    return Article.findAll ()
    return Article.create ({title: 'Bienvenue sur mon blog', corps: 'L'endroit le plus heureux de la terre'})
    return Article.findById (1)
    return Article.update ({title: 'Apprendre Hapi', corps: `L'API JSON est une brise.}}, {where: {id: 1}})
    return Article.findAll ()
    return Article.destroy ({where: {id: 1}})
    return Article.findAll ()
  }
})

Si vous êtes familier avec SQL ou d'autres ORM, l'API Sequelize API devrait être explicite, elle est construite avec Promises, donc elle fonctionne très bien avec les gestionnaires async de Hapi aussi. ] Note: en utilisant Article.sync () pour créer les tables ou Article.sync ({force: true}) à déposer et créer sont bien pour les fins de cette démo. Si vous souhaitez l'utiliser en production, vous devriez vérifier sequelize-cli et écrire Migrations pour tout changement de schéma.

Nos actions RESTful

Bâtissons ce qui suit routes:

 GET / articles récupère tous les articles
GET / articles /: id récupère l'article par id
POST / articles créent un article avec les paramètres `{title, body}`
PUT / articles /: id met à jour l'article avec les paramètres `{title, body}`
DELETE / articles /: id supprimer l'article par id

Ajouter un nouveau fichier, routes.js pour séparer la configuration du serveur de la logique de l'application:

 // routes.js
const {Article} = require ('./ models')

exports.configureRoutes = (serveur) = & gt; {
  // server.route accepte un objet ou un tableau
  return server.route ([{
    method: 'GET',
    path: '/articles',
    handler: () => {
      return Article.findAll()
    }
  }, {
    method: 'GET',
    // The curly braces are how we define params (variable path segments in the URL)
    path: '/articles/{id}',
    handler: (request) => {
      return Article.findById(request.params.id)
    }
  }, {
    method: 'POST',
    path: '/articles',
    handler: (request) => {
      const article = Article.build(request.payload.article)

      return article.save()
    }
  }, {
    // method can be an array
    method: ['PUT', 'PATCH'],
    chemin: '/ articles / {id}',
    gestionnaire: async (demande) = & gt; {
      const article = attente Article.findById (request.params.id)
      article.update (request.payload.article)

      return article.save ()
    }
  }, {
    méthode: 'SUPPRIMER',
    chemin: '/ articles / {id}',
    gestionnaire: async (demande) = & gt; {
      const article = attente Article.findById (request.params.id)

      retourne article.destroy ()
    }
  }])
}

Importez et configurez nos routes avant de démarrer le serveur:

 // server.js
const Hapi = require ('hapi')
const {configureRoutes} = require ('./ routes')

serveur const = Hapi.server ({
  hôte: 'localhost',
  port: 3000
})

// Cette fonction nous permettra de l'étendre facilement plus tard
const main = async () = & gt; {
  attendre configureRoutes (serveur)
  attend server.start ()

  retourner le serveur
}

main (). then (server = & gt; {
  console.log ('Serveur fonctionnant sous:', server.info.uri)
}). catch (err = & gt; {
  console.log (err)
  process.exit (1)
})

Tester notre API est aussi simple que HTTPie

HTTPie est un excellent petit client HTTP en ligne de commande qui fonctionne sur tous les systèmes d'exploitation. Suivez les instructions d'installation dans la documentation et essayez de cliquer sur l'API depuis le terminal:

 http GET http: // localhost: 3000 / articles
http POST http: // localhost: 3000 / articles article: = '{"title": "Bienvenue sur mon blog", "body": "Le plus grand endroit sur terre"}'
http POST http: // localhost: 3000 / articles article: = '{"title": "Apprendre Hapi", "body": "API JSON un jeu d'enfant."}'
http GET http: // localhost: 3000 / articles
http GET http: // localhost: 3000 / articles / 2
http PUT http: // localhost: 3000 / articles / 2 article: = '{"title": "Le vrai bonheur, c'est une qualité intérieure"}'
http GET http: // localhost: 3000 / articles / 2
http DELETE http: // localhost: 3000 / articles / 2
http GET http: // localhost: 3000 / articles

D'accord, tout semble bien fonctionner. Essayons un peu plus:

 http GET http: // localhost: 3000 / articles / 12345
http DELETE http: // localhost: 3000 / articles / 12345

Yikes ! Lorsque nous essayons d'aller chercher un article qui n'existe pas, nous obtenons un 200 avec un corps vide et notre gestionnaire de destruction lance une erreur qui se traduit par un 500 ] Cela se produit parce que findById renvoie null par défaut lorsqu'il ne trouve pas d'enregistrement. Nous voulons que notre API réponde avec un 404 dans les deux cas.

Vérifier de manière défensive null Valeurs et renvoyer une erreur

Il y a un paquet appelé boom qui aide à créer des objets de réponse aux erreurs standards: [19659005] npm installer –save boom

Importer et modifier GET / articles /: id route:

 // routes.js
const Boom = require ('boom')

{
  méthode: 'GET',
  chemin: '/ articles / {id}',
  gestionnaire: async (demande) = & gt; {
    const article = attente Article.findById (request.params.id)
    if (article === null) retourne Boom.notFound ()

    retourner l'article
  }
}

Étendre Sequelize.Model pour lancer une erreur

Sequelize.Model est une référence au prototype dont tous nos modèles héritent, donc nous pouvons facilement ajouter une nouvelle méthode find à findById et renvoyer une erreur si elle renvoie null :

 // models.js
const Boom = require ('boom')

Sequelize.Model.find = fonction async (... args) {
  const obj = attendre this.findById (... args)
  if (obj === null) lance Boom.notFound ()

  return obj
}

Nous pouvons ensuite rétablir le gestionnaire à son ancienne gloire et remplacer les occurrences de findById par find :

 {
  méthode: 'GET',
  chemin: '/ articles / {id}',
  gestionnaire: (demande) = & gt; {
    return Article.find (request.params.id)
  }
}
 http GET http: // localhost: 3000 / articles / 12345
http DELETE http: // localhost: 3000 / articles / 12345

Boom . Nous obtenons maintenant une erreur 404 Not Found chaque fois que nous essayons d'extraire quelque chose de la base de données qui n'existe pas. Nous avons remplacé nos contrôles d'erreur personnalisés par une convention facile à comprendre qui maintient notre code propre.

Note: un autre outil populaire pour faire des requêtes aux API REST est Postman . Si vous préférez une interface utilisateur et la possibilité de sauvegarder des requêtes courantes, c'est une excellente option

Paramètres de chemin

Le routage dans Hapi est un peu différent des autres frameworks. La route est sélectionnée sur la spécificité du chemin, donc l'ordre dans lequel vous les définissez n'a pas d'importance.

  • / hello / {name} correspond à / bonjour / bob et passe 'bob' sous le nom param
  • / hello / {nom?} – le ? ] rend le nom optionnel et correspond à la fois / bonjour et / bonjour / bob
  • / bonjour / {nom * 2} – le * dénote plusieurs segments, correspondant / hello / bob / marley en passant 'bob / marley' comme nom param
  • / { args *} correspond à / any / route / imaginable et a la plus faible spécificité.

L'objet de requête

L'objet de requête transmis au gestionnaire d'itinéraire a les propriétés utiles suivantes:

  • request.params chemin params
  • request.query – paramètres de chaîne de requête
  • request.payload – corps de requête pour JSON ou formulaire params
  • request.state – cookies
  • request.headers
  • request.url

Ajout d'un second modèle

Notre deuxième modèle traitera les commentaires sur les articles. Voici le fichier complet:

 // models.js
const path = require ('chemin')
const Sequelize = require ('sequelize')
const Boom = require ('boom')

Sequelize.Model.find = fonction async (... args) {
  const obj = attendre this.findById (... args)
  if (obj === null) lance Boom.notFound ()

  return obj
}

const sequelize = new Sequelize (null, null, null, {
  dialecte: 'sqlite',
  storage: path.join ('tmp', 'db.sqlite')
})

const Article = sequelize.define ('article', {
  titre: Sequelize.STRING,
  corps: Sequelize.TEXT
})

const Comment = sequelize.define ('comment', {
  commentateur: Sequelize.STRING,
  corps: Sequelize.TEXT
})

// Ces associations ajoutent une clé étrangère articleId à notre table de commentaires
// Ils ajoutent des méthodes utiles comme article.getComments () et article.createComment ()
Article.hasMany (Commentaire)
Comment.belongsTo (Article)

// Créer des tables
Article.sync ()
Comment.sync ()

module.exports = {
  Article,
  Commentaire
}

Pour créer et supprimer des commentaires, nous pouvons ajouter des routes imbriquées sous le chemin de l'article:

 // routes.js
const {Article, Commentaire} = require ('./ models')

{
  méthode: 'POST',
  chemin: '/ articles / {id} / commentaires',
  gestionnaire: async (demande) = & gt; {
    const article = attente Article.find (request.params.id)

    return article.createComment (request.payload.comment)
  }
}, {
  méthode: 'SUPPRIMER',
  chemin: '/ articles / {articleId} / commentaires / {id}',
  gestionnaire: async (demande) = & gt; {
    const {id, articleId} = request.params
    // Vous pouvez passer des options à findById en tant que second argument
    const comment = attend Comment.find (id, {where: {articleId}})

    retourne comment.destroy ()
  }
}

Enfin, nous pouvons étendre GET / articles /: id pour retourner à la fois l'article et ses commentaires:

 {
  méthode: 'GET',
  chemin: '/ articles / {id}',
  gestionnaire: async (demande) = & gt; {
    const article = attente Article.find (request.params.id)
    const comments = wait article.getComments ()

    return {... article.get (), commentaires}
  }
}

article voici l'objet Modèle ; article.get () renvoie un objet brut avec les valeurs du modèle, sur lesquelles nous pouvons utiliser l'opérateur spread pour combiner avec nos commentaires. Testons-le:

 http POST http: // localhost: 3000 / articles / 3 / comments commentaire: = '{"commenter": "mb4", "body": "D'accord, ce blog gouverne!" } '
http POST http: // localhost: 3000 / articles / 3 / commentaires commentaire: = '{"commenter": "prince nigérian", "corps": "Vous êtes le bénéficiaire de la fortune de 4.000.000 $ d'un prince nigérian." } '
http GET http: // localhost: 3000 / articles / 3
http DELETE http: // localhost: 3000 / articles / 3 / commentaires / 2
http GET http: // localhost: 3000 / articles / 3

Notre API de blog est presque prête à être livrée en production, nécessitant juste quelques touches de finition

Authentification avec JWT

Les jetons Web JSON sont un mécanisme d'authentification commun aux API. Il y a un plugin hapi-auth-jwt2 pour le configurer, mais il n'a pas encore été mis à jour pour Hapi 17.0, nous aurons donc besoin d'installer une fork pour l'instant:

 npm install - sauvegarder salzhrani / hapi-auth-jwt2 # v-17

Le code ci-dessous enregistre le plugin hapi-auth-jwt2 et met en place une stratégie nommée admin utilisant le schéma jwt . Si un jeton JWT valide est envoyé dans un en-tête, une chaîne de requête ou un cookie, il appellera notre fonction validate pour vérifier que nous sommes heureux d'accorder ces droits d'accès:

 // auth.js
const jwtPlugin = require ('hapi-auth-jwt2'). plugin
// Ce serait dans une variable d'environnement en production
const JWT_KEY = 'NeverShareYourSecret'

var validate = function (informations d'identification) {
  // Exécute toutes les vérifications ici pour confirmer que nous voulons accorder l'accès à ces informations d'identification
  revenir {
    isValid: vrai,
    informations d'identification // request.auth.credentials
  }
}

exports.configureAuth = async (serveur) = & gt; {
  attendez server.register (jwtPlugin)
  server.auth.strategy ('admin', 'jwt', {
    clé: JWT_KEY,
    valider,
    verifyOptions: {algorithmes: [ 'HS256' ]}
  })

  // Par défaut, toutes les routes nécessitent JWT et désactivent les routes publiques
  server.auth.default ('admin')
}

Ensuite, importez et configurez notre stratégie d'authentification avant de démarrer le serveur:

 // server.js
const {configureAuth} = require ('./ auth')

const main = async () = & gt; {
  attendre configureAuth (serveur)
  attendre configureRoutes (serveur)
  attend server.start ()

  retourner le serveur
}

Maintenant, toutes les routes nécessiteront notre stratégie d'authentification admin . Essayez ces trois:

 http GET localhost: 3000 / articles
http GET localhost: 3000 / articles Autorisation: yep
http GET localhost: 3000 / articles Autorisation: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6IkFudGhvbnkgVmFsaWQgVXNlciIsImlhdCI6MTQyNTQ3MzUzNX0.KA68l60mjiC8EXaC2odnjFwdIDxE__iDu5RwLdN1F2A

Le dernier devrait contenir un jeton valide et renvoyer les articles de la base de données. Pour rendre un itinéraire public, il suffit d'ajouter config: {auth: false} à l'objet route. Par exemple:

 {
  méthode: 'GET',
  chemin: '/ articles',
  gestionnaire: (demande) = & gt; {
    return Article.findAll ()
  },
  config: {auth: false}
}

Rendre ces trois routes publiques afin que chacun puisse lire des articles et poster des commentaires:

 GET / articles articles # index
GET / articles /: id articles # show
POST / articles /: id / commentaires commentaires # créer

Génération d'un JWT

Il y a un paquet nommé jsonwebtoken pour signer et vérifier JWT:

 npm install --save jsonwebtoken

Notre route finale prendra un email / mot de passe et générera un JWT. Définissons notre fonction de login dans auth.js pour garder toute la logique d'authentification dans un seul endroit:

 // auth.js
const jwt = require ('jsonwebtoken')
const Boom = require ('boom')

exports.login = (email, mot de passe) = & gt; {
  if (! (email === 'mb4@gmail.com' & amp; & amp; mot de passe === 'ours')) return Boom.notAcceptable ()

  const credentials = {email}
  const token = jwt.sign (informations d'identification, JWT_KEY, {algorithme: 'HS256', expiresIn: '1h'})

  return {token}
}
 // routes.js
const {login} = require ('./ auth')

{
  méthode: 'POST',
  chemin: '/ authentifications',
  gestionnaire: async (demande) = & gt; {
    const {email, mot de passe} = request.payload.login

    retour login (email, mot de passe)
  },
  config: {auth: false}
}
 http POST localhost: 3000 / authentifications login: = '{"email": "mb4@gmail.com", "mot de passe": "ours"}'

Essayez d'utiliser le jeton retourné dans vos requêtes aux routes sécurisées!

Validation avec joi

Vous pouvez valider les paramètres de requête en ajoutant config à l'objet route. Le code ci-dessous garantit que l'article soumis a un corps et titre entre trois et dix caractères. Si une validation échoue, Hapi répondra avec une erreur 400 :

 const Joi = require ('joi')

{
    méthode: 'POST',
    chemin: '/ articles',
    gestionnaire: (demande) = & gt; {
      const article = Article.build (request.payload.article)

      return article.save ()
    },
    config: {
      valider: {
        charge utile: {
          article: {
            titre: Joi.string (). min (3) .max (10),
            corps: Joi.string (). required ()
          }
        }
      }
    }
  }
}

En plus de la charge utile vous pouvez également ajouter des validations aux chemins requête et en-têtes . En savoir plus sur la validation dans les documents .

Qui consomme cette API?

Nous pourrions diffuser une application d'une seule page à partir de / . Nous avons déjà vu – au début du tutoriel – un exemple de comment servir un fichier HTML avec des flux. Cependant, il existe de bien meilleures façons de travailler avec les vues et les mises en page dans Hapi. Voir Servir le contenu statique et Vues et mises en page pour en savoir plus sur la façon de rendre les vues dynamiques:

 {
  méthode: 'GET',
  chemin: '/',
  gestionnaire: () = & gt; {
    return require ('fs'). createReadStream ('index.html')
  },
  config: {auth: false}
}

Si le frontal et l'API sont sur le même domaine, vous n'aurez aucun problème à faire des demandes: client -> hapi-api .

Si vous êtes desservant l'extrémité frontale à partir d'un domaine différent et souhaitant envoyer des requêtes à l'API directement depuis le client, vous devez activer CORS. C'est très facile dans Hapi:

 const server = Hapi.server ({
  hôte: 'localhost',
  port: 3000,
  itinéraires: {
    cors: {
      informations d'identification: true
      // Voir les options sur https://hapijs.com/api/17.0.0#-routeoptionscors
    }
  }
})

Vous pouvez également créer une nouvelle application entre les deux. Si vous suivez cette route, vous n'aurez pas besoin de vous soucier de CORS, car le client ne fera que des demandes à l'application frontale, et il pourra alors faire des demandes à l'API sur le serveur sans aucune restriction entre domaines. : client -> hapi-front-end -> hapi-api .

Que ce front-end soit une autre application Hapi, ou Next, ou Nuxt … Je vais laisser ça à vous de décider!




Source link