Reposez-vous avec le routeur imbriqué Express.js

136

Supposons que je veuille avoir des points de terminaison REST qui ressemblent à peu près à ceci:

/user/
/user/user_id 

/user/user_id/items/
/user/user_id/items/item_id

CRUD sur chacun si cela a du sens. Par exemple, / user POST crée un nouvel utilisateur, GET récupère tous les utilisateurs. / user / user_id GET ne récupère qu'un seul utilisateur.

Les éléments sont spécifiques à l'utilisateur, je les mets donc sous user_id , qui est un utilisateur particulier.

Maintenant, pour rendre le routage Express modulaire, j'ai créé quelques instances de routeur. Il y a un routeur pour l'utilisateur et un routeur pour l'article.

var userRouter = require('express').Router();
userRouter.route('/')
  .get(function() {})
  .post(function() {})
userRouter.route('/:user_id')
  .get(function() {})

var itemRouter = require('express').Router();
itemRouter.route('/')
  .get(function() {})
  .post(function() {})
itemRouter.route('/:item_id')
  .get(function() {})

app.use('/users', userRouter);

// Now how to add the next router?
// app.use('/users/', itemRouter);

L'URL vers itemest les descendants de la hiérarchie d'URL du user. Maintenant, comment puis-je obtenir une URL avec /usersquoi que ce soit à userRouter, mais la route plus spécifique de /user/*user_id*/items/à itemRouter? Et aussi, je voudrais que user_id soit également accessible à itemRouter, si possible.

câlin
la source
Excellentes réponses déjà sur l'utilisation d'Express pour résoudre ce problème. Vous pouvez cependant utiliser Loopback (construit sur Express) pour implémenter une API basée sur Swagger et ajouter des relations entre les modèles pour effectuer le CRUD comme vous l'avez demandé. Une bonne chose est qu'après la courbe d'apprentissage initiale, il est beaucoup plus rapide à assembler. loopback.io
Mike S.

Réponses:

278

Vous pouvez imbriquer des routeurs en les attachant comme middleware sur un autre routeur, avec ou sans params.

Vous devez passer {mergeParams: true}au routeur enfant si vous souhaitez accéder au à paramspartir du routeur parent.

mergeParamsa été introduit dans Express4.5.0 (5 juillet 2014)

Dans cet exemple, l' itemRouterattache à la userRoutersur l' /:userId/itemsitinéraire

Cela entraînera les itinéraires possibles suivants:

GET /user-> hello user
GET /user/5-> hello user 5
GET /user/5/items-> hello items from user 5
GET /user/5/items/6->hello item 6 from user 5

var express = require('express');
var app = express();

var userRouter = express.Router();
// you need to set mergeParams: true on the router,
// if you want to access params from the parent router
var itemRouter = express.Router({mergeParams: true});

// you can nest routers by attaching them as middleware:
userRouter.use('/:userId/items', itemRouter);

userRouter.route('/')
    .get(function (req, res) {
        res.status(200)
            .send('hello users');
    });

userRouter.route('/:userId')
    .get(function (req, res) {
        res.status(200)
            .send('hello user ' + req.params.userId);
    });

itemRouter.route('/')
    .get(function (req, res) {
        res.status(200)
            .send('hello items from user ' + req.params.userId);
    });

itemRouter.route('/:itemId')
    .get(function (req, res) {
        res.status(200)
            .send('hello item ' + req.params.itemId + ' from user ' + req.params.userId);
    });

app.use('/user', userRouter);

