ProFeel est une app de profilage professionnel. Le principe : l’utilisateur répond à une série de questionnaires comportementaux (mises en situation managériale, choix de mots, hiérarchisation de valeurs) et l’algorithme du client déduit un profil de personnalité. L’app couvre trois “univers” : Job (recrutement), Friends (affinités), Love (compatibilité). Chaque univers est un parcours de 18 questions avec un timer.

Le projet est classique côté technique : une app Ionic 1, du Webpack, du Babel, du SCSS. Ce qui est moins classique, c’est la situation dans laquelle on s’est retrouvés avec l’API.

Écran de questionnaire ProFeel en mode landscape, avec une mise en situation managériale et six réponses possibles
Un questionnaire ProFeel : mise en situation professionnelle avec choix multiples, timer en bas, et navigation séquentielle.

Le problème : une API qui n’existe qu’en prod

Le client avait un algorithme de profilage qui tournait déjà en production, derrière une API REST. C’est cette API qui servait les questionnaires, collectait les réponses et calculait les profils. Notre boulot : construire l’app mobile qui consomme cette API.

Sauf que le client n’avait pas d’environnement de test. Pas de staging, pas de sandbox, rien. L’API tournait en production, point. Et il nous promettait un accès “dans quelques semaines”. En attendant, on avait la doc Swagger de l’API et c’est tout.

On avait deux options. Attendre l’accès et perdre trois semaines. Ou trouver un moyen de bosser sans.

Le stub server : 50 lignes de Node pour débloquer le projet

L’idée était simple : si on a la spec Swagger, on peut monter un faux serveur qui répond exactement comme le vrai. Pas un mock fragile avec des if/else dans l’app, un vrai serveur HTTP séparé, avec les bons endpoints, les bons formats de réponse, et des données réalistes.

On a utilisé swagger-express-mw, un middleware Express qui lit un fichier swagger.yaml et route automatiquement les requêtes vers des controllers JavaScript. Le routage est déclaratif : chaque endpoint dans le Swagger pointe vers une fonction via operationId, et le middleware s’occupe de valider les paramètres et d’appeler le bon controller.

// app.js — le serveur complet tient en 35 lignes
const express = require("express");
const SwaggerExpress = require("swagger-express-mw");
const logger = require("morgan");

const app = express();
app.use(logger("short"));

// Swagger UI à la racine pour la doc
app.get("/", function(req, res) {
  res.sendFile(__dirname + "/index.html");
});

SwaggerExpress.create({ appRoot: __dirname }, function(err, swaggerExpress) {
  if (err) { throw err; }
  swaggerExpress.register(app);
  app.listen(process.env.PORT || 10010);
});

Les controllers sont tout aussi minimalistes. Un fichier JSON contient les 18 questions du questionnaire avec leurs réponses possibles, rédigées en français, dans le format exact de l’API de production. Les controllers lisent ce JSON et renvoient les données :

// surveys.js — renvoie un questionnaire complet
const { questions } = require("./stub_data.json");

function getSurveyById(req, res) {
  const surveyId = req.swagger.params.surveyId.value;
  res.json({
    idSurvey: surveyId,
    numQuestions: questions.length,
    questions: questions.map((q) => Object.assign({}, q, {
      idSurvey: surveyId,
    })),
  });
}
// questions.js — GET et PATCH sur une question
function getQuestionForSurveyByPosition(req, res) {
  const surveyId = req.swagger.params.surveyId.value;
  const questionPosition = req.swagger.params.questionPosition.value;

  if (questionPosition < 0 || questionPosition >= questions.length) {
    res.status(404).json({ code: 0, message: "Invalid question position" });
  } else {
    const question = questions[questionPosition];
    res.json(Object.assign({}, question, { idSurvey: surveyId }));
  }
}

function updateQuestion(req, res) {
  const questionPosition = req.swagger.params.questionPosition.value;
  const update = req.swagger.params.body.value;
  Object.assign(questions[questionPosition], update);
  res.json(Object.assign({}, questions[questionPosition], {
    idSurvey: req.swagger.params.surveyId.value,
  }));
}

Le PATCH modifie les données en mémoire. C’est volontaire : ça permet de tester le parcours complet (répondre à une question, voir la réponse persistée, revenir en arrière) sans base de données. Au redémarrage du serveur, tout est remis à zéro.

Pourquoi Swagger et pas un mock codé en dur

On aurait pu coller un fichier JSON derrière un json-server et basta. Mais le Swagger apporte quelque chose en plus : la validation automatique. Le middleware vérifie que les requêtes de l’app respectent le contrat, bons paramètres, bons types, bon format de body. Quand on s’est trompés sur un champ, on l’a su tout de suite, pas trois semaines plus tard en branchant la vraie API.

Et le stub embarque Swagger UI à la racine. N’importe qui dans l’équipe, y compris le client, peut ouvrir le navigateur, voir les endpoints disponibles, et tester les requêtes sans lire le code.

