Express
Express est une librairie simple, mais puissant pour la création d'un serveur avec NodeJS.
Personnellement j'aime beaucoup pour les raisons suivantes :
Elle est non-opinionated, c'est-à-dire, je peux structurer comme je veux mes applications, sans avoir une structure imposée sur moi (comme Symfony etc). Ceci ouvre la possibilité d'erreurs, bien sûr, mais offre plus de flexibilité aussi.
Elle supporte toutes les fonctions d'un serveur :
Serve des fichiers statics
Répond à toutes les requêtes HTTP
La gestion des formulaires
La gestion de multinoeud cluster
La gestion de sockets
...
Elle est entièrement configurable :
On peut ajouter des
middleware
où on veut pour paramétrer entièrement nos end-points.Facilement préciser le format des réponses
Inclure express
Selon la librairie, on devrait parfois installer les définitions Typescript de la libraire. La convention est le préfixe "@types/" avant le nom de la librairie.
De plus en plus de librairies incluent par défaut ses définitions dans la librairie de base, mais pas encore Express.
Le serveur le plus simple
Voici un exemple d'un serveur extrêmement basique :
J'ai créé une ligne dans package.json
qui permet de lancer mon serveur avec npm run server
:
En lançant le serveur, je peux donc ouvrir un navigateur aux liens suivants :
Fondamentaux d'Express
Comment Express fonctionne ?
D'abord, Express écoute des connexions TCP entrantes (en réalité, Express n'est juste une enveloppe pour les fonctionnalités NodeJS qui aide à créer des Sockets, écouter sur des ports, etc).
Quand une nouvelle connexion est ouverte, la requête HTTP est interprétée pour ses différents composants :
La METHODE demandé par HTTP,
get
,put
,post
, etc.Le PATH, ou le chemin indique dans l'URL (relatif au nom de demain ou adresse IP de votre serveur)
Express gère la requête via des callbacks. Avant de lancer le serveur, nous allons dire à Express quelles méthodes HTTP à implémenter sur quels chemins, en fournissant une fonction qui est censé gérer la requête.
Par exemple, le code suivant :
Ici, on précise que, pour une requête http de type GET
, sur le chemin relatif /helo
, il faut exécuter la fonction flèche que j'ai passée comme 2ème paramètre.
Vocabulaire
On construit notre API en fournissant l'ensemble de callbacks sur les méthodes et les chemins.
Chaque combinaison de méthode/chemin est souvent appelé un endpoint, ou une route. Dans l'exemple, nous créons un API avec 4 endpoints. Notez que chaque endpoint est différent grâce à sa méthode, même si tous les 4 partagent le même chemin :
Le deuxième paramètre est un handler, une fonction (ici exprimé en tant que fonction flèche) qui est appelée quand la requête arrive.
Express permet de préciser plusieurs handlers pour le même endpoint :
Dans l'exemple dessus, on précise 3 fonctions flèches à exécuter dans l'ordre pour le endpoint. Les fonctions intermédiaires s'appellent des middleware, car elles s'exécutent au milieu.
À noter : chaque middleware doit obligatoirement appeler la fonction next()
qui est passé comme paramètre au callback. Cette fonction next()
permet à Express de continuer avec la chaîne de middleware. L'idée est qu'on peut, par exemple, lancer un chargement long d'un fichier, et on ne continue pas avec les middlewares suivants, tant que le fichier n'est pas chargé en mémoire.
Appeler la fonction next(error)
avec un paramètre non-nulle signale à Expresse qu'il y a eu une erreur. En effet, le paramètre contient l'information de l'erreur constatée. Expresse quitte la chaîne de middleware, et renvoie une réponse avec un code d'erreur.
Un middleware est intéressant dans la mesure où on peut réutiliser la même logique sur plusieurs endpoints. Par exemple, une fonction qui valide l'autorisation de l'utilisateur :
Ici, on réutilise le même middleware, authorise
, sur plusieurs endpoints.
Comme exercice, essayer les différents endpoints. Modifier l'autorisation afin de retourner une erreur (authenticated = false
), et vérifier le comportement observé dans le navigateur.
Paramètres des handlers
Chaque handler peut prendre plusieurs paramètres :
(facultatif)
error
: le message d'erreur. Si le handler a 4 paramètres, il serait utilisé dans le cas d'erreur (voir en bas).request
: un objet qui représente la requête HTTP, qui donne accès aux en-têtes (header), les paramètres query (dans l'URL), les paramètres extraits de l'URL (params), et les données du corps du message (body)response
: un objet qui représente la réponse qui sera renvoyée, ses en-têtes etc. Cet objet contient des fonctions qui permettent de renvoyer une réponse immédiate, gérer son format, gérer le code HTTP de la réponse, etc.next
: un callback à utiliser si on crée un middleware, c'est-à-dire une fonction qui traite la requête, mais qui ne va pas retourner une réponse. Il faut soit appelernext()
pour signaler à Express de passer dans le prochain handler, soitnext(err)
pour signaler à Express qu'on a rencontré une erreur.
L'objet Request
Request
Quand on gère une requête HTTP, souvent, on modifie le comportement selon les données passées avec la requête. Ces données peuvent arriver via différents chemins. Considérer la requête HTTP suivant :
Cette requête sera gérée par le endpoint suivant :
Ici, on envoie une requête de type POST
au chemin /user/:userId/profile
. Avec cette requête il y a d'autres informations :
Des URL parameters, notamment des morceaux du chemin identifiés par des deux-points. Dans notre cas, c'est le
:userId
. En effet, ce morceau de chemin peut être dynamique, et par exemple, précise un identifiant unique pour notre utilisateur. C'est ainsi qu'on construit des URLs uniques pour chaque ressource de notre API Rest. On accède à ces valeurs via leparams
objet sur leRequest
.Des query parameters, spécifiquement ceux qui se trouvent après le point d'interrogation sur le chemin. Dans l'exemple, on précise
country=france
, et on récupère ces valeurs via lequery
objet sur leRequest
Le body ou le corps du message. Ce sont les données envoyées après la ligne blanche qui suit les en-têtes. Le body peut être en forme de JSON, un formulaire, ou bien juste des données binaires.
Les en-têtes, ou headers. On peut les récupérer via l'objet
requet.headers
Attention ! Les différents types de méthode HTTP supportent (ou pas) la présence d'un corps de message :
Exemple
Le suivant est un exemple d'un mini API qui rassemble tous les éléments présentés ci-dessus.
Prenez note de :
La possibilité d'enchaîner les middleware
L'utilisation de la fonction
json
de la fonctionbody-parser
, utilisé comme middleware global (avecapp.use(...)
). Ce middleware sert à parser automatiquement le corps de messages en tant que JSON.La possibilité d'utiliser les middleware
async
La pattern try/catch avec la fonction
next()
L'utilisation des différentes valeurs dans
request
, commeparams
,query
, etc.
Tester les endpoints
Il est possible d'utiliser des outils comme curl
pour tester votre endpoint :
En général, je préfère utiliser Postman :
Cet outil permet de configurer et tester vos endpoints.
Exercice
Téléchargez Postman et configurez un test pour le endpoint /user/:userId/update
.
Observez les headers
Essayez d'envoyer des paramètres query (dans l'url, après le
?
)Essayez d'envoyer du json dans le body
Essayer à ne pas passer un paramètre
:userId
Exercice
Créez un nouveau projet "from-scratch", en essayant de tester vos compétences en dev-containers, nodejs, typescript, expressjs, etc.
Il faut créer un serveur calculatrice avec les endpoints suivants :
Router
Il est possible de créer aussi des Router
, un objet qui regroupe un set de routes, qu'on peut attacher comme middleware à l'arborescence :
Dans l'exemple, on attache un router qui commence à gérer les opérations CRUD au chemin /user
. On commence à voir qu'il serait possible (avec un peu d'intelligence) de créer un set générique de handler pour gérer les opérations CRUD, et les attacher aux différents points dans la hiérarchie.
Exemple avec Router
Nous aimerions séparer nos différentes routes en fichiers différents.
D'abord, on va créer une sous-branche de notre API qui gère l'utilisateur.
Ensuite, on peut assembler notre API :
À noter :
On peut utiliser le mot clé
export
d'un fichier typescript afin de le rendre accessible de l’extérieur du fichier, et l'inclure dans un autre fichier.Le fichier qui concerne les endpoints de l'User est agnostique à sa position dans l'arborescence. On l'attache avec
app.use()
en construisant notre API.
Dernière mise à jour