app.listen(3003);
Willem D'Haeseleer
la source
3
Merci d'avoir répondu. Le routeur que vous utilisez ici est plus explicitement imbriqué que celui partagé par Jordonias. Mais cela fonctionne-t-il de la même manière sous le capot? Je voudrais vous accorder la prime pour l'exhaustivité, mais je ne peux le faire que quelques heures plus tard.
huggie
Merci d'avoir répondu. Existe-t-il un moyen similaire d'obtenir à partir de l'itinéraire enfant les paramètres de requête de l'itinéraire parent?
cwarny
1
Cela me surprendrait s'ils ne sont disponibles sur aucun itinéraire, car le paramètre de requête n'est lié à aucun itinéraire en particulier ...
Willem D'Haeseleer
Réponse très complète! Une question: dans un souci d'encapsulation et de séparation des connaissances entre le routeur utilisateur et le routeur item, y a-t-il une manière déclarative de spécifier qu'un sous-routeur nécessite un paramètre? En d'autres termes, existe-t-il un moyen explicite d'écrire les appels d'enregistrement ou d'accès de telle sorte que le routeur d'élément nous fasse savoir qu'il s'attend à recevoir un identifiant d'utilisateur? Exemple de situation, le routeur d'élément est dans un autre fichier, structurellement, il n'est pas clair qu'il nécessite un utilisateur sauf si vous entrez dans ses appels et il est seulement clair dans le routeur utilisateur qu'il transmettrait un identifiant d'utilisateur
yo.ian.g
Ce n'est plus lisible que l'utilisation "standard" des routeurs, je cherche un moyen de visualiser l'imbrication lors de la visualisation du code.
DrewInTheMountains
127

itinéraires imbriqués gérables ...

Je voulais un exemple spécifique de faire des routes imbriquées d'une manière très gérable dans express 4 et c'était le premier résultat de recherche pour "routes imbriquées dans express". Voici une API qui aurait de nombreuses routes qui devraient être interrompues par exemple.

./index.js:

var app = require('express')();

// anything beginning with "/api" will go into this
app.use('/api', require('./routes/api'));

app.listen(3000);

./routes/api/index.js:

var router = require('express').Router();

// split up route handling
router.use('/products', require('./products'));
router.use('/categories', require('./categories'));
// etc.

module.exports = router;

./routes/api/products.js:

var router = require('express').Router();

// api/products
router.get('/', function(req, res) {
  res.json({ products: [] });
});

// api/products/:id
router.get('/:id', function(req, res) {
  res.json({ id: req.params.id });
});

module.exports = router;

Exemple d'imbrication dans la structure de dossiers

J'ai remarqué quelques commentaires sur la "structure des dossiers d'imbrication". Cela est implicite mais pas évident, j'ai donc ajouté la section ci-dessous. Voici un exemple spécifique de structure de dossiers imbriqués pour les itinéraires .

index.js
/api
  index.js
  /admin
    index.js
    /users
      index.js
      list.js
    /permissions
      index.js
      list.js

Ceci est plus un exemple général du fonctionnement du nœud. Si vous utilisez "index.js" dans des dossiers de la même manière que "index.html" fonctionne dans les pages Web pour un répertoire par défaut, il sera facile de faire évoluer votre organisation en fonction de la récursivité sans modifier vos points d'entrée en code. "index.js" est le document par défaut auquel on accède lors de l'utilisation de require dans un répertoire.

contenu de index.js

const express = require('express');
const router = express.Router();
router.use('/api', require('./api'));
module.exports = router;

contenu de /api/index.js

const express = require('express');
const router = express.Router();
router.use('/admin', require('./admin'));
module.exports = router;

contenu de /api/admin/index.js

const express = require('express');
const router = express.Router();
router.use('/users', require('./users'));
router.use('/permissions', require('./permissions'));
module.exports = router;

contenu de /api/admin/users/index.js

const express = require('express');
const router = express.Router();
router.get('/', require('./list'));
module.exports = router;

Il y a peut-être des problèmes DRY ici, mais cela se prête bien à l'encapsulation des préoccupations.

FYI, récemment, je suis entré dans l'actionhero et j'ai trouvé qu'il était complet avec des sockets et des tâches, plus comme un véritable cadre tout-en-un renversant le paradigme REST. Vous devriez probablement le vérifier en allant nu avec express.

