Vous plairez t'il d'apprendre à réaliser une API REST en TypeScript, sous NodeJS ? Si oui, je vous propose de le faire en décortiquant un exemple d'API Node/TypeScript basique (création/édition/consultation/suppression d'utilisateurs), que j'ai spécialement réalisé pour vous, pour l'occasion ! Ça me permettra d'ailleurs de vous montrer quelques bonnes pratiques, aussi bien au niveau du typage fort TypeScript, qu'au niveau stockage de données en local (avec SQLite) et cryptage de mots de passe (avec Argon2).
Ainsi, vous verrez les bases d'une API REST, fonctionnant sous NodeJS, écrite en TS, qui vous apprendra (par l'exemple) comment créer une API avec NodeJS/TypeScript. Comme d'habitude, je vous expliquerais comment télécharger ce projet-exemple pour l'installer sur votre ordi, et comment s'en servir ; puis je vous expliquerais le code bloc par bloc, afin que ce soit le plus didactique, simple, et digeste possible ! Du reste, nous verrons également quelques commandes en ligne (avec curl), pour tester toutes les routes de cette api (lecture/GET, écriture/POST, mise à jour/PUT, effacement/DELETE, et test mot de passe). Ça vous dit ? Alors par ici la suite !
Remarque : ce projet utilise SQLite pour le stockage des données en local, afin de simplifier les choses ; ainsi, pas besoin de login/mot de passe pour établir une connexion à une base de données externe (comme MySQL, MongoDB, ou autre). De plus, pour renforcer le côté pédagogique de ce projet, j'ai intégré le package Argon2 pour crypter les mots de passe, afin de vous offrir un exemple plus réaliste, et plus sécurisé. Enfin, que vous soyez un débutant TypeScript ou un simple développeur curieux, je suis sûr que vous trouverez ici des choses intéressantes à apprendre. Alors pas d'excuses pour ne pas lire ce qui suit 😉
Salut ! Bienvenue sur LeCoinTS, où je partage des tutos TypeScript gratuits et sans pub. Envie de me soutenir dans cette aventure, avec un café ? ☕

Présentation du projet d'API REST Node/TS
L’API que nous allons prendre en exemple et voir ensemble ici est ce qu'on appelle une API REST ; c'est à dire une interface de programmation d'application, qui permet, via des requêtes HTTP, d'obtenir (GET), d'enregistrer (POST), de mettre à jour (PUT), et de supprimer (DELETE) des données.
Aussi, pour que cette API REST repose sur quelque chose de concret et d'utile, nous verrons comment gérer des utilisateurs avec. C'est pourquoi cette API inclura des fonctionnalités de type CRUD (acronyme de Create, Read, Update, Delete), pour respectivement créer, voir, mettre à jour, et effacer des utilisateurs. Ces fonctionnalités seront accessibles via des "routes" qui leur sont propres, c'est à dire des URL spécifiques, pour chaque type d'action possible. Et d'ailleurs, en parlant de routes, voici celles de l'API Node/TS que je vous décortique aujourd'hui en exemple, pour que vous puissiez apprendre à travers cet exemple :
- POST /utilisateurs : méthode + route pour créer un nouvel utilisateur (tout en cryptant son mot de passe, au passage)
- GET /utilisateurs : méthode + route pour lister tous les utilisateurs enregistrés en base de données
- GET /utilisateurs/:id : méthode + route pour récupérer les infos d'un utilisateur en particulier
- PUT /utilisateurs/:id : méthode + route pour mettre à jour une ou plusieurs infos d'un utilisateur
- DELETE /utilisateurs/:id : méthode + route pour supprimer un utilisateur
- POST /verifier-motdepasse : méthode + route pour vérifier si le mot de passe envoyé correspond à celui crypté en base de données
Nous verrons ensuite, en fin d'article, comment interroger chacune de ces routes, avec le terminal "cmd" de Windows (à remplacer par votre terminal préféré au besoin, ou Postman pour ceux qui préfèrent !), pour tester le fonctionnement de l'API en réel.
Du reste, vous verrez ici un "bel" exemple de comment bien typer ses variables et fonctions en TypeScript, si vous débutez. Vous verrez également comment créer puis accéder à une base SQLite locale, et comment encrypter des mots de passe, avec la bibliothèque Argon2 (vraiment moderne et reconnue pour sa robustesse, c'est pourquoi je vous la présente aujourd'hui !).
En résumé : ce projet-exemple d'API REST NodeJS + TypeScript (100% fonctionnel) permet simplement de gérer des utilisateurs, en base de données. C'est donc un support idéal pour apprendre à bien coder en TypeScript sous NodeJS, qui je l'espère, saura vous inspirer pour vos propres développements 🙂
Installation et lancement du projet, à partir de GitHub
Avant de nous plonger dans le code, voyons comment installer et exécuter ce projet/exemple sur votre ordi. Alors, tout d'abord, il faut que vous ayez les programmes suivants installés sur votre machine (prérequis indispensables) :
- Avoir NodeJS et npm installés (si besoin, voir ce tuto pour installer NodeJS/npm sur Windows/Mac/Linux)
- Avoir git installé, pour pouvoir cloner le dépôt (contenant les fichiers de ce projet, sur GitHub)
Si vous avez tous ces éléments d'installés sur votre PC, il vous suffira donc ensuite de taper les commandes suivantes pour télécharger et installer cette API :
git clone https://github.com/LeCoinTS/ApiRestTypeScript.git
pour télécharger le projet (faites cela dans le répertoire où vous souhaitez que le répertoire de ce projet soit créé, avec les fichiers GitHub dedans)cd ApiRestTypeScript
une fois le téléchargement fini, pour entrer dans ce répertoirenpm i
pour installer toutes les dépendances du projet (cela peut prendre quelque dizaines de secondes, soyez patient !)- et
npm run start
pour lancer le programme
Si tout se passe bien, vous devriez voir s'inscrire dans votre terminal la phrase suivante : Serveur démarré sur http://localhost:3000
(cela peut prendre quelques secondes là aussi, le temps que le serveur démarre). L’API est à présent en marche, et prête à recevoir des requêtes ! Voici ce que ça donne de mon côté :