Le déploiement : Heroku en 30 secondes

Le stub est déployé sur Heroku. L’app Ionic pointe dessus en production via une variable Webpack :

// webpack.config/development.js
new webpack.DefinePlugin({
  'process.env': {
    'HOST_URL': JSON.stringify(`http://${address()}:3000`),
  }
}),

// webpack.config/production.js
new webpack.DefinePlugin({
  'process.env': {
    'HOST_URL': JSON.stringify("https://prof_stub_node.herokuapp.com"),
  }
}),

En dev, l’app tape sur le serveur local. En “production” (sur les devices de test), elle tape sur le stub Heroku. Le jour où le client a fourni l’accès à la vraie API, on a changé l’URL. Rien d’autre.

L’app Ionic : un projet classique bien outillé

Côté app, c’est de l’Ionic 1 avec Angular 1.x. Ionic 2 est sorti, mais on reste sur la v1 pour ce projet : le scope est limité, l’équipe connaît le stack, et Ionic 1 fait le boulot.

L’originalité est plutôt dans le tooling. On a monté un pipeline Webpack 2 complet avec Babel 6, ce qui nous permet d’écrire du ES2015+ (classes, arrow functions, destructuring, modules) tout en ciblant Cordova. C’est moins courant que ça en a l’air : la plupart des projets Ionic 1 qu’on croise utilisent encore Gulp ou le CLI Ionic standard.

// app/index.js — bootstrap Angular avec des imports ES6
import "ionic-sdk/release/js/ionic.bundle";
import stateConfig from "./app.state";
import appRun from "./app.run";
import modCommon from "./Common";
import modLogin from "./Login";
import modHome from "./Home";

const mod = angular.module("app", [
  "ionic", "ui.router",
  modCommon, modLogin, modHome,
]);
mod.config(stateConfig);
mod.run(appRun);

Organisation modulaire

Chaque feature est un module autonome avec son controller, son state UI Router, son template et ses styles :

app/
├── index.js              # bootstrap Angular
├── Common/
│   ├── services/
│   │   └── ExternalWebPages.js
│   ├── app.scss          # overrides Ionic
│   └── config.json
├── Login/
│   ├── Login.controller.js
│   ├── Login.state.js
│   ├── Login.html
│   └── LoginPage.css
└── Home/
    ├── Home.controller.js
    ├── Home.state.js
    ├── Home.html
    └── Transition/
        ├── Transition.controller.js
        ├── Transition.state.js
        └── universeDescription.json

Les controllers utilisent la syntaxe ES2015 class avec $inject pour la minification :

class LoginCtrl {
  constructor($scope, externalWebPages) {
    this.state = "signIn";
    this.externalWebPages = externalWebPages;

    $scope.$on("$ionicView.beforeEnter", () => {
      this._resetForm();
    });
  }

  toggleState() {
    this.state = this.state === "signIn" ? "signUp" : "signIn";
  }

  proceed(email, password) {
    if (this.isSignIn()) {
      this._signUserIn(email, password);
    } else {
      this._signUserUp(email, password);
    }
  }
}
LoginCtrl.$inject = ["$scope", "externalWebPages"];

La qualité de code en garde-fou

Le pipeline Webpack intègre ESLint, Stylelint (avec le preset SUIT CSS pour le BEM), PostCSS avec autoprefixer, et un truc qu’on venait de découvrir : prettier-eslint-webpack-plugin, qui reformate le code automatiquement à chaque build en dev. Tu enregistres un fichier mal indenté, Webpack le reformate et recharge. Ça a mis fin aux discussions sur le style de code dans l’équipe.

Côté CSS, Stylelint avec stylelint-selector-bem-pattern impose la convention BEM. Les noms de classe qui ne suivent pas le pattern sont signalés comme des erreurs. C’est un peu rigide, mais sur un projet avec plusieurs développeurs, ça évite le chaos dans les styles.

Le bilan : trois semaines de gagnées

Le stub a pris une demi-journée à monter. Le fichier JSON de données, une journée de plus (rédiger 18 questions réalistes avec 6 réponses chacune, c’est du travail). Total : un jour et demi d’investissement.

Quand le client nous a enfin donné l’accès à l’API de production, six semaines plus tard, l’app était pratiquement terminée. On a changé l’URL, lancé les tests, corrigé deux ou trois différences de format mineures (un champ en camelCase d’un côté et en snake_case de l’autre, ce genre de choses). En une journée, tout marchait.

Sans le stub, on aurait attendu six semaines avant de commencer à intégrer. Le projet aurait pris neuf semaines au lieu de six. Le stub a absorbé 80% du risque d’intégration en amont, parce que les validations Swagger avaient déjà vérifié que nos requêtes respectaient le contrat.

Si c’était à refaire, on referait pareil. Un jour et demi pour le stub, c’est rien comparé à six semaines à se tourner les pouces. Et le Swagger comme contrat partagé, c’est ce qui a fait que le branchement sur la vraie API s’est passé sans drame.