Jason Sebring
la source
11
Je vois comment cela divise les itinéraires, mais comment aborde-t-il l'imbrication?
1252748
parfait ... et a du sens. C'est une option évolutive. Je serais curieux de savoir comment l'op implémentera le contrôle de version (v1, v2 etc)
Kermit_ice_tea
8
var userRouter = require('express').Router();
var itemRouter = require('express').Router({ mergeParams: true }); 

userRouter.route('/')
  .get(function(req, res) {})
  .post(function(req, res) {})
userRouter.route('/:user_id')
  .get(function() {})

itemRouter.route('/')
  .get(function(req, res) {})
  .post(function(req, res) {})
itemRouter.route('/:item_id')
  .get(function(req, res) {
    return res.send(req.params);
  });

app.use('/user/', userRouter);
app.use('/user/:user_id/item', itemRouter);

La clé de la deuxième partie de votre question est l'utilisation de l'option mergeParams

var itemRouter = require('express').Router({ mergeParams: true }); 

De /user/jordan/item/catje reçois une réponse:

{"user_id":"jordan","item_id":"cat"}
Jordonias
la source
Cool. La vôtre et la méthode de Willem fonctionnent pour ce que je voulais. Je vais vérifier son exhaustivité, mais je vous marque également. Merci beaucoup. Votre méthode n'a pas l'air imbriquée mais elle fait à peu près ce que je voulais, je pense que je préfère même la vôtre. Merci.
huggie
l'option mergeParams est la clé ici!
MrE
2

Utilisation de la solution @Jason Sebring et adaptation à Typescript.

server.ts

import Routes from './api/routes';
app.use('/api/', Routes);

/api/routes/index.ts

import { Router } from 'express';
import HomeRoutes from './home';

const router = Router();

router.use('/', HomeRoutes);
// add other routes...

export default router;

/api/routes/home.ts

import { Request, Response, Router } from 'express';

const router = Router();

router.get('/', (req: Request, res: Response) => {
  res.json({
    message: 'Welcome to API',
  });
});

export default router;
Pierre RA
la source
Pourriez-vous fournir ./api/routes?
Julian
1
@Julian: J'ai corrigé les emplacements des fichiers. ./api/routesa deux fichiers index.tset home.ts. Le premier est utilisé par server.ts. J'espère que cela vous aidera.
Pierre RA
0
try to add  { mergeParams: true } look to simple example  which it middlware use it in controller file getUser at the same for  postUser
    const userRouter = require("express").Router({ mergeParams: true });
    export default ()=>{
    userRouter
      .route("/")
      .get(getUser)
      .post(postUser);
    userRouter.route("/:user_id").get(function () {});
    
    
    }
Mohammed_Alreai
la source
-9

Vous n'avez besoin que d'un seul routeur et utilisez-le comme ceci:

router.get('/users');
router.get('/users/:user_id');

router.get('/users/:user_id/items');
router.get('/users/:user_id/items/:item_id');

app.use('api/v1', router);
eguneys
la source
Oui mais je veux séparer les logiques entre les éléments et les utilisateurs, et je préfère donc les séparer. Je ne sais pas si c'est possible.
huggie
@huggie itemsappartient à usersdroite, pourquoi avez-vous besoin de séparer cela? vous pouvez les définir dans différents fichiers en utilisant toujours le même routeur si vous le souhaitez.
eguneys
Il appartient à l'utilisateur mais je veux pouvoir le brancher ou le débrancher facilement sans affecter l'utilisateur. Et actuellement, j'ai chaque routeur pour différents points de terminaison d'URL. Le style semble être encouragé par express-générateur. Si ce n'est pas possible, alors oui peut-être que je devrais envoyer l'instance du routeur vers différents fichiers? Mais ce n'est pas cohérent avec les structures d'origine.
huggie
Est-il possible d'ajouter un routeur sous un autre? Puisque l'architecture middleware Express semble être gérée par le routeur en dessous (je ne suis pas tout à fait sûr que ce soit le cas), je pense que cela pourrait être possible.
huggie
2
-1 Cela ne répond pas à la question sur les routeurs imbriqués
Willem D'Haeseleer