Lors de la création d'un back-end pour une API REST, Express.js est souvent le premier choix parmi les frameworks Node.js. Bien qu'il prenne également en charge la création de modèles HTML et de modèles statiques, dans cette série, nous nous concentrerons sur le développement back-end à l'aide de TypeScript. L'API REST résultante sera celle que n'importe quel framework frontal ou service back-end externe serait en mesure d'interroger.
Tu auras besoin:
Dans un terminal (ou une invite de commande), nous allons créer un dossier pour le projet. À partir de ce dossier, exécutez npm init
. Cela créera certains des fichiers de projet de base Node.js dont nous avons besoin.
Ensuite, nous ajouterons le framework Express.js et quelques bibliothèques utiles:
npm install --save express debug winston express-winston cors
Il y a de bonnes raisons pour lesquelles ces bibliothèques sont Développeur Node.js favoris:
debug
est un module que nous allons utiliser pour éviter d'appeler console.log()
tout en développant notre application. De cette façon, nous pouvons facilement filtrer les instructions de débogage lors du dépannage. Ils peuvent également être entièrement désactivés en production au lieu de devoir être retirés manuellement.winston
est responsable de la journalisation des requêtes vers notre API et des réponses (et erreurs) renvoyées. express-winston
s'intègre directement avec Express.js, de sorte que toutes les API standard winston
le code de journalisation est déjà terminé.cors
est un élément du middleware Express.js qui nous permet d'activer partage de ressources cross-origin . Sans cela, notre API ne serait utilisable qu'à partir des frontaux servis à partir du même sous-domaine que notre back-end.Notre back-end utilise ces packages lorsqu'il est en cours d'exécution. Mais nous devons également installer certains développement dépendances pour notre configuration TypeScript. Pour cela, nous allons lancer:
npm install --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript
Ces dépendances sont nécessaires pour activer TypeScript pour le propre code de notre application, ainsi que les types utilisés par Express.js et d'autres dépendances. Cela peut faire gagner beaucoup de temps lorsque nous utilisons un IDE comme WebStorm ou VSCode en nous permettant de compléter automatiquement certaines méthodes de fonction lors du codage.
Les dépendances finales dans package.json
devrait être comme ça:
'dependencies': { 'debug': '^4.2.0', 'express': '^4.17.1', 'express-winston': '^4.0.5', 'winston': '^3.3.3', 'cors': '^2.8.5' }, 'devDependencies': { '@types/cors': '^2.8.7', '@types/debug': '^4.1.5', '@types/express': '^4.17.2', 'source-map-support': '^0.5.16', 'tslint': '^6.0.0', 'typescript': '^3.7.5' }
Maintenant que toutes nos dépendances requises sont installées, commençons à créer notre propre code!
Pour ce tutoriel, nous allons créer seulement trois fichiers:
./app.ts
./common/common.routes.config.ts
./users/users.routes.config.ts
L'idée derrière les deux dossiers de la structure du projet (common
et users
) est d'avoir des modules individuels qui ont leurs propres responsabilités. En ce sens, nous allons éventuellement avoir tout ou partie des éléments suivants pour chaque module:
Cette structure de dossiers fournit un point de départ précoce pour le reste de cette série de tutoriels et suffisamment pour commencer à pratiquer.
Dans le common
dossier, créons le common.routes.config.ts
fichier pour ressembler à ce qui suit:
import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } }
La façon dont nous créons les itinéraires ici est facultative. Mais puisque nous travaillons avec TypeScript, notre scénario de routes est l'occasion de s'entraîner à utiliser l'héritage avec le extends
mot-clé, comme nous le verrons sous peu. Dans ce projet, tous les fichiers de route ont le même comportement: ils ont un nom (que nous utiliserons à des fins de débogage) et accèdent au fichier Express.js principal Application
objet.
Maintenant, nous pouvons commencer à créer le fichier d'itinéraire des utilisateurs. Au niveau users
dossier, créons users.routes.config.ts
et commencez à le coder comme ceci:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } }
Ici, nous importons le CommonRoutesConfig
class et en l'étendant à notre nouvelle classe, appelée UsersRoutes
. Avec le constructeur, nous envoyons l'application (l'objet principal express.Application
) et le nom UsersRoutes au constructeur de CommonRoutesConfig
.
Cet exemple est assez simple, mais lors de la mise à l'échelle pour créer plusieurs fichiers d'itinéraire, cela nous aidera à éviter le code en double.
Supposons que nous souhaitons ajouter de nouvelles fonctionnalités dans ce fichier, telles que la journalisation. Nous pourrions ajouter le champ nécessaire au CommonRoutesConfig
classe, puis toutes les routes qui s'étendent CommonRoutesConfig
y aura accès.
Et si nous souhaitons avoir des fonctionnalités similaire entre ces classes (comme la configuration des points de terminaison de l'API), mais qui nécessite une implémentation différente pour chaque classe? Une option consiste à utiliser une fonctionnalité TypeScript appelée abstraction .
Créons une fonction abstraite très simple que le UsersRoutes
class (et les futures classes de routage) hériteront de CommonRoutesConfig
. Disons que nous voulons forcer toutes les routes à avoir une fonction (afin que nous puissions l'appeler à partir de notre constructeur commun) nommée configureRoutes()
. C’est là que nous déclarerons les points de terminaison de la ressource de chaque classe de routage.
Pour ce faire, nous allons ajouter trois choses rapides à common.routes.config.ts
:
abstract
à notre class
line, pour activer l'abstraction pour cette classe.abstract configureRoutes(): express.Application;
. Cela force toute classe étendant CommonRoutesConfig
pour fournir une implémentation correspondant à cette signature - si ce n'est pas le cas, le compilateur TypeScript lèvera une erreur.this.configureRoutes();
à la fin du constructeur, puisque nous pouvons maintenant être sûrs que cette fonction existera.Le résultat:
import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; }
Avec cela, toute classe étendant CommonRoutesConfig
doit avoir une fonction appelée configureRoutes()
qui renvoie un express.Application
objet. Cela signifie users.routes.config.ts
doit être mis à jour:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } }
Pour récapituler ce que nous avons fait:
Nous importons d'abord le common.routes.config
fichier, puis le express
module. On définit alors le UserRoutes
classe, en disant que nous voulons qu'il étende le CommonRoutesConfig
classe de base, ce qui implique que nous promettons qu'elle implémentera configureRoutes()
.
Pour envoyer des informations au CommonRoutesConfig
classe, nous utilisons le constructor
de la classe. Il s'attend à recevoir le express.Application
objet, que nous décrirons plus en détail dans la prochaine étape. Avec super()
, nous transmettons au constructeur de CommonRoutesConfig
l'application et le nom de nos routes, qui dans ce scénario est UsersRoutes. (super()
, à son tour, appellera notre implémentation de configureRoutes()
.)
Le configureRoutes()
La fonction est l'endroit où nous allons créer les points de terminaison pour les utilisateurs de notre API REST. Là, nous utiliserons le application et son route fonctionnalités d'Express.js.
L'idée d'utiliser le app.route()
La fonction est d’éviter la duplication de code, ce qui est facile puisque nous créons une API REST avec des ressources bien définies. La principale ressource de ce didacticiel est utilisateurs . Nous avons deux cas dans ce scénario:
users
à la fin du chemin demandé. (Nous n'entrerons pas dans le filtrage des requêtes, la pagination ou d'autres requêtes similaires dans cet article.)users/:userId
.Le chemin .route()
fonctionne dans Express.js nous permet de gérer les verbes HTTP avec un chaînage élégant. En effet, .get()
, .post()
, etc., tous renvoient la même instance de IRoute
que le premier .route()
l'appel fait. La configuration finale sera comme ceci:
configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // this middleware function runs before any request to /users/:userId // but it doesn't accomplish anything just yet--- // it simply passes control to the next applicable function below using next() next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; }
Le code ci-dessus permet à tout client d'API REST d'appeler notre users
extrémité avec un POST
ou un GET
demande. De même, il permet à un client d'appeler notre /users/:userId
extrémité avec un GET
, PUT
, PATCH
ou DELETE
demande.
Mais pour /users/:userId
, nous avons également ajouté un middleware générique à l'aide de all()
, qui sera exécutée avant l'une des fonctions get()
, put()
, patch()
ou delete()
les fonctions. Cette fonction sera utile lorsque (plus tard dans la série) nous créerons des routes destinées à être accessibles uniquement par des utilisateurs authentifiés.
Vous avez peut-être remarqué que dans notre .all()
function - comme avec n'importe quel middleware - nous avons trois types de champs: Request
, Response
et NextFunction
.
NextFunction
sert de fonction de rappel, permettant au contrôle de passer par toutes les autres fonctions du middleware. En cours de route, tous les middleware partageront les mêmes objets de requête et de réponse avant que le contrôleur ne renvoie finalement une réponse au demandeur.app.ts
Maintenant que nous avons configuré certains squelettes de routes de base, nous allons commencer à configurer le point d'entrée de l'application. Créons le app.ts
fichier à la racine de notre dossier de projet et commencez-le avec ce code:
import express from 'express'; import * as http from 'http'; import * as bodyparser from 'body-parser'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug';
Seules deux de ces importations sont nouvelles à ce stade de l'article:
http
est un module natif de Node.js. Il est nécessaire pour démarrer notre application Express.js.body-parser
est un middleware fourni avec Express.js. Il analyse la demande (dans notre cas, comme JSON) avant que le contrôle ne passe à nos propres gestionnaires de demandes.Maintenant que nous avons importé les fichiers, nous allons commencer à déclarer les variables que nous voulons utiliser:
const app: express.Application = express(); const server: http.Server = http.createServer(app); const port: Number = 3000; const routes: Array = []; const debugLog: debug.IDebugger = debug('app');
Le express()
La fonction retourne l'objet principal de l'application Express.js que nous transmettrons à travers notre code, en commençant par l'ajouter au http.Server
objet. (Nous devrons démarrer le http.Server
après avoir configuré notre express.Application
.)
Nous écouterons sur le port 3000 au lieu des ports standard 80 (HTTP) ou 443 (HTTPS), car ceux-ci seraient généralement utilisés pour le front-end d'une application.
Il n’existe pas de règle selon laquelle le port doit être 3 000 - s’il n’est pas spécifié, un port arbitraire sera assigné - mais 3000 est utilisé dans les exemples de documentation pour Node.js et Express.js, donc nous continuons la tradition ici.
Nous pouvons toujours fonctionner localement sur un port personnalisé, même lorsque nous voulons que notre back-end réponde aux demandes sur les ports standard. Cela nécessiterait un proxy inverse pour recevoir des requêtes sur le port 80 ou 443 avec un domaine ou un sous-domaine spécifique. Il les redirigerait ensuite vers notre port interne 3000.
Le routes
array gardera une trace de nos fichiers de routes à des fins de débogage, comme nous le verrons ci-dessous.
Enfin, debugLog
se terminera par une fonction similaire à console.log
, mais en mieux: c'est plus facile à régler car elle est automatiquement étendue à ce que nous voulons appeler notre contexte de fichier / module. (Dans ce cas, nous l'avons appelé 'app' lorsque nous l'avons passé dans une chaîne au constructeur debug()
.)
Nous sommes maintenant prêts à configurer tous nos modules middleware Express.js et les routes de notre API:
// here we are adding middleware to parse all incoming requests as JSON app.use(bodyparser.json()); // here we are adding middleware to allow cross-origin requests app.use(cors()); // here we are configuring the expressWinston logging middleware, // which will automatically log all HTTP requests handled by Express.js app.use(expressWinston.logger({ transports: [ new winston.transports.Console() ], format: winston.format.combine( winston.format.colorize(), winston.format.json() ) })); // here we are adding the UserRoutes to our array, // after sending the Express.js application object to have the routes added to our app! routes.push(new UsersRoutes(app)); // here we are configuring the expressWinston error-logging middleware, // which doesn't *handle* errors per se, but does *log* them app.use(expressWinston.errorLogger({ transports: [ new winston.transports.Console() ], format: winston.format.combine( winston.format.colorize(), winston.format.json() ) })); // this is a simple route to make sure everything is working properly app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(`Server up and running!`) });
Vous avez peut-être remarqué que le expressWinston.errorLogger
est réglé après nous définissons nos itinéraires. Ce n'est pas une erreur! Comme le documentation express-winston États:
L'enregistreur doit être ajouté APRÈS le routeur express (
app.router
) et AVANT l'un de vos gestionnaires d'erreurs personnalisés (express.handler
).
Enfin et surtout:
server.listen(port, () => { debugLog(`Server running at http://localhost:${port}`); routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); });
Cela démarre en fait notre serveur. Une fois lancé, Node.js exécutera notre fonction de rappel, qui signale que nous sommes en cours d’exécution, suivie des noms de toutes les routes que nous avons configurées - jusqu’à présent, juste UsersRoutes
.
package.json
pour transpiler TypeScript en JavaScript et exécuter l'applicationMaintenant que notre squelette est prêt à fonctionner, nous avons d'abord besoin d'une configuration standard pour activer la transpilation TypeScript. Ajoutons le fichier tsconfig.json
à la racine du projet:
{ 'compilerOptions': { 'target': 'es2016', 'module': 'commonjs', 'outDir': './dist', 'strict': true, 'esModuleInterop': true, 'inlineSourceMap': true } }
Ensuite, il suffit d'ajouter la touche finale à package.json
sous la forme des scripts suivants:
'scripts': { 'start': 'tsc && node ./dist/app.js', 'debug': 'export DEBUG=* && npm run start', 'test': 'echo 'Error: no test specified' && exit 1' },
Le test
script est un espace réservé que nous remplacerons plus tard dans la série.
La tsc dans le start
script appartient à TypeScript. Il est responsable de la transpilation de notre code TypeScript en JavaScript, qu'il affichera dans le dist
dossier. Ensuite, nous exécutons simplement la version construite avec node ./dist/app.js
.
Le debug
script appelle le start
script mais définit d'abord un DEBUG
variable d'environnement. Cela a pour effet d'activer tous nos debugLog()
instructions (plus d'autres similaires à partir d'Express.js lui-même, qui utilise le même module debug
que nous faisons) pour afficher des détails utiles au terminal - des détails qui sont (commodément) autrement cachés lors de l'exécution du serveur en mode de production avec un standard npm start
.
Essayez d'exécuter npm run debug
vous-même, puis comparez cela avec npm start
pour voir comment la sortie de la console change.
Astuce: vous pouvez limiter la sortie de débogage à notre app.ts
propre fichier debugLog()
instructions utilisant DEBUG=app
au lieu de DEBUG=*
. Le debug
module est généralement assez flexible, et cette fonctionnalité ne fait pas exception .
Les utilisateurs de Windows devront probablement changer le export
à SET
depuis export
est comment cela fonctionne sur Mac et Linux. Si votre projet doit prendre en charge plusieurs environnements de développement, le package cross-env fournit une solution simple ici.
Avec npm run debug
ou npm start
toujours en cours, notre API REST sera prête à traiter les requêtes sur le port 3000. À ce stade, nous pouvons utiliser cURL, Facteur , Insomnie , etc. pour tester le back-end.
Étant donné que nous n'avons créé qu'un squelette pour la ressource utilisateurs, nous pouvons simplement envoyer des requêtes sans corps pour voir que tout fonctionne comme prévu. Par exemple:
curl --location --request GET 'localhost:3000/users/12345'
Notre back-end devrait renvoyer la réponse GET requested for id 12345
.
Quant à POST
ing:
curl --location --request POST 'localhost:3000/users' --data-raw ''
Ceci et tous les autres types de requêtes pour lesquelles nous avons construit des squelettes seront assez similaires.
Dans cet article, nous avons commencé à créer une API REST en configurant le projet à partir de zéro et en plongeant dans les bases du framework Express.js. Ensuite, nous avons fait notre premier pas vers la maîtrise de TypeScript en créant un modèle avec UsersRoutesConfig
étendant CommonRoutesConfig
, un modèle que nous réutiliserons pour le prochain article de cette série. Nous avons terminé en configurant notre app.ts
point d'entrée pour utiliser nos nouvelles routes et package.json
avec des scripts pour construire et exécuter notre application.
Mais même les bases d'une API REST créée avec Express.js et TypeScript sont assez impliquées. Dans la partie suivante de cette série, nous nous concentrons sur la création de contrôleurs appropriés pour la ressource des utilisateurs et explorons quelques modèles utiles pour les services, les intergiciels, les contrôleurs et les modèles.
Le projet complet est disponible sur GitHub , et le code à la fin de cet article se trouve dans le toptal-article-01
branche.
Absolument! Il est très courant que les packages npm populaires (y compris Express.js) aient des fichiers de définition de type TypeScript correspondants. C'est vrai à propos de Node.js lui-même, ainsi que des sous-composants inclus comme son package de débogage.
Oui. Node.js peut être utilisé seul pour créer des API REST prêtes pour la production, et il existe également plusieurs frameworks populaires comme Express.js pour réduire l'inévitable passe-partout.
Non, il n'est pas difficile de commencer à apprendre TypeScript pour ceux qui ont une expérience JavaScript moderne. C'est encore plus facile pour ceux qui ont de l'expérience en programmation orientée objet. Mais maîtriser toutes les nuances et les meilleures pratiques de TypeScript prend du temps, comme pour toute compétence.
Cela dépend du projet, mais c'est certainement recommandé pour la programmation Node.js. C'est un langage plus expressif pour modéliser des domaines problématiques du monde réel sur le back-end. Cela rend le code plus lisible et réduit le potentiel de bogues.
TypeScript est utilisé partout où JavaScript est trouvé, mais il est particulièrement bien adapté aux applications plus importantes. Il utilise JavaScript comme base, ajoutant un typage statique et une bien meilleure prise en charge du paradigme de la programmation orientée objet (POO). Ceci, à son tour, prend en charge une expérience de développement et de débogage plus avancée.