Important : cette API = serveur web, en quelque sorte. En effet, le programme que nous allons voir ici permet de créer un serveur web, avec des routes d'accès spécifiques, permettant d'effectuer des opérations spécifiques ; dans notre cas, il s'agira des routes de notre API, permettant de créer/mettre à jour/consulter/effacer un utilisateur. C'est pourquoi, pour la suite de cet article, j'utiliserais aussi bien le terme d'API que le terme de serveur web, car le premier repose sur le second. Dans tous les cas, cela sous entend "l'API", à chaque fois.
Remarque 1 : rien d'autre ne va s'afficher ici (à part d'éventuelles erreurs), car on est "côté serveur". En fait, tout se passera du côté client, dans un autre terminal interrogeant cette API, par exemple.
Remarque 2 : il faudra faire un CTRL+C pour interrompre ce serveur, lorsque vous n'aurez plus besoin d'accéder à cette API.
Analyse détaillée du code TypeScript
Maintenant, passons au cœur du projet, qui repose sur un seul et même fichier (src/index.ts
, sur GitHub). Compte tenu de sa taille, et pour que ce soit plus digeste, je vais le décomposer en plusieurs blocs de code ; ainsi, tout sera plus clair, et plus facile à appréhender. Du reste, je vous conseille d'examiner le code entier de cette API REST, qui tient sur un seul et même fichier, avant de passer au détail (sans quoi, vous risquez manquer de repères).
Imports et initialisation
Pour commencer, voyons comment débute le fichier index.ts (qui contient tout le programme, pour rappel) :
import express, { Request, Response, NextFunction, Express } from "express"
import argon2 from "argon2"
import SQLiteDatabase from "better-sqlite3"
// Initialisation
const app: Express = express()
const bdd: SQLiteDatabase.Database = new SQLiteDatabase("utilisateurs.db")
const PORT: number = 3000
app.use(express.json())
On retrouve tout d'abord tout un tas d'imports, à savoir :
express
, qui nous permettra de créer un serveur web, support de notre API RESTargon2
, qui nous permettra de crypter les mots de passe utilisateurs- et
better-sqlite3
, qui nous permettra de gérer une base SQLite (qui contiendra toutes les infos utilisateurs)
Ensuite arrivent plusieurs initialisations :
app
, qui est une instance d’Express (serveur web très connu, pour NodeJS)bdd
, qui connecte l’API à notre base de données SQLite (un fichierutilisateurs.db
, dans notre cas)PORT
, qui définit le port d’écoute du serveur web (3000, en l'occurrence) ; ainsi, notre API REST sera disponible via l'url de basehttp://localhost:3000/
, sur son ordi- et
app.use(express.json())
, qui permet d'insérer un middleware global (c'est à dire un programme intermédiaire qui s'exécutera à chaque fois que notre serveur/API sera interrogé), pour parser les requêtes JSON (c'est à dire pour rendre accessible viareq.body
chaque paramètre d'appel, comme vous le verrez un peu plus loin dans le code)
Définition de l’interface Utilisateur
Vient ensuite une interface, c'est à dire une structure détaillant tous les types de toutes les propriétés d'un objet donné. Pour nous, il s'agira de caractériser ce qu'est un "utilisateur", c'est à dire, quelles sont les propriétés qui lui sont rattachées (identifiant, nom, mot de passe, et email), et de quels types sont ces propriétés (number ou string, dans notre cas). Voici telle qu'apparaît notre interface utilisateur, dans le code :
interface Utilisateur {
id: number
nomutilisateur: string
motdepasse: string
email: string
}
Dit autrement, cette interface TypeScript définit la structure de base d’un utilisateur (un coquille vide / modèle d'utilisateur, si vous préférez). Et grâce au typage strict de ses propriétés id
, nomutilisateur
, motdepasse
, et email
(en type number ou string), on s’assure que tous les utilisateurs auront bien des id, nom, mot de passe, et email conformes aux types définis ici.
Création de la base de données (SQLite) et de la table "utilisateurs"
Maintenant que toutes les initialisations sont faites, on va entrer dans le dur ! Et cela commence par la création de la base de données (BDD) si inexistante, puis création d'une table utilisateur dedans. Cela se fait en une seules commande, dans notre code :
bdd.exec(`
CREATE TABLE IF NOT EXISTS utilisateurs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nomutilisateur TEXT UNIQUE NOT NULL,
motdepasse TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)
Avec ces quelques lignes, on demande à SQLite de créer une table nommée "utilisateurs", dans la base de données qu'il pointe (le nom de fichier de cette BDD ayant été défini au moment de l'initialisation, comme vu un peu plus haut). Et si jamais le fichier de notre base de données n'existe pas, alors SQLite va en créer un vierge à ce moment là, afin de pouvoir créer la table souhaitée dedans.
Vous remarquerez que la requête SQL contenue dans la commande bdd.exec()
reprend les paramètres de notre interface, à savoir l'identifiant, le nom, le mot de passe, et l'email d'un "utilisateur type", tout en précisant les types correspondants (à savoir : INTERGER pour le type number, et TEXT pour les types string).
À noter que :
id
sera la clef primaire (identifiant unique) de chaque utilisateur en base (cet identifiant sera automatiquement généré par SQLite, de façon incrémentale)nomutilisateur
sera un texte "unique" (sans doublon permis en base, donc), et non "null" (le nom devra donc au minimum comporter un caractère)motdepasse
contiendra la chaîne de caractère issue du cryptage du mot de passe utilisateur- et
email
sera un texte "unique" (aucun doublon en base donc, là aussi), et non "null" (l'email devra donc comporter au moins un caractère)
Middleware de validation
Arrive ensuite un "petit outil fait maison", pour ainsi dire, écrit sous la forme d'un middleware (c'est à dire, un bout de code intermédiaire qui s'exécutera lorsque souhaité, à chaque nouvel appel de l'API). Voici le code TS de ce middleware, qui sera greffé à une route de notre serveur web express :
const validateUserInput = (req: Request, res: Response, next: NextFunction): void => {
const { nomutilisateur, motdepasse, email } = req.body as Partial<Utilisateur>
if (!nomutilisateur || !motdepasse || !email) {
res.status(400).json({ erreur: "Tous les champs sont requis" })
return
}
next()
}
Pour faire simple, ce middleware vérifie que tous les champs obligatoires (nomutilisateur, motdepasse, email) sont bien présents, dans la requête d'appel. Cela nous servira dans la route de type POST/création utilisateur, afin qu'on ait tous les éléments requis/obligatoires, pour enregistrer un nouvel utilisateur en base de données. En clair, dans ce cas, notre middleware agira de la manière suivante :
- s'il manque un des paramètres (nom utilisateur, mot de passe, ou email), alors une erreur 400 est renvoyée (pour dire que la requête n'est pas valide)
- si tous les paramètres sont présents, alors la requête quitte ce middleware et poursuit son chemin, grâce à la fonction/callback
next()
Route "POST /utilisateurs" (pour Créer un utilisateur)
À présent, nous allons examiner chaque route de notre API REST exemple. Et pour commencer, voyons le code permettant de créer un nouvel utilisateur (méthode POST) :
app.post("/utilisateurs", validateUserInput, async (req: Request, res: Response): Promise<void> => {
try {
const { nomutilisateur, motdepasse, email } = req.body as Omit<Utilisateur, "id">
const motdepasse_hashed: string = await argon2.hash(motdepasse)
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare(
"INSERT INTO utilisateurs (nomutilisateur, motdepasse, email) VALUES (?, ?, ?)"
)
const result: SQLiteDatabase.RunResult = requete_preparee.run(nomutilisateur, motdepasse_hashed, email)
const nouvelUtilisateur: Omit<Utilisateur, "motdepasse"> = {
id: Number(result.lastInsertRowid),
nomutilisateur,
email,
}
res.status(201).json(nouvelUtilisateur)
} catch {
res.status(400).json({ erreur: "Erreur lors de la création de l'utilisateur" })
}
})
Si on analyse uniquement la 1ère ligne de ce bloc de code, on s'aperçoit que ce bout de code ne sera exécuté que lorsque notre serveur (API REST) recevra une demande de type "POST" (d'où le app.post
) via la route /utilisateurs
. Et si on regarde de plus près, on voit que cette requête devra préalablement passer au travers du middleware validateUserInput
, vu au paragraphe précédent ; cela permet de vérifier un certain nombre de choses préalables (que les nom/mdp/email soient bien transmis, en l'occurrence), avant d'exécuter notre code à proprement parler (si quelque chose est manquant, alors on quitte cette route en renvoyant un message d'erreur, comme défini dans le middleware "validateUserInput").
Ensuite, notre code est encapsulé dans un try/catch, c'est à dire une structure permettant d'intercepter d'éventuelles erreurs, afin de pouvoir les traiter sans crash programme imprévu. Ici, dans les grandes lignes : si tout se passe bien notre code renverra un statut HTTP 201 (signifiant "bien créé") avec un retour sur les infos enregistrées (infos de notre nouvel utilisateur donc, hors mot de passe bien entendu, pour des raisons de sécurité) ; et s'il y a une erreur, notre code renverra un statut HTTP 400 (signifiant "requête invalide"), accompagné du message d'erreur approprié.
Si on examine le code d'encore plus près, entre les { }
de la fonction try, alors on s'aperçoit que :
- on récupère les données accompagnant la requête POST, à savoir toutes les propriétés que l'on retrouve dans notre interface
Utilisateur
, à l'exception de l'ID qui lui, sera communiqué par la base de données, seulement après création de l'utilisateur (d'où le typage TypeScriptOmit<Utilisateur, "id"
, pour avoir un type "Utilisateur" sans l'id). À noter que toutes ces propriétés (paramètres de la requête POST) sont disponibles dans l'objetreq.body
(donc le corps de la requête POST), sous la formereq.body.nomutilisateur
,req.body.motdepasse
, etreq.body.email
. Aussi, histoire de simplifier les choses, on va faire de la déstructuration, en créant directement des variablesnomutilisateur
,motdepasse
, etemail
à partir de ces mêmes variables comprises dansreq.body
; ça nous fait gagner du temps, même s'il faut un peu de temps pour s'habituer à ce "nouveau" style d'écriture, si vous débutez en TS 🙂 - ensuite on hache (crypte/encode) le mot de passe, grâce à la bibliothèque Argon2, pour générer une chaîne de caractère
motdepasse_hashed
à partir demotdepasse
envoyé avec le nom et l'email - puis on prépare une requête SQL, ou plutôt un modèle d'insertion/création de nouvel utilisateur, avec des
?
à la place des valeurs à mettre ; ce qui nous donne un objet nommérequete_preparee
- vient ensuite l'exécution de la requête SQL à proprement dit, avec la commande
.run
(suivi des variables qui viendront se mettre à la place de chaque?
présents dans la requête préparée, fournie ici) - une fois la requête SQL exécutée, on récupère la réponse retournée par SQLite, dans la variables
result
- enfin, on créé une variable
nouvelUtilisateur
, qui contiendra les infos à retourner, concernant le nouvel utilisateur créé ; mais sans le mot de passe, bien évidemment, pour des raisons de sécurité (d'où le type TypeScriptOmit<Utilisateur, "motdepasse">
, sur la variablenouvelUtilisateur
). Au passage, l'ID du nouvel utilisateur sera récupéré via la propriétélastInsertRowid
de l'objetresult
, puisque SQLite nous le met gentiment à disposition 😉
Voilà ! J'espère n'avoir rien oublié ici ! En tous cas, il est important de parfaitement bien comprendre à quoi sert chaque ligne, car ce "motif de code" sera plus ou moins le même pour chaque route (en plus simple ou plus complexe, mais "toujours" dans le même esprit). Qui plus est, cela m'évitera d'avoir à tout redétailler à chaque route ensuite !
Remarque technique : Pourquoi utiliser des requêtes préparée, dans le code ? Pour faire court, c'est pour éviter les injections SQL. En fait, les injections SQL sont un type d'attaque où un pirate insère du code malveillant dans une requête, afin de pirater, manipuler, ou détruire une base de données. Avec une requête préparée, les données sont traitées comme des valeurs brutes (pas comme du code), ce qui bloque ce type d'attaque. Mieux vaut s'en protéger !
Pour aller plus loin : dans ce projet, vous allez retrouver des typages avancés, comme Omit ou Partial. Si vous souhaitez en apprendre plus là dessus, n'hésitez pas à jeter un coup d'oeil sur l'article que j'avais fait sur les types utilitaires (tels que Partial, Pick, Omit, Record, et ReturnType).
Route "GET /utilisateurs/:id" (pour Récupérer un utilisateur spécifique)
Voyons à présent une nouvelle route de notre API REST, permettant, cette fois-ci, de récupérer toutes les infos d'un utilisateur, à partir de son identifiant (id). Et cela se fait avec le bloc de code suivant (méthode GET) :
app.get("/utilisateurs/:id", (req: Request, res: Response): void => {
try {
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare(
"SELECT id, nomutilisateur, email FROM utilisateurs WHERE id = ?"
)
const user: Omit<Utilisateur, "motdepasse"> | undefined = requete_preparee.get(req.params.id) as Omit<Utilisateur, "motdepasse"> | undefined
if (!user) {
res.status(404).json({ erreur: "Utilisateur non trouvé" })
return
}
res.status(200).json(user)
} catch {
res.status(500).json({ erreur: "Erreur lors de la récupération de l'utilisateur" })
}
})
Là aussi, si on analyse la 1ère ligne uniquement, on s'aperçoit que ce bloc de code ne sera exécuté que lorsque notre serveur (API REST) recevra une demande GET (d'où le app.get
) via la route /utilisateurs
, et si et seulement si cette route est suivie d'un identifiant (une valeur numérique, dans le cas de notre API). À noter ici qu'il n'y a pas de middleware appelé, contrairement à la route précédente, car nous n'avons pas de contrôle spécifique à faire (du fait que, en dehors de l'id, cette route ne requiert pas d'arguments, au moment de l'appel).
Par contre, à l'image de la route précédente, le code est par habitude encadré par une structure try/catch, permettant d'intercepter et traiter les erreurs en conséquence (sans provoquer de crash programme, sauf si souhaité avec un "throw new error", mais là, ce n'est pas le cas). Dans notre cas :
- s'il y a une erreur lors de l'exécution de ce code, celui-ci renverra un statut HTTP 500 (signifiant "erreur interne, côté serveur"), accompagné d'un message d'erreur approprié
- si tout se passe bien et si l'utilisateur a bien été trouvé en base de données, alors notre code renverra un statut HTTP 200 (signifiant "succès"), accompagné des infos de l'utilisateur ciblé, telles que lues en base (sans le mot de passe, bien évidemment, pour des raisons de sécurité, comme toujours !)
- si tout se passe bien mais que l'utilisateur n'a pas été trouvé en BDD, alors notre code renverra un statut HTTP 404 (signifiant "non trouvé"), accompagné du message d'erreur qui va bien
Du reste, à l'image de la route précédente, on retrouve une requête SQL préparée (nommée requete_preparee
). Celle-ci sera ensuite exécutée via la méthode .get
, avec l'ID de l'utilisateur recherché passé en argument (cela permettra à SQLite de remplacer le ?
de la requête précédemment préparée par l'id
de l'utilisateur qu'on recherche). Et si la réponse est positive (un utilisateur trouvé), alors on retourne les infos le concernant (mot de passe exclus, comme à l'accoutumée !) ; et si la réponse est négative (aucun utilisateur trouvé en base, avec l'identifiant communiqué), alors on retourne un message d'erreur, pour clairement spécifier cela.
Route "GET /utilisateurs" (pour Lister tous les utilisateurs)
Cette route est faite pour lister tous les utilisateurs connus/enregistrés dans notre base de données. Contrairement à la route précédente, qui ne renvoyait qu'un seul utilisateur correspondant à l'identifiant spécifié, cette route va retourner l'ensemble des utilisateurs (donc toujours en utilisant une méthode GET, mais sans passer d'id en argument). Du coup, le code de cette route sera similaire à la route précédente, mais en plus simple ! Pour preuve :
app.get("/utilisateurs", (req: Request, res: Response): void => {
try {
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare("SELECT id, nomutilisateur, email FROM utilisateurs")
const utilisateurs: Omit<Utilisateur, "motdepasse">[] = requete_preparee.all() as Omit<Utilisateur, "motdepasse">[]
res.status(200).json(utilisateurs)
} catch {
res.status(500).json({ erreur: "Erreur lors de la récupération des utilisateurs" })
}
})
Grosso modo, ce code permet de retourner le code HTTP 500 si une erreur est rencontrée lors de l'exécution, ou un code HTTP 200 accompagné d'un tableau d'utilisateurs si tout est bon, avec toutes les infos de chaque utilisateur enregistré en base (hors mot de passe là encore, toujours pour des raisons de sécurité).
À noter qu'on utilise ici la méthode .all
de SQLite, afin de récupérer un tableau d'éléments (un ensemble d'utilisateurs, dans notre cas).
Route "PUT /utilisateurs/:id" (pour Mettre à jour un utilisateur)
Bon, maintenant que vous avez compris comment fonctionnent les routes, voyons-en une qui sera certainement la plus complexe de toutes ! Donc si vous débutez, accrochez vous (bien que ce ne soit pas si compliqué que ça, si on analyse le code ligne par ligne !). En fait, ici, cette route permettra de mettre à jour les données d'un d'utilisateur donné (en spécifiant son id) via la méthode PUT, en passant par la route /utilisateur
. Pour ce que ce soit plus parlant, voyons sans plus attendre le code de cette route là :
app.put("/utilisateurs/:id", async (req: Request, res: Response): Promise<void> => {
try {
const { nomutilisateur, motdepasse, email } = req.body as Partial<Omit<Utilisateur, "id">>
if (!nomutilisateur && !motdepasse && !email) {
res.status(400).json({ erreur: "Au moins un champ doit être fourni (nomutilisateur, motdepasse, ou email)" })
return
}
const updates: Partial<Omit<Utilisateur, "id">> = {
...(nomutilisateur && { nomutilisateur: nomutilisateur }),
...(motdepasse && { motdepasse: await argon2.hash(motdepasse) }),
...(email && { email: email }),
}
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare(`
UPDATE utilisateurs
SET ${Object.keys(updates)
.map((key) => `${key} = ?`)
.join(", ")}
WHERE id = ?
`)
const result: SQLiteDatabase.RunResult = requete_preparee.run(...Object.values(updates), req.params.id)
if (result.changes === 0) {
res.status(404).json({ erreur: "Utilisateur non trouvé" })
return
}
res.status(200).json({ message: "Utilisateur mis à jour avec succès" })
} catch {
res.status(400).json({ erreur: "Erreur lors de la mise à jour de l'utilisateur" })
}
})
Cela vous paraît compliqué ? Rassurez-vous, car ça ne l'est pas vraiment, lorsqu'on analyse ce genre de code morceau par morceau 🙂
Alors, pour commencer, on a l'instruction app.put
; cela permet de créer une route pour les requêtes de type PUT, dans notre API REST. Ici, pas de middleware appelé, mais on fait quelque chose d'équivalent, en testant des valeurs en début de code (avec le if (!nomutilisateur && !motdepasse && !email) { ... }
).
Cette route renvoie :
- erreur 400 si une erreur s'est produite lors de l'exécution du code (le fameux "catch" du try/catch mis en œuvre ici)
- erreur 400 si aucun paramètre n'a été passé en argument (en sachant qu'il en faut au moins 1 sur les 3 possibles, que sont nomutilisateur, motdepasse, et/ou email ; d'où le type TypeScript
Partial
, pour rendre toutes les propriétés de l'interfaceUtilisateur
optionnelles) - erreur 404 si l'utilisateur visé n'a pas été trouvé en BDD (eh oui, ça peut arriver !)
- et code 200 si l'utilisateur ciblé a pu être mis à jour (avec un petit message accompagnant, du style : "Utilisateur mis à jour avec succès")
Là où ça commence à devenir compliqué, peut-être pour vous, c'est en voyant la variable updates
, et les ...
présents dedans. En fait, ces 3 petits points permettent d'ajouter des propriétés à un élément donné ; dans notre cas, on ajoutera le nom utilisateur, et/ou le mot de passe, et/ou l'email, selon s'ils ont été transmis en même temps que la méthode PUT ou pas. Plus précisément, on insèrera un couple clé/valeur, à l'image de l'interface Utilisateur
(vue tout en haut). Avec un cryptage du mot de passe au passage, si un nouveau mot de passe a été transmis lors de l'appel de cette route.
Arrive ensuite la requête préparée, un poil plus complexe que les précédentes, car on va insérer ou non des éléments dans la requête SQL, selon les paramètres récupérés dans la variable "updates". Cela est permis grâce à la méthode .map
, qui pour chaque élément, renvoie ici une chaîne de caractère du type "élément = ?" (pour préparer notre requête) ; en duo avec la méthode .join
, qui permet de concaténer (mettre bout en bout) ces éléments, en les séparant par des virgules, au besoin.
Enfin, la commande .run
de SQLite permet d'exécuter cette requête préparée, tout en remplaçant le ou les ?
présents dedans par le ou les paramètres correspondants ; et tout ça, accompagné de l'ID de l'utilisateur visé. À noter que là aussi au utilise des ...
, mais ceux-là représentent ce qu'on appelle un "spread operator" dans ce cas, car cela permet de redistribuer toutes les valeurs d'un objet dans un autre objet (en sachant que Object.values
permet de créer un tableau avec uniquement les valeurs des propriétés présentes dans l'objet "updates").
Il nous reste plus qu'à renvoyer un message disant que l'utilisateur a bien été mis à jour ou non, selon si la propriété changes
retournée par notre commande .run
retourne une valeur égale à 0 (aucune ligne affectée, donc utilisateur non trouvé) ou non (au moins une ligne affectée, et une seule en fait du fait que l'ID est unique, donc tout s'est bien passé dans ce cas).
Conseil : si vous avez du mal à comprendre cette route, ajoutez simplement des console.log tout au long du cheminement du code, et exécutez là (comme nous verrons plus bas) ; ainsi, vous verrez très clairement ce que contiennent tous les objets et toutes les variables de cette route ! Faites le, vous verrez à quel point tout devient clair après 🙂
Vous aimez cet article ? LeCoinTS reste gratuit et sans pub grâce à vos dons.
Motivez-moi à en faire plus ! ☕

Route "DELETE /utilisateurs/:id" (pour Supprimer un utilisateur)
Allez, on revient sur quelque chose de plus simple, à présent ! Ici, nous allons voir le code de la route qui permet de supprimer un utilisateur, via la méthode DELETE :
app.delete("/utilisateurs/:id", (req: Request, res: Response): void => {
try {
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare("DELETE FROM utilisateurs WHERE id = ?")
const result: SQLiteDatabase.RunResult = requete_preparee.run(req.params.id)
if (result.changes === 0) {
res.status(404).json({ erreur: "Utilisateur non trouvé" })
return
}
res.status(200).json({ message: "Utilisateur supprimé avec succès" })
} catch {
res.status(500).json({ erreur: "Erreur lors de la suppression de l'utilisateur" })
}
})
Ici, on prépare simplement une requête SQL de type DELETE, avec un ?
au niveau de l'id
(pour rappel, pour plus de sécurité, on prépare les requêtes sans mettre des valeurs dedans, afin de pouvoir les isoler et les mettre de manière "brute" ensuite). Cette requête préparée sera pas la suite exécutée via la méthode .run
de SQLite, avec l'id qu'on a extrait de la route appelée, à savoir : /utilisateurs/:id
.
Une fois la requête exécutée par SQLite, on obtient en retour un objet, qui contient une propriété nommée changes
. Ici :
- si "changes" est à 0, cela signifie qu'aucun utilisateur n'a été effacé (et que donc, l'ID renseigné ne correspondait à aucun utilisateur connu, en base de données)
- si "changes" est différent de 0, cela signifie qu'au moins 1 utilisateur a été supprimé (et on sait qu'il ne peut y avoir qu'un seul utilisateur supprimé à la fois, à partir de son id, car l'id a été défini comme unique, au niveau de la BDD)
Et plus globalement, comme d'habitude, cette route renvoie :
- le code 500 s'il y a eu une erreur pendant l'exécution du code
- le code 404 si l'utilisateur à effacer n'a pas pu être trouvé en base
- le code 200 si l'effacement de l'utilisateur s'est bien déroulé
Route "POST /verifier-motdepasse" (pour Vérifier un mot de passe)
J'en ai profité pour vous rajouter une dernière route, qui n'a rien à voir avec ce qui précède (qui étaient des routes de type CRUD, pour CREATE, READ, UPDATE, et DELETE, via les méthodes post, get, put, et delete). Ici, il s'agit tout simplement d'une route qui permettra de vérifier le mot de passe d'un utilisateur donné, via la méthode POST (car il faut envoyer le mot de passe à tester pour comparaison avec celui stocké de manière cryptée en BDD, d'où la méthode "post"). Voici ce que ça donne, au niveau du code :
app.post("/verifier-motdepasse", async (req: Request, res: Response): Promise<void> => {
try {
const { nomutilisateur, motdepasse } = req.body
// Vérification des champs requis
if (!nomutilisateur || !motdepasse) {
res.status(400).json({ erreur: "Nom d'utilisateur et mot de passe requis" })
return
}
// Récupérer l'utilisateur depuis la base de données
const requete_preparee: SQLiteDatabase.Statement<string[], unknown> = bdd.prepare("SELECT motdepasse FROM utilisateurs WHERE nomutilisateur = ?")
const utilisateur: { motdepasse: string } | undefined = requete_preparee.get(nomutilisateur) as { motdepasse: string } | undefined
if (!utilisateur) {
res.status(404).json({ erreur: "Utilisateur non trouvé" })
return
}
// Vérifier si le mot de passe correspond
const motdepasseValide: boolean = await argon2.verify(utilisateur.motdepasse, motdepasse)
if (motdepasseValide) {
res.status(200).json({ message: "Mot de passe correct !" })
} else {
res.status(401).json({ erreur: "Mot de passe incorrect, désolé" })
}
} catch {
res.status(500).json({ erreur: "Erreur lors de la vérification du mot de passe" })
}
})
Comme toujours, ce bloc de code typescript (définissant une route) renvoie différents codes HTTP (200, 400, 401, 404, ou 500) selon les situations :
- si le mot de passe envoyé correspond bien à celui crypté en base de données, alors cette route renvoie un statut 200 ("succès")
- si le mot de passe envoyé ne correspond pas à celui crypté en base de données, alors cette route renvoie un statut 401 ("accès non autorisé")
- si l'utilisateur ciblé avec son "nomutilisateur" n'existe pas en base, alors cette route renvoie un statut 404 ("non trouvé")
- si le mot de passe à tester n'a pas été transmis en même temps que cette requête POST, alors cette route renvoie un statut 400 ("requête incorrecte")
- et si une erreur survient pendant l'exécution de ce code, alors cette route renvoie un statut 500 ("erreur interne")
Remarque : ces codes/statuts HTTP (pour cette route, et toutes celles qui précèdent) sont ici donnés à titre d'exemple ; à noter qu'ils pourraient fort bien avoir d'autres numéros, selon le contexte ou vos préférences.
Du reste, le code de cette route ne fait que récupérer le mot de passe crypté en BDD, pour un "nomutilisateur" donné (celui "posté" lors de l'appel de la requête POST), pour le comparer ensuite à celui également transmis lors de l'appel de cette route. À noter que cette comparaison se fait ici à l'aide de la méthode verify
de la bibliothèque argon2
. Si les mots de passes correspondent, alors cette route renvoie un message "Mot de passe correct !" ; dans le cas contraire, celle-ci renvoie le message "Mot de passe incorrect".
Démarrage du serveur
Tout dernier bloc de code, qui n'est pas une route, cette fois-ci : le bloc de code permettant de démarrer le serveur web (notre API REST, donc), à proprement parler. Le voici :
app.listen(PORT, () => {
console.log(`Serveur démarré sur http://localhost:${PORT}`)
})
Bon là c'est extrêmement simple : l'app (serveur express pour NodeJS) essaye de démarrer un serveur web (listen
) sur le port d'écoute qu'on lui a indiqué dans la variable PORT
(cf. paramètres d'initialisation tout en haut : on avait choisi le port 3000). Si le démarrage du webserver se passe bien, alors on envoie un message en console disant "Serveur démarré sur …".
Et comme évoqué tout en haut, notre API REST, qui repose sur ce serveur web, ne s'arrêtera que si on effectue un CTRL+C au clavier (pour interrompre le programme).
Test de l’API REST avec "curl"
Maintenant que nous avons une API REST NodeJS/TypeScript, il va falloir la tester, pour voir si tout fonctionne bien ! Pour ce faire, nous allons utiliser des commandes en ligne (dans un terminal quelconque). Bien sûr, pour la suite, rien ne vous empêchera de créer une interface web plus simple/plus parlante/plus friendly pour faire ces mêmes tests (via une app React + TypeScript, par exemple !). Mais pour aujourd'hui, restons sur des choses simples, en utilisant les lignes de commande !
Pour interroger notre API REST, nous allons utiliser "curl" (qui est un utilitaire permettant notamment d'effectuer des requêtes HTTP, à partir des commandes en ligne / via un terminal). Pour ma part, j'utilise le terminal "cmd" de Windows, pour faire ces tests et vous montrer les résultats à l'écran.
Important : il faudra utiliser 2 terminaux ici (deux instances différentes, j'entends) :
- un terminal pour faire tourner le serveur NodeJS de l'API REST (pour rappel, le serveur se lance avec la commande npm run start
), car l'API REST Node/TypeScript devra être lancée pour pouvoir être interrogée
- un terminal pour interagir avec l'API REST (via des commandes utilisant curl
)
Pour ma part, j'ai lancé le serveur web depuis le terminal de VScode, via la commande "npm run start", et j'ai ouvert un terminal "cmd" sous Windows, pour exécuter les commandes "curl" ; bien évidemment, vous êtes libre de faire comme vous préférez 😉
Remarque : de mon côté, le serveur web de l'API tourne en local, sur le port 3000. C'est pourquoi vous verrez du http://localhost:3000/
dans les commandes CURL ci-dessous.
Mais juste avant de démarrer, voici un tableau résumant toutes les routes qu'on peut tester avec :
Action | Méthode | URL | Body JSON (paramètres) |
---|---|---|---|
Lister tous les utilisateurs | GET | /utilisateurs/:id | - |
Créer un utilisateur | POST | /utilisateurs | {"nomutilisateur": "...", "motdepasse": "...", "email": "..."} |
Vérifier mot de passe | POST | /verifier-motdepasse | {"nomutilisateur": "...", "motdepasse": "..."} |
Récupérer un utilisateur | GET | /utilisateurs/:id | - |
Mettre à jour un utilisateur | PUT | /utilisateurs/:id | {"nomutilisateur": "...", "motdepasse": "...", "email": "..."} Remarque : 1, 2, ou 3 de ces paramètres |
Supprimer un utilisateur | DELETE | /utilisateurs/:id | - |
Test route 1/6 : Lister tous les utilisateurs (GET /utilisateurs)
Alors, pour commencer, nous allons interroger l'API REST, pour qu'elle nous retourne toutes les infos de tous les utilisateurs enregistrés en base de données. Cela se fait avec la commande suivante :
curl -X GET http://localhost:3000/utilisateurs
Si c'est la première fois que vous interrogez l'API (comme c'est mon cas), la base de données sera forcément vide (donc aucun utilisateur enregistré dedans, pour l'instant). Du coup, en l'absence de données, vous aurez simplement un tableau vide retourné par l'API, comme visible ci-dessous :

Sinon, si vous aviez déjà créé des utilisateurs en base, alors ils apparaîtraient ici (comme nous le verrons plus tard).
Test route 2/6 : Créer un utilisateur (POST /utilisateurs)
Ensuite, nous allons créer un nouvel utilisateur, en demandant à notre API de le faire. Pour cela, nous allons saisir la commande curl
suivante, dans le terminal :
curl -X POST http://localhost:3000/utilisateurs -H "Content-Type: application/json" -d "{\"nomutilisateur\":\"clara\",\"motdepasse\":\"secret123\",\"email\":\"clara@example.com\"}"
Ici, nous ajoutons (à la commande curl) les paramètre suivants :
-H "Content-Type: application/json"
pour dire qu'on va lui envoyer/adjoindre des données au format JSON-d "{\"nomutilisateur\":\"clara\",\"motdepasse\":\"secret123\",\"email\":\"clara@example.com\"}"
qui sont les données envoyées à l'API
Ce n'est peut-être pas clair avec les commandes en lignes, car il y a ici des échappements de guillemets doubles (les \"), et tout est écrit sur une seule ligne. Mais peut-être cela vous semblerait plus clair, si je vous représentais ces mêmes infos au format "conventionnel" JSON :
{
"nomutilisateur": "clara",
"motdepasse": "secret123",
"email": "clara@example.com"
}
Là à mon avis, c'est plus parlant ! Mais bon, pour tester notre API, nous le faisons ici avec les lignes de commande, qui s'écrivent … sur une seule ligne !
Du reste, avec cette commande "curl", on demande à notre API REST de créer un nouvel utilisateur, dont le nom sera "clara", le mot de passe sera "secret123", et l'email "clara@example.com". Et si on exécute cette commande CURL, voici ce qu'on obtient en retour :
{"id":1,"nomutilisateur":"clara","email":"clara@example.com"}
Cela signifie que notre API NodeJS/TypeScript a bien créé un nouvel utilisateur, dont l'identifiant est "1", le nom utilisateur est "clara", et dont l'email est "clara@example.com".
Remarque : comme vu plus haut, dans le code des routes de l'API, le mot de passe est volontairement exclus des retours, pour des raisons de sécurité. Il n'est donc affiché nulle part, dans ce que retourne l'API.
Test route 3/6 : Vérification du mot de passe (POST /verifier-motdepasse)
À présent, nous allons vérifier si le mot de passe est bien encrypté en base de données. Pour ce faire, nous allons exécuter la commande cURL suivante :
curl -X POST "http://localhost:3000/verifier-motdepasse" -H "Content-Type: application/json" -d "{\"nomutilisateur\": \"clara\", \"motdepasse\": \"secret123\"}"
Dans cette instruction, j'ai volontairement mis le même mot de passe qu'au moment de la création, à savoir "secret123". Du coup, notre API Node/TS devrait nous retourner une réponse positive. Et c'est d'ailleurs le cas, puisqu'elle nous retourne :
{"message":"Mot de passe correct !"}
À noter que si nous avions envoyé un mauvais mot de passe pour tester la correspondance, en écrivant par exemple la commande curl suivante : curl -X POST "http://localhost:3000/verifier-motdepasse" -H "Content-Type: application/json" -d "{\"nomutilisateur\": \"clara\", \"motdepasse\": \"mauvaispass\"}"
, alors la réponse aurait été négative. Pour preuve, voici un aperçu écran de ces 2 tests, sous terminal "cmd" :

Comme vous pouvez le constater, si le mot de passe envoyé ne correspond pas à celui stocké/crypté en BDD, alors l'API REST retourne le message : {"erreur":"Mot de passe incorrect, désolé"}
.
Test route 4/6 : Récupérer les infos d'un utilisateur unique (GET /utilisateurs/:id)
Pour rappel, un peu plus haut, nous avions créé un utilisateur, et l'API nous avait retourné l'identifiant qui lui avait été attribué ("1", dans notre cas). On va donc pouvoir vérifier la route permettant de récupérer les infos d'un utilisateur donné, d'après son id, avec la méthode GET + id. Voici la commande CURL permettant de faire cela :
curl -X GET http://localhost:3000/utilisateurs/1
On retrouve l'appel de "curl" en mode "GET", visant l'url de notre API "http://localhost:3000", en utilisant la route "/utilisateurs/1" (1 étant ici, pour rappel, l'ID de l'utilisateur que nous avions précédemment créé).
Et voici ce que notre API NodeJS/TypeScript nous retourne :
{"id":1,"nomutilisateur":"clara","email":"clara@example.com"}
L'API nous retourne bien l'utilisateur qu'on avait créé précédemment, tout en masquant le mot de passe enregistré en base (pour raison de sécurité, comme toujours !).
Remarque : si nous avions saisi un ID utilisateur qui n'existait pas, du style curl -X GET http://localhost:3000/utilisateurs/2
(l'utilisateur n°2 n'existant pas en BDD), alors l'API aurait un message du style : {"erreur":"Utilisateur non trouvé"}
.
Test route 5/6 : Mettre à jour un utilisateur (PUT /utilisateurs/:id)
Maintenant, nous allons mettre à jour une ou plusieurs données, concernant un utilisateur présent en base, d'après son id. Pour ce faire, nous allons comme toujours utiliser la commande CURL, mais cette fois-ci, en utilisant le mode "PUT" (pour faire de la "mise à jour de données").
Voici un exemple de code pour changer l'email de l'utilisateur n°1, qu'on avait précédemment enregistré :
curl -X PUT http://localhost:3000/utilisateurs/1 -H "Content-Type: application/json" -d "{\"email\": \"superclara@example.com\"}"
À noter ici qu'on aurait pu changer le nom utilisateur ou le mot de passe plutôt que l'email, et même, on aurait pu changer plusieurs de ces champs en même temps au lieu d'un seul, en les séparant par une virgule (si vous voulez un exemple de cela, revenez au test 2/6, où nous avions envoyé 3 paramètres à la fois, via curl).
Voici ce que j'obtiens en retour console :
{"message":"Utilisateur mis à jour avec succès"}
Et on peut vérifier que le(s) changement(s) ont bien été apportés en base de données, en demandant à notre API d'afficher à nouveau toutes les infos de cet utilisateur (avec la commande curl -X GET http://localhost:3000/utilisateurs/1
, pour rappel). Ce qui donne :

Là, on voit bien que l'email a bien été changé de "clara@example.com" en "superclara@example.com".
Test route 6/6 : Supprimer un utilisateur (DELETE /utilisateurs/:id)
Et voici le tout dernier test, pour vérifier la toute dernière route, permettant d'effacer un utilisateur donné, à partir de son id. Voici la commande CURL à saisir dans votre terminal, pour ce faire :
curl -X DELETE http://localhost:3000/utilisateurs/1
Pour rappel, le "1" à la fin de cette commande indique qu'on souhaite effacer l'utilisateur ayant un identifiant (id) égal à 1. Si cet utilisateur existe bel et bien en base, et que la suppression s'est bien passée, alors vous devriez obtenir le message suivant :
{"message":"Utilisateur supprimé avec succès"}
Et si on réinterroge l'API pour qu'elle nous retourne toutes les infos de cet utilisateur (id = 1), avec la commande curl -X GET http://localhost:3000/utilisateurs/1
, alors on obtient à présent {"erreur":"Utilisateur non trouvé"}
; ce qui est logique,, vu qu'on a effacé cet utilisateur là de notre base de données, à l'aide de notre API REST ! Pour preuve :

Voilà, je pense que nous avons à présent testé toutes les routes de notre API REST NodeJS / TypeScript ! C'était un peu long, mais au moins, on a fait ça à fond 😉
Conclusion : API REST NodeJS/TypeScript fonctionnelle !
Vous avez à présent une vue complète sur un exemple d'API REST écrit en TypeScript, tournant sous NodeJS, vous permettant d'apprendre comment développer une API REST de votre côté, par l'exemple ! Cela m'a également permis de vous montrer comment combiner Node.js, TypeScript, SQLite, et Argon2, pour construire une API sécurisée et fonctionnelle, et fortement typée (pour éviter un maximum de bugs). À noter toutefois que cette API est vraiment très basique, et qu'elle n'a qu'un but éducatif ; c'est pourquoi elle mériterait d'être améliorée, notamment au niveau de la vérification de mot de passe, et qu'une interface frontend ne serait pas du luxe, pour interagir avec cette API !
Au passage, n’hésitez pas à cloner ce projet d'API REST (Node JS + TypeScript) à partir de GitHub, pour vous en inspirer ! Du reste, si cet article vous a plu, n'hésitez pas à le partager et/ou à m'offrir un café (don symbolique), pour soutenir le site (via bouton jaune ci-dessous). Cela m'encouragera à faire tout plein d'autres articles, autour de TypeScript 🙂
Merci à vous !
Jérôme.
Merci d’avoir lu cet article ! LeCoinTS reste gratuit et sans pub grâce à vous.
Un petit don pour soutenir le site ? ☕


JEROME
Passionné par tout ce qui touche à la programmation informatique en TypeScript, sans toutefois en être expert, j'ai à coeur de vous partager ici, peu à peu, tout ce que j'ai appris, découvert, réalisé, et testé jusqu'à présent ! En espérant que tout cela puisse vous servir, ainsi qu'au plus grand nombre de francophones possible !
(*) Mis à jour le 21/04/2025