# Un backend web clean en NodeJS

Le but de ce "hands-on"  est de réaliser un backend web complet en NodeJS de manière progressive avec des étapes simples, en essayant de produire du code moderne et de qualité et se poser les bonnes questions.

Cette matière à vocation pédagogique peut aisément servir de "boilerplate" pour des besoins variés de réalisation que ce soit pour accompagner un frontend ou une app mobile.

L'idée est de proposer un cadre simple et suffisamment évolutif pour servir de base à étendre avec vos propres besoins (bases / stores, files de message, …)

# Etape 1

> [Code source](https://github.com/NijiDigital/clean-nodejs-web-backend/tree/step1)

## Projet initial

Créer le projet :

```shell
$ mkdir web-backend
$ cd web-backend
$ npm init
…
package name: (web-backend)
version: (1.0.0) 0.0.1
description: A clean web backend powered by NodeJS
entry point: (index.js) src/server.js
test command: 
git repository: 
keywords: 
license: (ISC) 
…
```

Pour propulser notre serveur web, plusieurs choix sont possibles :
- [Express](https://expressjs.com/) : le framework le plus connu, historique, mais qui ne supporte pas les promesses et l'instruction `async` ce qui nous empêche d'utiliser du code moderne
- [NestJS](https://nestjs.com/) : framework complet tout en un qui inclut beaucoup de briques et propose un cadre proche d'Angular, surdimensionné et trop structurant pour notre besoin
- [Fastify](https://www.fastify.io/) : assez proche d'express en terme de fonctionnalités et positionnement, plus moderne, assez ouvert, extensible et performant

Nous partirons sur Fastify qui a le mérite d'être complètement "promisifé" et efficace pour faire des choses simples.

Mais auparavant un peu de préparation pour installer des dépendances proprement…

## Préparation du projet

Ce que l'on souhaite :
- configurer npm pour sauvegarder par défaut les dépendances dans package.json
- stocker les modules npm téléchargés dans un répertoire local `tmp` pour ne pas polluer le scope global

On en profite aussi pour créer notre .gitignore afin d'éviter de pousser dans le référentiel des sources des fichiers qui n'ont pas vocation à l'être (temporaires, personnels ou générés).

Copier ce contenu dans `.gitignore` ([plus d'infos](https://www.atlassian.com/fr/git/tutorials/saving-changes/gitignore)) :

```
# IDE settings
/.idea/
/.npm/
/.vscode/
/*.iml

# installed NPM modules
/node_modules/

# temp files
/tmp/
.DS_Store

# log files
npm-debug.log

# local files
*.local
.env
```

### Fixer les versions de NodeJS et NPM

Pour cela on va utiliser l'excellent outil [volta](https://volta.sh/) :
- [Installer volta](https://docs.volta.sh/guide/getting-started)
- Configurer la version de NodeJS
	```shell
	$ volta pin node@16.14.2
	```
- Configurer la version de NPM
	```shell
	$ volta pin npm@8.5.5
	```

Cela a pour effet d'ajouter un attribut volta dans `package.json` qui permet de définir les version de NodeJS et NPM à utiliser lorsqu'on exécute les commandes node et npm.

### Configurer NPM

Créer un fichier `.npmrc` avec le contenu suivant :

```
cache=./.npm
engine-strict=true
save=true
save-exact=true
tag-version-prefix=""
```

Ajouter `private: true` au fichier `package.json`, pour éviter de publier par erreur dans la registry npm publique :

```
{
  "name": "web-backend",
  "version": "0.0.1",
  "private": true,
  "description": "A clean web backend powered by NodeJS",
…
}
```

### Dépendances NPM

Installation de la dépendance fastify, notre dépendance principale qui permet de propulser le serveur web :

```shell
$ npm i fastify
```

### Scripts NPM

Ajoutons une fonctionnalité de watch avec `nodemon` ([plus d'infos](https://github.com/remy/nodemon#nodemon)), bien pratique pendant les phases de mise au point pour redémarrer notre futur serveur à chaud, ainsi que `prettier` ([plus d'infos](https://prettier.io/)) pour normaliser le format de notre code source  :

```shell
$ npm i -D nodemon prettier
```

On ajoute la configuration  dans `nodemon.json` :

```json
{
  "watch": [
    "package.json",
    "src",
    ".env"
  ],
  "ext": "js,json"
}
```

Déclarons les scripts NPM `start` et `watch` qui permettent de normaliser nos interactions avec le projet :

```json
{
  …
	"scripts": {
    "prettier": "prettier --check .",
    "start": "node --experimental-specifier-resolution=node src/server",
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "nodemon --exec npm start"
  },
  …
}
```

Pour vérifier le format des sources du projet, il suffira de lancer `npm run prettier`.

On souhaite utiliser une syntaxe moderne (ECMAScript) pour profiter des instructions `import` / `export`, il faut pour cela ajouter une option dans la commande node ([plus d'infos](https://nodejs.org/api/packages.html#type)), et déclarer le type de résolution des modules node avec `module` dans `package.json` :

```json
{
  …
  "type": "module",
  …
}
```

# Etape 2

> [Code source](https://github.com/NijiDigital/clean-nodejs-web-backend/tree/step2)

## Serveur "Hello"

Créer le source principal du serveur minimal `src/server.js` :

```javascript
import Fastify from 'fastify'

const fastify = Fastify()

fastify.get('/', async () => ({ hello: 'world' }))

const start = async () => {
  try {
    await fastify.listen(3000)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

void start()
```

Démarrage de notre serveur :

```shell
$ npm run watch
```

Il est temps maintenant de tester le fonctionnement de notre serveur avec un client HTTP (cURL, Postman, ou encore httpie).

Par exemple avec `httpie` ([plus d'infos](https://httpie.io/)) :

```shell
$ http :3000/
HTTP/1.1 200 OK
…
content-type: application/json; charset=utf-8

{
    "hello": "world"
}
```

C'est le moment de se congratuler avant de s'intéresser à l'itération suivante 😄

# Etape 3

> [Code source](https://github.com/NijiDigital/clean-nodejs-web-backend/tree/step3)

## Ajouter une base MongoDb

Provisionner un serveur mongodb en local avec Docker en ajoutant la stack suivante dans `docker-compose.yml` :

```
version: '3'

services:

  mongo:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    volumes:
      - mongodb-data:/data
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_ADMIN_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ADMIN_PASSWORD}
    network_mode: bridge
    restart: unless-stopped

  mongo-express:
    image: mongo-express
    container_name: mongo-express
    ports:
      - "8081:8081"
    links:
      - mongo
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGO_ADMIN_USERNAME}
      ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_ADMIN_PASSWORD}
    network_mode: bridge
    restart: unless-stopped
    depends_on:
      - mongo

volumes:
  mongodb-data:
```

Prérequis : installer [docker](https://docs.docker.com/get-docker/) et [docker-compose](https://docs.docker.com/compose/install/)

Puis démarrer les services :

```shell
$ docker-compose up -d
```

Provisionnement des données dans mongodb :
- Se connecter à l'UI mongo express : http://localhost:8081
- Créer une base `dev-db` ("Create database")
- Créer une collection de documents `animals` ("Create collection")
- Ajouter quelques documents :
	```json
	{
    	"name": "Roger",
    	"type": "Rabbit"
	}
	```
	```json
	{
    	"name": "Peppo",
    	"type": "Cat"
	}
	```
	```json
	{
    	"name": "Pluto",
    	"type": "Dog"
	}
	```

## Intégration du client Mongo avec Fastify

Pour utiliser mongodb depuis notre application nous avons besoin de :
- fastify-mongodb : librairie client de mongodb
- fastify-plugin : un helper de plugin pour fastify

### Installer les dépendances

```shell
$ npm i fastify-mongodb fastify-plugin
```

### Ajout d'une route GET /animals

Modifier `src/plugins/routes-plugin.js` pour ajouter la nouvelle route :

```javascript
const routesPlugin = async (fastify) => {
  const animalsCollection = fastify.mongo.db.collection('animals')

  fastify.get('/', async () => ({ hello: 'world' }))

  fastify.get('/animals', () => animalsCollection.find().toArray())
}

export default routesPlugin
```

Pour que le gestionnaire de requête puisse fonctionner, il faut que fastify fournisse la propriété `mongo`.
Cette fonctionnalité est fournie par le module `fastify-mongodb` associé au connecteur `src/plugins/db-connector-plugin.js` :

```javascript
import fastifyPlugin from 'fastify-plugin'
import fastifyMongo from 'fastify-mongodb'

const dbConnector = async (fastify) => {
  fastify.register(fastifyMongo, {
    forceClose: true,
    url: 'mongodb://localhost:27017/dev-db',
  })
}

const dbConnectorPlugin = fastifyPlugin(dbConnector)

export default dbConnectorPlugin
```

Maintenant il faut enregistrer `dbConnectorPlugin` en adaptant `src/server.js` :

```javascript
import Fastify from 'fastify'
import dbConnectorPlugin from './plugins/db-connector-plugin'
import routesPlugin from './plugins/routes-plugin'
…
fastify.register(dbConnectorPlugin)
fastify.register(routesPlugin)
…
```

Il est temps de tester l'application : `npm run watch`

Et consommer la ressource /animals :

```shell
$ http :3000/animals
HTTP/1.1 200 OK
…
content-type: application/json; charset=utf-8

[
    {
        "_id": "623ed2036d656e61c26e542a",
        "name": "Roger",
        "type": "Rabbit"
    },
    {
        "_id": "623ed2116d656e61c26e542c",
        "name": "Peppo",
        "type": "Cat"
    },
    {
        "_id": "623ed21b6d656e61c26e542e",
        "name": "Pluto",
        "type": "Dog"
    }
]
```

Notre serveur manque cruellement de traces…

Fastify s'appuie sur `pino` pour les traces, et de base elles sont en JSON, ce qui est plutôt adapté à des besoins de machine-to-machine, par exemple pour de la production où les logs sont analysées automatiquement.

En revanche pour le confort en local, on aimerait bien des traces plus "human readable", c'est là qu'intervient `pino-pretty`

On installe la dépendance :

```shell
$ npm i pino-pretty
```

On peut maintenant configurer le logger en modifiant `src/server.js` :

```javascript
…
const fastify = Fastify({
  logger: {
    prettyPrint: {
      translateTime: true,
      ignore: 'pid,hostname,reqId,responseTime,req,res',
    },
  },
})
…
```

Maintenant il ne reste plus qu'à tester : comme on est en `watch`, pas besoin de faire quoi que ce soit, à part relancer une requête

```
[2022-…] INFO: Server listening at http://127.0.0.1:3000
[2022-…] INFO: incoming request
[2022-…] INFO: request completed
```

# Etape 4

> [Code source](https://github.com/NijiDigital/clean-nodejs-web-backend/tree/step4)

## Un composant pour centraliser la configuration

Dans notre projet, un certain nombre d'éléments font actuellement l'objet de "hardcoding", ce n'est pas une bonne pratique et cela pose problème si par exemple la base mongodb change de port ou d'adresse, ou encore simplement de nom. Par ailleurs, notre serveur écoute forcément sur le port 3000, si l'environnement cible ne le permet pas, dommage.

Pour toutes ces raisons, il est temps de se doter d'une "confguration" qui apportera :
- la possibilité de modifier des valeurs utilisées dans notre appli sans changer le code
- la possibilité de définir ces valeurs en fonction de l'environnement au moment du run

Une réponse simple à ce besoin consiste à utiliser :
1. Des variables d'environnement pour toutes les valeurs de configuration
2. Un objet JavaScript représentant l'image de la configuration dans notre code (avec des valeurs typées contrairement aux variables d'environnement qui ne peuvent être que des strings)

Ainsi on spécifie les particularités dans des variables d'environnement, et notre composant de configuration construit la configuration finale.

Pour gérer les variables d'environnement, nous utiliserons le module `dotenv` qui est intéressant car il permet en plus de déclarer simplement nos variables d'environnement dans un fichier `.env` qui sera évidemment ignoré par Git grâce à notre `.gitignore`.

Installation de la dépendance :

```shell
$ npm i dotenv
```

Maintenant on peut créer le nouveau composant `src/config.js` :

```javascript
import 'dotenv/config'

const config = {
  port: process.env.PORT || 3000,
  mongoDbUrl: process.env.MONGO_DB_URL || 'mongodb://localhost:27017/prod-db',
}

export default config
```

Adapter `src/server.js` pour loguer la configuration et s'assurer qu'elle est bien alimentée :

```javascript
import Fastify from 'fastify'
import config from './config'
import dbConnectorPlugin from './plugins/db-connector-plugin'
…
console.log('config :', config)
fastify.register(dbConnectorPlugin)
…
```

> Remarque : on utilise ici `console.log` en guise de logger temporaire de mise au point, ce code doit disparaître au profit d'un logger plus industriel (naturellement, celui de fastify)

Résultat :

```
config : { port: 3000, mongoDbUrl: 'mongodb://localhost:27017/prod-db' }
```

Il faut maintenant faire en sorte de rendre cette configuration disponible pour le reste de notre application, en évitant de faire un couplage fort pour faciliter la réutilisation (on évite d'importer le composant directement partout où on en a besoin).

Pour cela, on modifie `src/server.js` pour utiliser un mécanisme de "décoration" élégant fourni par fastify, remplacer la ligne `console.log` par :

```javascript
fastify.decorate('config', config)
console.log('config :', fastify.config)
```

> Le résultat dans la console devrait être le même

Il suffit à ce stade d'utiliser la propriété `config` fournie par fastify dans les sources qui en ont besoin.

Dans `src/plugins/db-connector-plugin.js` remplacer :

```javascript
    url: 'mongodb://localhost:27017/dev-db',
```

par :

```javascript
    url: fastify.config.mongoDbUrl,
```

Et vérifier que l'application est toujours opérationnelle en refaisant une requête HTTP GET /animals.

Vous devriez obtenir :

```shell
$ http :3000/animals
HTTP/1.1 200 OK
…
content-type: application/json; charset=utf-8

[]
```

Ce qui est normal car la base utilisée par l'application est celle par défaut de notre configuration : `prod-db`

Pour rétablir le service, il suffit de spécifier la base de développement dans `.env` :

```
# Base Mongo DB de développement
MONGO_DB_URL=mongodb://localhost:27017/dev-db
```

Résultat :

```shell
$ http :3000/animals
HTTP/1.1 200 OK
…
content-type: application/json; charset=utf-8

[
    {
        "_id": "623ed2036d656e61c26e542a",
        "name": "Roger",
        "type": "Rabbit"
    },
    {
        "_id": "623ed2116d656e61c26e542c",
        "name": "Peppo",
        "type": "Cat"
    },
    {
        "_id": "623ed21b6d656e61c26e542e",
        "name": "Pluto",
        "type": "Dog"
    }
]
```

Il reste à rendre le port d'écoute du serveur configurable, avec la même approche.

Remplacer dans `src/server.js` :

```javascript
    await fastify.listen(3000)
```

par :

```javascript
    await fastify.listen(config.port)
```

Et on en profite pour changer notre trace de la configuration :

```javascript
console.log('config :', fastify.config)
```

devient :
```javascript
fastify.log.debug(`config : ${JSON.stringify(config)}`)
```

On constate que la trace a disparu, c'est normal car on utilise maintenant un `log level`, il faut maintenant rendre cette information configurable pour pouvoir la définir au "runtime" (avec la même approche : une variable d'environnement).

Modifier `src/config.js` pour avoir :

```javascript
const config= {
  port: process.env.PORT || 3000,
  logLevel: process.env.LOG_LEVEL || 'error',
  mongoDbUrl: process.env.MONGO_DB_URL || 'mongodb://localhost:27017/prod-db',
}
```

Puis `src/server.js` pour avoir :

```javascript
…
const fastify = Fastify({
  logger: {
    level: config.logLevel,
    prettyPrint: {
…
```

A ce stade, plus aucune trace n'apparaît, c'est encore normal puisque notre log level par défaut est `error` (pour s'en convaincre il suffit d'injecter n'importe quelle erreur dans le code et constater qu'elle est tracée ^^).

Il nous reste donc à définir le log level que l'on souhaite pour un environnement de développement dans notre fichier `.env` :

```
# Log lebvel
LOG_LEVEL=debug

# Base Mongo DB de développement
MONGO_DB_URL=mongodb://localhost:27017/dev-db
```

Résultat dans les traces :

```
[2022-…] DEBUG: config : {"port":3000,"logLevel":"debug","mongoDbUrl":"mongodb://localhost:27017/dev-db"}
[2022-…] INFO: Server listening at http://127.0.0.1:3000
```

Et voilà !

Nous avons mis en oeuvre un serveur backend NodeJS en utilisant le minimum de dépendances pour servir nos besoins :
- Router les requêtes de manière élégante
- Utiliser une syntaxe de code moderne
- Mettre en place une boucle [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) confortable pour développer, privilégiant les petites itérations
- Proposer une gestion de configuration simple et robuste
- Proposer un cadre propre et extensible, sans être surdimensionné

Le but de cet exercice est aussi de prendre conscience qu'il y a un écart non négligeable entre faire une application rapide (par exemple avec express) avec un "sample" de quelques lignes, et investir dans un "boilerplate" qui pourra résister lorsque l'application va grossir : "le diable est dans les détails" 😄

Une suite à donner à ce "hands-on" qui pourrait être intéressante serait de déployer cette application dans un environnement cible, par exemple un service cloud  / PaaS du type [Heroku](https://heroku.com/) ou [Netlify](https://www.netlify.com/).

Mais c'est une autre histoire… 

> Retrouvez l'ensemble des sources sur [github.com/NijiDigital](https://github.com/NijiDigital/clean-nodejs-web-backend)

