# Comment créer une API gRPC avec NodeJS ?

## Introduction

Lorsque l'on parle d'une **API gRPC**, cela signifie que l'on parle d'une **application Web** qui communique en utilisant le **framework** [gRPC](https://grpc.io/) créé par **Google** en 2016.

Ce framework **multiplateforme** est basé sur les **R**emote **P**rocedure **C**alls, en français *Appels de Procédure à Distance*.  
Ce sont donc des appels à des fonctions qui **ont des effets sur un serveur distant,** mais qui sont exécutées de manière locale **sans que le développeur ait besoin** de développer les détails de l'interaction à distance.

La version **gRPC** diffère de la communication RPC classique en choisissant d'utiliser la **version 2** du protocole réseau **HTTP**, nommée HTTP/2. Une version majeure créée en 2015 qui améliore la vitesse du transport des données et offre la possibilité d'une communication **bidirectionnelle.**

La **différence majeure** entre *RPC* et *gRPC* se trouve dans le format utilisé pour transporter les données. Il s'agit de l'utilisation de **Protocol Buffers**, un format de donnée **open source** développé par Google.  
Ce format permet de **sérialiser** des données structurées en un paquet compact **rétro compatible** mais aussi compatible avec les **futures versions**.

Ces données sont faites pour être envoyées **vers un serveur distant** ou stockées, mais ne peuvent pas être compréhensibles sans se rapporter à son **schéma**, contenu dans un fichier avec l'extension "**.proto**".

# Création de l'API

*Le code source de cette étape est disponible* [*ici*](https://github.com/abouteiller-niji/api-node-grpc/tree/proto-step)*.*

Passons maintenant à l'**action**, en commençant par créer un simple **fichier "hello\_world.proto"** qui va contenir le **schéma** de notre API.

```plaintext
syntax = "proto3";

service GreeterService {
  rpc SayHello (SayHelloRequest) returns (SayHelloResponse);
}

message SayHelloRequest {
  string name = 1;
}

message SayHelloResponse {
  string message = 1;
}
```

Pour le moment, le fichier contient le **strict minimum** pour être **exécuté** côté serveur et être **appelé** côté client.

### Comprendre le schéma Protocol Buffers

`syntax = "proto3"` permet de spécifier que le schéma utilise la **version 3** de Protocol Buffers. Si cette ligne n'est pas présente, le compilateur se comportera comme s'il s'agissait de la version 2 `"proto2"`. La version `"proto1"` existait avant que Protocol Buffers soit open source, mais est désormais **dépréciée.**

`service GreeterService { ... }` englobe les **différentes fonctions** de notre service qui devront être **implémentées coté serveur** et qui pourront être appelées **côté client**. Plusieurs services peuvent exister au sein d'un même fichier d'extension ".proto" en précédant le nom du service par le mot-clé `service`.

`rpc SayHello (SayHelloRequest) returns (SayHelloResponse)` décrit une **fonction** attachée au service parent. Ces fonctions commencent par `rpc` suivi d'un nom écrit en **PascalCase**, comme suggéré dans [le guide de style](https://developers.google.com/protocol-buffers/docs/style#message_and_field_names) de Google.

Le premier mot entre parenthèses `SayHelloRequest` représente la structure de données des **paramètres** qui peuvent être **envoyés** à cette fonction.  
Le second mot entre parenthèses `SayHelloResponse` est précédé par le mot-clé obligatoire `returns` pour indiquer la structure de donnée **renvoyée par la méthode** `SayHello`.

Il est conseillé de reprendre le **nom de la fonction** et de rajouter **"Request"** ou **"Response"** en tant que **suffixe** pour le nom de la structure de donnée, en fonction de son rôle.  
Dans un but de **rétro compatibilité**, ces paramètres doivent être définis pour pouvoir changer dans le futur **sans changement de code nécessaire**.

`message SayHelloRequest { ... }` est la **structure de donnée** utilisée comme **argument** à notre méthode `SayHello`. Celle-ci est précédée du mot-clé `message`, **obligatoire pour les structures de données**, suivie de son nom en [Pascal Case](https://definitions-digital.com/developpeur/pascal-case).

`string name = 1` est la première propriété de cette structure de donnée. Elle est composée en 3 parties : son ***type***, son ***nom*** et sa ***position*** dans la structure, précédée par un symbole "=", en commençant par la valeur "1".  
De **nombreux types** sont disponibles pour déterminer le contenu de chaque propriété. Vous pouvez trouver [ici](https://developers.google.com/protocol-buffers/docs/proto3#scalar) la liste exhaustive des **types disponibles** dans un message.

`message HelloReply { ... }` est aussi une **structure de donnée** qui est utilisée pour décrire ce que **contient la réponse** de la méthode comme premier argument

### Création de la partie serveur

*Le code source de cette étape est disponible* [*ici*](https://github.com/abouteiller-niji/api-node-grpc/tree/server-step)*.*

Dans le même dossier qui contient votre schéma, initialisez un projet node avec la commande `npm init` en rajoutant `-y` pour ne pas avoir de confirmation supplémentaire.

```bash
npm init -y
```

Deux librairies sont **nécessaires** pour faire une **API gRPC** avec **NodeJS**  
Installez les librairies en utilisant `npm`.

```bash
npm install @grpc/grpc-js @grpc/proto-loader
```

Dans un nouveau fichier **"server.mjs"**, commencez par importer la méthode `loadSync` de "**@grpc/proto-loader**" pour charger le schéma.

```javascript
import { loadSync } from '@grpc/proto-loader'
```

Créez une variable qui va contenir le **chemin relatif** vers le fichier de notre schéma.

```javascript
const PROTO_PATH = './hello_world.proto'
```

Utilisez la méthode `loadSync` avec comme **premier paramètre** le chemin du schéma contenu dans la variable `PROTO_PATH`.  
Le **deuxième paramètre** de cette fonction permet de définir des **options** pour récupérer les différentes informations du schéma **formaté d'une certaine façon**. Liste exhaustive disponible [ici](https://github.com/grpc/grpc-node/blob/master/packages/proto-loader/README.md).

```javascript
const packageDefinition = loadSync(PROTO_PATH, {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
})
```

Pour utiliser le **contenu du schéma** après l'avoir chargé, une autre fonction doit être appelée : `loadPackageDefinition`. Cette fonction est disponible dans la seconde librairie que nous avons installée au préalable `@grpc/grpc-js`.

```javascript
import { loadPackageDefinition } from "@grpc/grpc-js"
```

Pour l'utiliser, il suffit d'ajouter la **variable** `packageDefinition` qui contient le **résultat** du chargement du schéma, en tant qu'argument de celle-ci.

```javascript
const hello_proto = loadPackageDefinition(packageDefinition)
```

Passons à la partie démarrage du serveur en créant une fonction `startServer`.

```javascript
function startServer() {}
```

Celle-ci va permettre de **créer un serveur, d’ajouter un service, puis de le démarrer** sur un port prédéfini. Ces actions nécessitent **deux nouveaux imports** `Server` et `ServerCredentials` de la librairie `@grpc/grpc-js` qui viennent se rajouter à la fonction `loadPackageDefinition`.

```javascript
import {
  loadPackageDefinition,
  Server,
  ServerCredentials,
} from "@grpc/grpc-js"
```

Créez une **instance du serveur** en l'appelant sans argument, bien que de [nombreux paramètres existent](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js#supported-channel-options) et puissent être configurés.

```javascript
function startServer() {
  const server = new Server()
}
```

Vous pouvez maintenant **ajouter un service** à l'instance de notre serveur en appelant `addService`. Il faut passer en **premier argument** la propriété `service` d'un service défini dans notre schéma.

```javascript
function startServer() {
  const server = new Server()
  server.addService(hello_proto.GreeterService.service, { sayHello })
}
```

Son **second argument** est un objet qui déclare l'implémentation de **chaque fonction** de ce service. Créez cette **fonction** `sayHello` en dehors du scope de la fonction `startServer`.

La fonction devra être composée de **deux paramètres** `call` et `callback` qui serviront respectivement à **récupérer un ou des éléments** de la requête et de **retourner un ou des éléments** dans la réponse.

```javascript
const sayHello = (call, callback) => {
  const { name } = call.request
  callback(null, { message: `Bonjour ${name}` })
}
```

Le paramètre `name` définit en tant que paramètre de la fonction `sayHello` dans notre schéma peut être récupéré en [décomposant](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) l'objet `request`.  
Puis on appelle la fonction callback pour envoyer une réponse avec **deux paramètres**: le premier pour l'**erreur**, le second pour le **détail** de la réponse.

Ici le premier argument est `null` car il n'y a pas d'erreur. Le second est un objet qui correspond à **SayHelloResponse** décrit dans le schéma "**hello\_world.proto**".

Maintenant que le serveur possède un **service**, il faut le démarrer.  
Le démarrage du serveur se fait en **deux temps**. D'abord en faisant appel à la méthode `bindAsync`. Puis dans la fonction **callback** de cette méthode, il faut appeler la méthode `start` de ce même serveur pour qu'il soit finalement démarré.

```javascript
function startServer() {
  const server = new Server()
  server.addService(hello_proto.GreeterService.service, { sayHello })
  const PORT = 3000
  const host = `localhost:${PORT}`
  server.bindAsync(host, ServerCredentials.createInsecure(), (error) => {
    if (error) {
      console.log("An error has occurred in bindAsync", error)
      return
    }
    server.start()
    console.log(`Server listening on: ${host}`)
  })
}
```

Le premier argument `` `localhost:${PORT}` `` est un *template string* pour désigner l'adresse d'écoute qui se compose d'un **hostname** et d'un **port**. Le port choisi doi être **disponible** sur votre réseau local. Dans le cadre de ce tutoriel, le port **3000** est choisi et donc assigné à une variable **PORT**.

Le deuxième argument `ServerCredentials.createInsecure()` est nécessaire pour spécifier la sécurité du serveur. Il est possible d'utiliser l'une des deux méthodes **statiques** de la classe ServerCredentials: **createInsecure** ou **createSsl**.

Le troisième argument `(error) => { ... }` permet d'inscrire une **fonction callback** pour exécuter d'autres fonctions **après vérification** que le serveur puisse bien démarrer avec les informations renseignées.  
Cette fonction peut **potentiellement** recevoir une **erreur** en premier argument. Après avoir passé la condition qu'il n'y ait pas d'erreur, on peut exécuter la méthode `start` de `server` avec plus de sécurité.

Le serveur est désormais configuré. Pour s'assurer que tout se déroule bien, il suffit de faire appel à la fonction `startServer` après avoir déclaré la fonction.

```javascript
startServer()
```

Puis d'exécuter dans un terminal, la commande `node` sur le fichier `server.mjs`.

```javascript
node server.mjs
```

Si tout s'est bien déroulé de votre côté, vous devriez voir ce résultat dans la console de votre terminal.

```bash
Server listening on: localhost:3000
```

### Création de la partie client

*Le code source de cette étape est disponible* [*ici*](https://github.com/abouteiller-niji/api-node-grpc/tree/client-step)*.*

Pour commencer à faire des requêtes clients vers notre tout nouveau serveur, il faut d'abord créer un nouveau fichier: **client.mjs**

Le début de ce fichier **ressemble** à la partie serveur, car il s'agit de la récupération et du chargement du contenu de notre schéma **Protocol Buffers**.  
On retrouve donc d'abord les **deux imports** nécessaires `loadSync` et `loadPackageDefinition`

```javascript
import { loadSync } from "@grpc/proto-loader"
import { loadPackageDefinition } from "@grpc/grpc-js"
```

Puis on les utilise de la **même façon** que dans la partie serveur, en définissant le chemin relatif du schéma à utiliser.

```javascript
const PROTO_PATH = "./hello_world.proto";
const packageDefinition = loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const hello_proto = loadPackageDefinition(packageDefinition);
```

Il faut maintenant développer la fonction principale qui s'appellera aussi simplement que celle dans la partie serveur: `startClient`.  
À l’intérieur, on y retrouve l'utilisation du service récupéré depuis le schéma chargé.

```javascript
function startClient() {
  const service = new hello_proto.GreeterService(
    "localhost:3000",
    credentials.createInsecure()
  )
}
```

Le service récupéré est **au même nom** que celui inscrit dans le schéma: `GreeterService`. Il est appelé avec deux paramètres : **son adresse d'écoute** ainsi que les **informations de sécurité** nécessaires pour s'y connecter.

Rappelez-vous, nous avons **choisi de ne pas sécuriser** la connexion. Il faut donc envoyer des informations du **même type** avec la fonction `createInsecure` après avoir importé `credentials` de la librairie npm déjà installée `@grpc/grpc-js`.

```javascript
import { credentials } from "@grpc/grpc-js";
```

Maintenant que le service est récupéré, vous pouvez appeler ses fonctions.  
La seule fonction déclarée dans notre schéma et implémentée côté serveur est `SayHello`.

```javascript
function startClient() {
  ...
  service.SayHello({ name: "Aurélien" })
}
```

Cette fonction est contenue dans le service et appelée avec comme **premier argument** un objet pour envoyer **un ou plusieurs paramètres**. Notre fonction `SayHello` n'a besoin que d'un paramètre `name`, il est donc seul dans cet objet.

À ce moment-là, la requête a bien été envoyée mais quid de la réponse ?  
Pour savoir ça, il faut rajouter un second paramètre à la méthode `SayHello`.

```javascript
function startClient() {
  ...
  service.SayHello({ name: "Lecteur" }, (error, result) => {
    if (error) {
      console.error("An error has occured", error)
      return
    }
    const { message } = result
    console.log(message)
  })
}
```

Ce second paramètre est une fonction qui agit comme **callback** après que la requête a bien été envoyée. On y retrouve deux paramètres: `error` et `result`.

Après avoir rajouté une condition qui récupère une erreur s’il y en a, on peut récupérer le contenu du résultat qui contient notamment une propriété message.

On affiche simplement le contenu du message créé par le serveur et récupéré par le client avec un `console.log`.

Maintenant on peut vérifier le fonctionnement de bout en bout, en ajoutant l'appel à la fonction `startClient`.

```javascript
startClient()
```

On exécute dans un nouveau terminal, la commande node sur le fichier **client.mjs**.

```javascript
node client.mjs
```

Après exécution, la console devrait afficher ce résultat dans votre terminal.

```bash
Bonjour Lecteur
```

## Bonus : TypeScript compatible.

*Le code source de cette étape est disponible* [*ici*](https://github.com/abouteiller-niji/api-node-grpc/tree/typescript-step)*.*

Si par chance tout fonctionne bien du premier coup, c'est parce que nous étions encore seulement en JavaScript. Pour continuer l'API gRPC avec plus de confiance dans le développement, passons à TypeScript.

"*Alors pourquoi ne pas l'avoir fait dès le début ?*", me direz-vous.  
Simplement parce que le framework gRPC n'a pas été conçu pour TypeScript mais pour NodeJS qui utilise donc seulement du JavaScript.  
Vous pouvez voir la liste complète [ici](https://grpc.io/docs/languages/) des langages supportés nativement par gRPC.

### Génération de l'interface TypeScript

Le schéma que nous avons créé peut être utilisé pour générer du code compilé dans de **multiples langages de programmation**. JavaScript et TypeScript ne font pas partie de cette liste, car ils n'ont pas besoin de **code compilé** pour fonctionner.

Afin d'obtenir la puissance des **types statiques** de TypeScript, un outil existe pour **générer des interfaces** TypeScript en se basant sur notre fichier "**hello\_world.proto**".  
Cet outil est disponible dans la librairie npm `@grpc/proto-loader` installé précédemment.

Utilisez `npx` sur l'outil `proto-loader-gen-types` avec le nom du fichier de notre schéma Protocol Buffers "**hello\_world.proto**".

```bash
npx proto-loader-gen-types hello_world.proto --longs=String --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --outDir=proto/
```

Les paramètres `--longs`, `--enums`, `--defaults`,`--oneofs` doivent avoir les mêmes valeurs que ceux utilisés dans votre code.

Le paramètre suivant `--grpcLib` permet de choisir la librairie `@grpc/grpc-js`, déjà installée, pour que les types générés se basent sur la même implémentation.

Le dernier paramètre `--outDir` permet de choisir le dossier où vous souhaitez extraire les interfaces et types qui vont être générés.

Si la génération des types s'est bien passée, vous devriez voir 4 nouveaux fichiers dans un nouveau dossier nommé "**proto**".

### Utilisation des types

Changez désormais l'extension des fichiers **server.mjs** et **client.mjs** en "*.ts"*

Le principal changement à faire pour appliquer la détection des types se fait sur le chargement du proto.

Il faut rajouter `as unknown as ProtoGrpcType` après l'utilisation de `loadPackageDefinition` que ce soit dans le fichier "**server.ts**" ou le fichier "**client.ts**".

```typescript
const hello_proto = loadPackageDefinition(packageDefinition) as unknown as ProtoGrpcType
```

Spécifier `as unknown` est **nécessaire,** car les types associés à la fonction `loadPackageDefinition` ne peuvent être au courant des propriétés de notre type tout juste généré.

Puis pour la **partie serveur**, il suffit d'utiliser l'interface `GreeterServiceHandlers` qui se nomme d'après le service spécifié (**GreeterService**) avec le suffixe "**Handlers**".  
Cela permet ensuite de sélectionner le nom de la méthode à implémenter dans cet exemple, il s'agit de "**SayHello**".

```typescript
import { GreeterServiceHandlers } from "./proto/GreeterService"
...
const sayHello: GreeterServiceHandlers["SayHello"] = ...
```

Rajouter cette interface permet une **sécurité supplémentaire** afin de récupérer les bonnes propriétés qui peuvent être dans la requête, ainsi qu'associer des types à chacune de ces valeurs.

### Un environnement typé

Maintenant que le code a bien été changé, il faut ajouter des nouvelles librairies à l'environnement pour exécuter ce code typé.

```bash
npm install -D typescript ts-node
```

Il reste à tester le nouveau code, en utilisant cette fois-ci la commande `ts-node` au lieu de `node` et `npx` pour utiliser la nouvelle librairie.

```bash
npx ts-node server.ts
npx ts-node client.ts
```

## Conclusion

En utilisant une **API gRPC** avec **NodeJS**, il est possible d'appeler des fonctions d'une **manière simple et efficace**. Tout se passe directement dans les fonctions renseignées par des services.

La découverte des fonctionnalités est **autodescriptive**, ce qui permet d'éviter de passer par une documentation écrite **externe à l'implémentation** pour connaître les fonctionnalités disponibles. Une documentation écrite est **toujours importante** pour renseigner plus d'informations, avec des exemples différents.

### Avantages

**Rendement élevé :** gRPC utilise le format *Protocol Buffers* pour communiquer **plus efficacement** entre les clients et les serveurs, comparé aux API REST qui utilisent traditionnellement des objets JSON. Dans **un système IoT**, ce besoin d'efficacité est souvent présent pour **communiquer le minimum de données** nécessaires.

**Indépendant** : Protobufs n'a pas besoin d'utiliser un langage de programmation ou un système spécifique pour **sérialiser les données** en binaire. Ce besoin peut être présent si l'implémentation du côté client/serveur est prévue pour être sur des **systèmes différents** (Web/Mobile/PC) ou utilisé avec des **langages de programmation différents** comme cela peut se faire dans une **architecture microservice**.

**Streaming inclus :** Bien que ce tutoriel ne couvre pas une utilisation streaming, celle-ci est **utilisable nativement** avec ce framework. Une requête peut donc être faite de [plusieurs façons différentes](https://grpc.io/docs/what-is-grpc/core-concepts/#service-definition) :

* Requête unique et réponse unique appelée "**Unary**", comparable à REST.
    
* Requête unique pour recevoir un flux de données (**Server streaming**).
    
* Flux de données vers le serveur qui répond une fois (**Client streaming**).
    
* Flux de données vers le serveur qui répond aussi en un flux de données (**Streaming bidirectionnel**).
    

### Inconvénients

**Langages de programmation limités** : Bien que son indépendance au langage de programmation soit un avantage comme décrit précédemment, il faut que ce langage soit présent dans [cette liste](https://grpc.io/docs/languages/) pour être supporté par gRPC.

**Utilisable seulement avec HTTP/2** : Alors que plus de la majorité des navigateurs internets modernes supporte la version 2 du protocole HTTP, certaines anciennes versions de navigateur peuvent ne pas fonctionner avec gRPC.

**Réutilisation fonctionnelle difficile** : En fonction de l'implémentation et donc du langage de programmation utilisé, réutiliser le même code **peut s'avérer compliqué**. Il faudra **changer le code implémenté** pour qu'il soit compatible avec le nouvel environnement.

**Mutation non prévisible** : Comme une API gRPC **n'utilise pas les méthodes standards** de HTTP (GET/POST/DELETE...), **il n'existe pas de manière** pour savoir à l'avance si une fonction a des effets de mutation.

**Promesses non supportées** : Depuis septembre 2019, [une issue](https://github.com/grpc/grpc-node/issues/54) est ouverte pour réclamer **l'intégration des Promesses** et de la syntaxe **async/await** qui ne sont toujours pas intégrés nativement dans le framework pour NodeJS.

### Dans quels cas utiliser gRPC ?

Après avoir vu les avantages/inconvénients d'une API de type gRPC. Certaines situations peuvent profiter de l'utilisation de gRPC :

* Lorsque le besoin de **communiquer nativement en streaming** avec HTTP, en utilisant un framework **moderne**.
    
* Dans une architecture **microservices**, pour communiquer **efficacement** avec plusieurs services en **un seul client HTTP**.
    
* L'utilisation de **FieldMask**, un type défini par *Protocol Buffers* pour éviter de requêter certaines informations non demandées, comme Netflix l'a [ici](https://netflixtechblog.com/practical-api-design-at-netflix-part-1-using-protobuf-fieldmask-35cfdc606518) implémenté. Une fonctionnalité que l'on trouve aussi nativement dans [GraphQL](https://fr.wikipedia.org/wiki/GraphQL).
    

### Alors faut-il changer de REST à gRPC ?

Dans le cadre d'une application web classique FullStack NodeJS avec TypeScript, ma réponse est : **non**.

D'autres solutions sont plus adaptées à TypeScript telles que [tRPC](https://github.com/trpc/trpc) qui reprend **la simplicité** de RPC, présentée ici avec gRPC, mais conçu spécifiquement pour **TypeScript**. Cela permet notamment de **ne pas avoir de schéma intermédiaire** et d'utiliser directement les types déclarés côté serveur depuis le côté client.

Il existe aussi **GraphQL**, une solution moderne, qui permet d'avoir une sélection précise des données que l'on souhaite recevoir du serveur, à l'instar de **FieldMask** présent dans gRPC.  
C'est aussi une bonne solution pour être utilisée dans une architecture microservices, grâce à une API Gateway.
