Partage
  • Partager sur Facebook
  • Partager sur Twitter

[NodeJS] #API #Théorie - Call RESTFull

Sujet résolu
    20 juillet 2015 à 22:21:35

    Bonjour à tous,

    J'ai un petit projet que je voudrais dev mais j'ai quelques questions théoriques au préalable sur mon architecture ainsi que sur les bonnes pratiques.

    1°)  Concernant les APIs REST / HATEOAS.

    S'il vous plaît s'il y a une bonne réponse, expliquez moi pourquoi c'est la bonne :p car je trouve des versions différentes sur le net et en lisant la RFC (une partie je dois l'avouer car ultra longue ) je n'ai pas trouvé ma réponse. Quel doit être le rendu d'un call du type :
    GET /user/{id}/events afin de respecter les normes REST
     
     A°/ Doit-il être du type :

    {
        firstname : "John",
        lastname  : "Smith",
        events    :  [
            {id : "1" , name : "Welcome Home", date : "01/01/01" },
            {id : "2" , name : "Let's eat"   , date : "01/01/01" }
        ]            
    }

    ou plus du type :

    {
        name  : "Welcome World",   
        date  : "01/01/01",        
        user  : [
                {id : "1" , name : "John Smith" }            
        ]
    },{
        name  : "Let's eat",
        date  : "01/01/01",
        user  : [
                {id : "1" , name : "John Smith" }
        ]
    }

     ou tout simplement renvoyer un tableau d'events :

    {
        name  : "Welcome World",   
        date  : "01/01/01"
    },
    {
       name  : "Welcome World",   
       date  : "01/01/01",
    }

    EDIT : Je crois avoir trouvé ma réponse pour ma question concernant RESTFul. Enfin de compte c'est lié au clef étrangère de notre DB mais du coup j'opterais pour la seconde car notre clef étrangère est dans notre table event. Si on fait un call sur /events on récupère notre user par la même occasion.

    B°/ Dans les calls REST, faut-il avoir tous les calls possibles /events/5/user  ET  /user/10/events/5 sont au final exactement les mêmes  mais le retour de ces call doivent-ils être différents ?

    2°) Aide architecture des dossiers

    On est d'accord que faire un /events ou un /users/{id}/events on appelle exactement la même méthode sauf que l'une des 2 méthodes peut avoir un paramètre donc au final j'appelle simplement la même méthode Je ne vais pas la redévelopper ^^ mais je voudrais séparer les routes par resources

    Mon architecture :

    /resources
       /user
          /controller
          routes.js
       /event
          /controller  => méthode getEvents(idUser=null)
          routes.js

    Mais dans ce cas là je vais faire un :

    require(../event/controller.js);

    dans mon module /user/routes.js de mon controller event. Je trouve ça un peu crade, dans le sens où ma ressource user n'est au final pas indépendante...

    Imaginons que je souhaite avoir les 2 resources sur des serveurs différents je ne pourrais pas...  Et je ne trouve pas ça pratique du tout... Je pêche vraiment sur les architectures des dossiers etc... donc si vous avez des conseils ou si vous voyez une solution je suis preneur. (Je n'ai pas déployer toute mon architecture mais c'est le gros point noir que je n'aime pas dans mon projet :/ ) Et idem si la réponse à ma précédente question est bien la bonne comment g

    D'ailleurs je passe un petit message, si un senior de dev pouvait me donner des conseils pour tout type de projet, j'apprécierais vraiment car je n'ai jamais eu de mentor senior.. Et je voudrais vraiment m'améliorer sur ces points... Je sais que parfois ce sont des questions bêtes mais même au niveau des noms de variables je n'ai jamais d'idées.. Et du coup j'ai toujours des variables qui ont des noms de merde et non parlantes :/ C'est sur tous ces "petits" points que je voudrais m'améliorer. Merci d'avance si quelqu'un se dévoue :p

    Merci d'avance pour vos réponses et merci d'avoir pris le temps de me lire.

    Cordialement,

    nb22721

    -
    Edité par nb22721 20 juillet 2015 à 22:34:54

    • Partager sur Facebook
    • Partager sur Twitter
    Celui qui pose une question risque cinq minutes d'avoir l'air bête, celui qui ne pose pas de question restera bête toute sa vie.
      21 juillet 2015 à 11:40:23

      Pour ta première question, un appel du type GET /user/{id}/events devrait pour moi retourner une collection d'évents sans pour autant retourner l'utilisateur avec (ca peut être le cas, mais pour moi c'est une surcharge de la réponse).


      Pour l'architecture, la solution la plus propre que j'ai trouvé pour ma part (j'ai eu aussi énormément de mal a structurer mon backend node), c'est d'enregistrer  tes points d'accès a la base a travers des services.

      Fichier route/player.js

      var services = app.get('services');
      
      module.exports.load = function (app) {
          app.get('/api/players/:id', function (req, res) {
              console.log("Routes -  Player::findById");
              services.player.findById(req.params.id).then(function (data) {
                  return res.json(data);
              }).catch(function (error) {
                  console.log(error);
                  //TODO implement error handler;
              });
          });
      }

      Services que je viens enregistrer de la manière suivante :

       Fichier services/index.js

      var Services = function () {
          return Services.initialize.apply(null, arguments);
      };
      
      // Constructor for a new `Services` object, it accepts
      // an active SqlConnection
      Services.initialize = function (configuration) {
          var services = {
              VERSION: '0.0.1'
          },
          knex = require('knex')({client: 'mysql', connection: configuration});
      
          services.knex = knex;
          services.bookshelf = require('bookshelf')(knex);
          services.models = require('./models.js')(services.bookshelf);
      
          // Register all services
          services.player = require('./player.js')(services.models);
      
          return services;
      };
      
      module.exports = Services;

      Et un service ressemble a ça :

       fichier services/player.js

      /**
       * Player services
       *
       * @param models
       */
      PlayerService = function (models) {
      
          var player = {};
      
          /**
           * Returns a player
           *
           * @param id
           * @returns player model
           */
          player.findById = function (id) {
              return new models.player({id: parseInt(id)}).
                      fetch({withRelated: ['country']});
          };
      
      
          return player;
      };
      
      module.exports = PlayerService;

      Et les services sont chargés dans l'app.js

      // Register services module
      var services = require('./app/services')(conf.db);
      app.set('services', services);


      Si jamais t'as besoin de clarifications hésites pas. Je sais que j'ai pas trouvé de VRAI stack node correctement formée. D'un côté ca pousse a bien réfléchir a sa structure, mais sans expérience c'est vraiment galère.

      -
      Edité par S4nGoKu 21 juillet 2015 à 11:42:42

      • Partager sur Facebook
      • Partager sur Twitter
        21 juillet 2015 à 14:09:23

        Salut, déjà le REST n'est pas une norme :).

        Tu as restify (bon ça ne te fait pas l'archi) c'est un peu comme Express mais sans les vues (uniquement pour les API REST).

        on bien loopback de strongloop (http://loopback.io) qui te permet de créer les routes automatiquement depuis tes modèles

        En général en REST :

        • GET /events = collections d'event (liste)
        • GET /events/:id = récupère l'event :id ou bien te balance une erreur http (404 normalement)
        • POST /event = créé un event
        • PUT /events/:id = modifie l'event :id
        • GET /users/:id/events = collections d'event d'un user :id
        • pas besoins de faire de /users/:uid/events/:eid car un simple /events/:id suffit.

        Après quand tu devs : les parties communes sont tes modèles (ou services ou peut importe le nom que tu leurs donnes) et dans tes routes tu appèles tout simplement ces modèles (l'exemple de s4ngoku est bon)

        • Partager sur Facebook
        • Partager sur Twitter
          21 juillet 2015 à 14:58:15

          Je note qu'il y a très peu d'exemples bien structurés sur le net. Après il est vrai que chacun à son organisation, soit par couche technique (MVC), soit par couche métier etc. Mais c'est vraiment galère quand on veut faire quelquechose de propre et qu'on a très peu d'expérience en architecture logicielle.

          -
          Edité par S4nGoKu 21 juillet 2015 à 14:58:32

          • Partager sur Facebook
          • Partager sur Twitter
            21 juillet 2015 à 21:27:46

            Bonjour !!

            Déjà UN MEGA GRAND MERCI BEAUCOUP pour vos réponses. Honnêtement je ne pensais pas avoir une réponse aussi vite ^^ Depuis hier j'ai fait de nouvelles recherches et du coup voici ce qui en a découlé.

            Note : Je reprends dans l'ordre de mes questions et je précise un peu plus mon environement :D

            Environment technique :
            NodeJS + Sequelize (ORM) + restify (justement pour mon API REST ^^ ) + gulp + mocha (Test unitaire)

            D'ailleurs petite question pour @S4nGoKu , j'ai vu que tu utilisais knex / bookshlef (knex => les requêtes ) / bookshelf (ORM) Pourquoi as-tu choisi ces 2 là ? Est ce que vous aviez fait un benchmark sur les ORM dispos sous node ou tu les connaissais déjà ? Comment ton choix technologique s'est orienté vers ses 2 techno ? Thanks

            (Je n'irais pas tout rechangé hein c'est juste par curiosité ;) )

            1°) Retours de call API REST :

            J'ai regadé l'api de GitHub et github fait le même process que ce que j'avais supposé mais j'avoue que c'est lourd. Après je comprends pourquoi ils ont fait ça mais je ne sais pas comment le gérer... Je m'explique , Lors du call sur :

            https://api.github.com/users/sequelize/repos : On voit l'owner dans chacun des repos.

            Au final je peux comprendre car la méthode appelée serait : getAllRepos(params) où le param peut être un id. Et dans le sens où si je fais un call sur  :

            https://api.github.com/repos : On voudrait retrouver directement qui est l'utilisateur du repos. Je pense que c'est ça l'implémentation de HATEOAS (? O.o ). C'est à dire qu'on récupère les infos mais en effet c'est assez lourd.

            Après j'ai vu avec certains ORM comme Sequelize, je pense qu'il me permet d'avoir ce retour donc je me dis que ce n'est pas anodin ^^

            Après je ne vois pas trop où l'implémenter mis à part lors du retour je formatte mon objet auquel je rajoute les liens HATEOAS. Avez-vous déjà implémenté HATEOAS ou pas dans vos projets ?

            2°/ Architecture:

            Voici mon archi complète et remodifiée en fonction de ce que j'ai pu lire sur le net ainsi que ton archi. Est ce que tu pourrais me dire aussi en quoi les 2 archi diffèrent dans le sens où tu verras peut être des points où je vais me tirer une balle dans le pied. Car des fois je fais des trucs et je me tire de balles dans le pied tout seul dans le sens où je n'arrive pas à anticiper l'ensemble par mon manque d'expérience :/ J'avoue que le principe KISS me fait défaut car je veux faire un truc simple à la base et je me retrouve avec une archi bidon :/

            ## RQE PERSO : TOUTES LES RESSOURCES AURONT LA MÊME ARCHI ######
            /ressources            => Ce folder aura tout le temps la même architecture
                 /user             => Le nom de notre ressource        
                /collections       => PAS ENCORE SUR  
                /controller        => pour le call appelé on renvoie aux services getEvents (les controllers auront éventuellement le check des valeurs envoyées / check type de valeur )
                /services          => notre service appelant notre méthode getEvents / findById => (ORM)
                routes.js          => les routes par défaut GET / POST / UPDATE DELETE pour cette ressource uniquement
                    init.js        => fichier qui initalisera notre resource user.
            ## END RQE PERSO                      ######
             
            /dal                   => Folder comprenant toutes nos DALs / models
               eventDAL.js         => Mon model
               init.js             => fichier qui initiera tous nos models Sequelize ainsi que les associations (hasMany / belongsTo etc..)
            
            /doc                   => Comment générer la doc directement depuis les models ? (check tool comments /** */ )
            
            /lib or /tool          => Contiendra tous nos middlewares + functions comme mixins et autres
            /migrations            => Migrations de Sequelize
            /test                  => Test Unitaires et fonctionnels. On pourrait recréer un folder avec le nom de notre ressource ou même éventuellement créer le folder test inside /resources/NOM_RESOURCE/ => A voir
            
            
            routes.js              => initialise toutes les routes de toutes nos ressources.. example tous les calls commençant par /users iront dans la ressource user . Mais lors d'un call /users/:id/events => il doit aller chercher la méthode dans le controller de la ressource events.... On va avoir des calls de ressources à travers toutes les autres ressources.. Ca va vite être le bordel non ?
            
            init.js                => initiate server
            ## ARCHI S4nGoKu
            /services
              event.js
              player.js
              index.js
            /route
              player.js
              event.js
              index.js  ???        => Ici as-tu un index qui te permet d'initialiser toutes tes routes ?
            ## END ARCHI S4nGoKu

            Je voudrais aussi rajouter quelque choses qui je pense me faciliterais la vie mais selon l'archi je ne sais pas si ça vaut le coup ou non : c'est lorsque mon call commence par /user je vais chercher direct dans la ressource user. Bonne ou mauvaise idée ? encore une fois est ce que je me tire une balle dans le pied ?

                
            3°) Question bonus :

            J'essaye de resecter ces normes : https://github.com/airbnb/javascript . Pensez-vous que c'est bien ou que ça ajoute trop de contraintes au projet ?

            Tests d'intégration : @S4nGoKu : J'ai vu dans ton code aussi que tu utilisais  les comments /** */ . Est-ce pour générer ta doc ? (J'en avais déjà entendu parler ^^) mais je voulais savoir quel outil tu utilisais pour faire ça (si tu l'utilises) ? Est ce un outil spécifique qui convient pour ton ORM (bookshelf) qui se mappe avec tes models ou c'est un tool qu'on peut utiliser quelque soit son ORM ?

            Quelles sont les bonnes pratiques pour les tests d'intégrations ? Car si je veux tester mes calls de mon API pour l'instant, histoire de voir ce qu'il renvoit etc je voudrais pouvoir être efficace. Alors comment je peux faire pour lui dire  sur le call GET /user/{id} je souhaite comme retour :

            {
              id        : "{id}",
              firstname : "John",
              lastname  : "Smith"
            }

            Je veux ça et rien d'autre dedans etc ? car avec mocha je peux faire ça mais il faut que je me tape tous les attrbuts à chaque fois, j'entends par là :

            (res.body).should.have.ownProperty('id');
            (res.body).should.have.ownProperty('firstname');
            (res.body).should.have.ownProperty('email');

            Mais ce code là dans chaque test unitaire comprenant mon Objet User. Je suis presque sur que quelqu'un a déjà dev ce module pour mapper ça avec les models de l'ORM un truc dans le genre non ? Parce que je trouve ça lourd de tout recopier à chaque fois :/

            Voilà j'ai enfin fini avec toutes mes questions :)

            Merci d'avance de vos réponses et encore une fois merci d'avoir pris le temps de me répondre :D

            P.S.: Vous connaissez des bons livres / sites webs à me conseiller sur l'architecture logicielle car en effet je n'ai absolument aucune expérience dans ce domaine :/

            -
            Edité par nb22721 21 juillet 2015 à 22:04:49

            • Partager sur Facebook
            • Partager sur Twitter
            Celui qui pose une question risque cinq minutes d'avoir l'air bête, celui qui ne pose pas de question restera bête toute sa vie.
              22 juillet 2015 à 10:13:44

              1. J'ai utilisé bookshelf.js car je l'ai testé et je l'ai trouvé tellement simple que je suis resté avec. Sachant qu'il se base sur knex, si jamais j'ai un requete un peu touchy à faire, je passe en mode requete knex, sinon je reste avec bookshelf.

              exemple :

              /**
                   * Returns a season
                   *
                   * @param id
                   * @returns season model
                   */
                  season.getMatchdayTable = function (seasonId, matchday) {
                      return new models.teamstat().query(function (qb) {
                          qb.innerJoin('match','teamstat.match_id','match.id')
                                  .where('match.matchday', '<=', parseInt(matchday))
                                  .andWhere('match.season_id', '=', parseInt(seasonId)); 
                      }).fetchAll();
                  };


              2. Je n'ai jamais utilise HATEOAS je ne peux pas t'aider la dessus.

              3. Pour mon archi, je t'avouerais que je ne suis pas sur qu'elle soit la meilleur, dans mon cas du moins. Car au fur et a mesure que mon site s'agrandi, je me retrouve avec des dossiers contenant 10 fichiers. Ca reste lisible, mais je me demande si une organisation "metier" n'aurait pas été mieux.

              Mes routes, je les charge dans l'app.js

              var routeDir = 'app/routes',
                  routeFiles = fs.readdirSync(routeDir);
              
              //Initializing routes
              routeFiles.forEach(function (file) {
                  var filePath = path.resolve('./', routeDir, file), route = require(filePath);
                  console.log('Loading routes for ' + file);
                  route.load(app);
              });
              4.

               initialise toutes les routes de toutes nos ressources.. example tous les calls commençant par /users iront dans la ressource user . Mais lors d'un call /users/:id/events => il doit aller chercher la méthode dans le controller de la ressource events.... On va avoir des calls de ressources à travers toutes les autres ressources.. Ca va vite être le bordel non ?

              Par forcément, /users/:id/events

              j'ai un cas comme le tiens, je l'ai résolu comme ça :

                  app.get('/api/players/:id/team', function (req, res) {
                      console.log("Routes -  Player::findTeamByPlayerId");
                      services.player.findTeamByPlayerId(req.params.id).then(function (data) {
                          return res.json(data.relations.team);
                      }).catch(function (error) {
                          console.log(error);
                          //TODO implement error handler;
                      });
                  });
                  /**
                   * Returns the player's team
                   *
                   * @param id
                   */
                  player.findTeamByPlayerId = function (id) {
                      return new models.player({id: parseInt(id)}).fetch({withRelated: 'team'});
                  };

              Je viens taper dans le repo de l'utilisateur et je remonte l'info avec une jointure. Ensuite le service ne remonte que l'info dont on a besoin (data.relation.team)

              5. Pour les commentaires, je me sert de mon IDE (WebStorm) qui se base sur la même syntaxe de la javadoc. Donc quand je me mets au dessus d'une fonction / classe et que je tape /**, mon IDE me génère la doc avec les arguments etc, je n'ai plus qu'a ajouter une description et l'intérêt des arguments et c'est bon. Ensuite je n'ai pas regardé pour la génération de la doc mais a mon avis, une simple commande doit pouvoir le faire.

              6.

              >Mais ce code là dans chaque test unitaire comprenant mon Objet User. Je suis presque sur que quelqu'un a déjà dev ce module pour mapper ça avec les models

              >de l'ORM un truc dans le genre non ? Parce que je trouve ça lourd de tout recopier à chaque fois :/

              Je vais te montrer un de mes tests (c'est la partie qui pèche chez moi, j'ai du faire genre 20 tests alors que j'ai 15 models différents, 50 routes etc...)

              describe('Player tests', function () {
                      it('Should returns a player', function (done) {
                          var playerId = 1;
                          tracker.on('query', function sendResult(query) {
                              query.response({'id': playerId});
                          });
                          services.player.findById(playerId).then(function (model) {
                              expect(model).to.be.an.instanceOf(services.models.player);
                              expect(model.get('id')).to.be.equals(playerId);
                              done();
                          });
                      });
              });

              Et j'ai donc la lib chai-expect qui me permet de vérifier que mon service me renvoi bien le bon model (donc il va vérifier chacun des champs, son type etc).

              Je sais pas si ca peut t'aider, mais c'est sur que si tu dois te taper la vérification de chaque champs de tes modèles t'as pas fini :)

              6. Pour les bouquins, je sais pas trop, j'en ai jamais lu sur la programmation, je devrais mais bon, la flemme.

              Encore une fois, ca viendra avec le temps. Si jamais je dois refaire une application web sous node, je sais que je ne suivrais pas la même architecture que celle que j'utilise en ce moment même si cette dernière me convient plus ou moins en ce moment. Faut juste faire, défaire, refaire etc. Jusqu'au moment ou on pourra savoir directement quel type d'archi on a besoin en fonction du projet qui va arriver :) .

              -
              Edité par S4nGoKu 22 juillet 2015 à 10:17:51

              • Partager sur Facebook
              • Partager sur Twitter
                22 juillet 2015 à 10:32:13

                Salut.

                Point par point : :)

                1:

                l'HATEOAS c'est juste des metadas concernant des liens :

                C'est souvent une propriété nommé links qui contient un tableau. par exemple :

                {
                prop1:"val1",
                links:[
                 {
                  href:"/tatayoyo",
                  rel: "self"
                 }
                ]

                href : correspond au lien de la resource, rel au type (self pour la resource même, previous/next si il y a de la pagination, image, thumbnail, ...)

                Donc github n'est pas forcement "HATEOAS".

                Dans le processus de ton appli : 

                • une requête est reçu 
                • appèle de la route en question
                • récupération des données
                • envois des données

                Tu n'appèles pas de fonction lors de la route, mais uniquement la méthode du modèle que tu veux pour récupérer les données ensuite dans la route tu fais le traitement et renvoies les données formaté (avec le HATEOAS si tu veux).

                2:

                Pour mon application j'ai quelque chose comme ceci :

                /resources
                 /0.0.1
                  /users
                    index.js
                  /events
                   index.js
                  /repos
                   index.js
                 /0.1.0
                  /users
                    index.js
                  /events
                   index.js
                  /repos
                   index.js
                /models
                 /0.0.1
                   user.js
                   events.js
                   repos.js
                 /0.1.0
                   user.js
                   events.js
                   repos.js

                J'utilise node-resource et restify-namespace

                Comme tu peux voir j'ai un répertoire pour chaque versions (pour assurer la rétro-compatibilité)

                Exemple d'un fichiers index :

                module.exports = function (server) {
                    namespace(server, '/users', function () {
                       server.get({
                            path: '',
                            version: version
                        }, function (req, res, next) {
                         var users = models.users.list();
                         res.json(users);
                });
                     server.get({
                            path: '/:id',
                            version: version
                        }, function (req, res, next) {
                         var user = models.users.get(req.param.id);
                         res.json(user);
                     });
                  });
                });
                
                

                en gros le nom du dossiers égale au début de l'adresse de l'url

                ça me permet d'avoir un versionning de l'api et assez modulaire.

                Pour le style j'utilise jslint et puis le lien que tu as donné c'est plus pour ES6 ^^

                Perso j'utile APIDOC (http://apidocjs.com) pour la génération de la doc de l'API et oui les /** */ c'est pour générer de la doc ;) .

                Moi je n'utilise pas knex (je n'utilise pas de base sql et encore moins MySQL) mais rethinkdb (avant j'étais avec MongoDB).

                Avec MongoDB j'utilisaiis mongoose (qui faisait office d'ODM, mais je l'utilisais surtout que c'était moins lourds à mettre en place que le driver de base).

                Avec rethinkDB je n'ai pas d'ODM

                P.S.: RethinkDB est une base NoSQL orienté temps réel avec support de jointures et plus facile à sharder que Mongo (faire du shard avec mongo c'est long ^^, avec rethinkdb c'est 1 clic :p)

                Voilà pour ma part

                • Partager sur Facebook
                • Partager sur Twitter
                  28 juillet 2015 à 21:36:58

                  Salut ,

                  Merci beaucoup pour vos réponses :D

                  Je me suis calqué sur vos conseils pour essayer d'avoir une bonne archi je verrais ce que ça donne :D

                  Je pense que je testerais vos ORMs et vos tools DB quand j'aurais le temps sur d'autres projets :p

                  Petites questions par curiosité : Qu'est ce que tu veux dire par sharder ? Qu'est ce que ça signifie ?  Et pour ton versionning en v.0.0.1 c'est avec tes modules que tu fais ça ou c'est toi qui copie toutes des datas inside ?

                  Merci encore :)

                  Amicalement,

                  nb22721

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Celui qui pose une question risque cinq minutes d'avoir l'air bête, celui qui ne pose pas de question restera bête toute sa vie.
                    28 juillet 2015 à 22:42:38

                    C'est moi qui créé les dossiers. Ton API va évoluer et tu devras être rétro-compatible avec les des versions précédentes c'est pour ça que je crée des dossier. (par exemple nouveau endpoints, endpoints deprecated, ...)
                    • Partager sur Facebook
                    • Partager sur Twitter

                    [NodeJS] #API #Théorie - Call RESTFull

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown