5. Express ? Késako ?
Initialisation
Express est un framework qui permet de faciliter la création de serveur HTTP. En effet, nous pourrions créer notre propre serveur web de A à Z en redéfinissant chaque méthode HTTP, chaque route possible, mais il faut l'avouer, Express mâche tellement le travail des développeurs qu'il est devenu un incontournable à connaître pour le milieu du backend Node.js.
Pour commencer, nous allons installer Express avec la commande :
npm install express @types/express
Ensuite, dans notre fichier main (souvent index.js), nous allons ajouter les lignes suivantes :
import express from 'express'
export const app = express()
const port = 3000
app.listen(port, () => {
console.log(`Mon serveur démarre sur le port ${port}`)
})
Remarque: Cette partie de code est pour le moment très similaire à ce que nous faisions précédemment avec le package
http, mais patience, c'est après qu'Express va révéler son intérêt.
Remarque: Nous exportons notre application express pour pouvoir l'utiliser dans d'autres fichiers. Cela se révèlera utile lorsque nous voudrons tester notre application plus tard.
Routes
Pour le moment, Express nous a permis de créer un serveur, ce que le package HTTP pouvait déjà faire. Mais pour créer réellement une API, il va falloir gérer des cas de lecture, écriture, modification, suppression, etc.
C'est là qu'Express prend toute sa valeur, car le framework possède déjà une liste de fonctions permettant de gérer ces cas.
Sur les documentations, vous trouverez souvent les routes définies de cette manière :
GET:/nom-de-la-route. Cela signifie que pour accéder à cette route, il faudra faire une requêteGETsur/nom-de-la-route.
Lecture via GET
Admettons que je travaille sur une API permettant de gérer une liste d'utilisateurs. Mon application front a besoin
d'accéder à cette liste et va donc demander une requête GET à mon API (notre serveur hébergé sur
localhost:3000), et ce dernier devra lui rendre la fameuse liste. Mais je vais aussi parfois avoir besoin d'obtenir
seulement un seul utilisateur. Et dans les deux cas, il s'agit d'une requête GET.
Afin de différencier ces cas, nous allons créer des routes qui sont en fait des URL supplémentaires.
Pour définir une route GET, nous allons utiliser la méthode du même nom d'Express.
import {Request, Response} from 'express'
// Route pour la page d'accueil
app.get('/', (_req: Request, res: Response) => {
res.status(200).send('Bienvenue sur le serveur HTTP')
})
// Route pour obtenir la liste des utilisateurs
app.get('/users', (_req: Request, res: Response) => {
res.status(200).json([
{id: 1, name: 'Alice', email: 'alice@example.com'},
{id: 2, name: 'Bob', email: 'bob@example.com'},
])
})
// Route pour obtenir un utilisateur en particulier
app.get('/users/:id', (req: Request, res: Response) => {
const userId = req.params.id
res.status(200).json({
id: parseInt(userId),
name: 'John Doe',
email: 'john@example.com',
})
})
Note sur les paramètres dynamiques: Le
:iddans/users/:idest un paramètre de route dynamique. Le deux-points (:) indique à Express que cette partie de l'URL est variable. Par exemple,/users/1,/users/42, ou/users/abccorrespondent tous à cette route. La valeur capturée est accessible viareq.params.id.
Notez l'utilisation de
res.json()au lieu deres.send()pour renvoyer des données JSON. Express se charge automatiquement de définir le bonContent-Type(application/json).
Création via POST
Pour créer une ressource, nous allons utiliser la méthode POST. Cette méthode est utilisée pour envoyer des données au
serveur. Par exemple, dans le cas d'un formulaire d'inscription, les données du formulaire seront envoyées au serveur
avec une requête POST.
import {Request, Response} from 'express'
app.post('/users', (req: Request, res: Response) => {
const {name, email} = req.body
res.status(201).json({
message: 'Utilisateur créé',
id: 3,
name,
email,
})
})
Ici, nous utilisons :
- Le code HTTP
201(Created) pour indiquer qu'une ressource a été créée req.bodypour accéder aux données envoyées par le client (grâce au middlewareexpress.json())res.json()pour renvoyer une réponse JSON
Middleware
Les middlewares sont des morceaux de code qui seront exécutés lors de CHAQUE requête. Les middlewares peuvent se révéler utiles lorsque vous avez des opérations communes à réaliser avant ou après chaque requête. Imaginons que je souhaite mettre en place un système d'horodatage des requêtes.
import {Request, Response, NextFunction} from 'express'
app.use((req: Request, _res: Response, next: NextFunction) => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] Requête reçue : ${req.method} ${req.url}`)
next() // Passe à la prochaine fonction middleware ou route
})
Body Parsing
Vous pouvez avoir des soucis lorsque votre requête va essayer de lire votre body. Dans ce cas il faut utiliser un parser qui va formater la chaîne de caractères reçue pour qu'elle soit interprétable par Express. Plus de détails sur le body-parser ici.
Express inclut maintenant le body-parser directement. On ajoute simplement un middleware qui demandera de parser le JSON. Ainsi toutes les requêtes qui arriveront sur notre serveur avec un body JSON seront automatiquement parsées.
import express from 'express'
import {Request, Response} from 'express'
export const app = express()
// Middleware pour parser le JSON (à placer AVANT les routes)
app.use(express.json())
// Maintenant on peut accéder à req.body dans nos routes
app.post('/users', (req: Request, res: Response) => {
const {name, email} = req.body
res.status(201).json({
message: 'Utilisateur créé',
id: 3,
name,
email,
})
})
Important: Le middleware
express.json()doit être placé avant la définition de vos routes pour qu'il puisse parser les requêtes entrantes.
Découpage des routes
Lorsque votre application grandit, il devient rapidement compliqué de gérer toutes les routes dans un seul fichier. Express propose un système de Router qui permet de découper votre application en modules logiques.
Le Router permet de créer des groupes de routes qui peuvent être définis dans des fichiers séparés, puis importés dans le fichier principal. C'est une bonne pratique qui améliore la lisibilité et la maintenabilité de votre code.
Structure modulaire avec Router
Imaginons que nous voulons séparer nos routes utilisateurs dans un fichier dédié. Voici comment procéder :
Fichier user.route.ts (contient toutes les routes liées aux utilisateurs) :
import {Router, Request, Response} from 'express'
// Création d'un routeur
export const userRouter = Router()
// Route pour obtenir tous les utilisateurs
// Accessible via GET /users
userRouter.get('/', (_req: Request, res: Response) => {
res.status(200).json([
{id: 1, name: 'Alice', email: 'alice@example.com'},
{id: 2, name: 'Bob', email: 'bob@example.com'}
])
})
// Route pour obtenir un utilisateur spécifique
// Accessible via GET /users/:id
userRouter.get('/:id', (req: Request, res: Response) => {
const userId = req.params.id
res.status(200).json({
id: parseInt(userId),
name: 'John Doe',
email: 'john@example.com'
})
})
// Route pour créer un utilisateur
// Accessible via POST /users
userRouter.post('/', (req: Request, res: Response) => {
const {name, email} = req.body
res.status(201).json({
message: 'Utilisateur créé',
id: 3,
name,
email
})
})
Fichier index.ts (fichier principal de l'application) :
import express from 'express'
import {userRouter} from './user.route'
export const app = express()
const port = 3000
// Middleware pour parser le JSON
app.use(express.json())
// Route d'accueil
app.get('/', (_req, res) => {
res.status(200).send('Bienvenue sur le serveur HTTP')
})
// Utilisation du router utilisateur
// Toutes les routes définies dans userRouter seront préfixées par /users
app.use('/users', userRouter)
// Démarrage du serveur
app.listen(port, () => {
console.log(`Mon serveur démarre sur le port ${port}`)
})
Avantages de cette approche
- Organisation : Chaque ressource (users, posts, comments, etc.) peut avoir son propre fichier de routes
- Maintenabilité : Il est plus facile de trouver et modifier une route spécifique
- Réutilisabilité : Les routeurs peuvent être réutilisés dans différentes parties de l'application
- Scalabilité : Vous pouvez facilement ajouter de nouvelles fonctionnalités sans surcharger un seul fichier