Bun : Un serveur Web ???
Tags: bunjs, webserver, websockets, slidesk
Published: 2023-09-13 23:17:28

Dans l'[article précédent](https://dev.to/gouz/bun-fichiers-et-repertoires-1363), nous avons vu comment manipuler les fichiers et répertoires avec _Bun_.

Dans ce nouvel article, nous allons voir comment créer un serveur web avec _Bun_.

Un serveur web, finalement c'est quoi ? C'est un passe-plat de fichiers qui peuvent être créés à la volée ou disponible dans des répertoires.

Nous allons aussi voir comment associer à ce serveur web un serveur de WebSockets qui permettra d'avoir des interactions simplifiées avec les clients.

Dans un troisième temps, nous verrons comment gérer les routes avec _Bun_.

Et pour terminer nous verrons comment [_SliDesk_](https://github.com/gouz/slidesk) utilise le serveur pour afficher la présentation, et comment l'application utilise le serveur de WebSockets pour la synchronisation avec les notes et le contrôle de la présentation.

## Coder un server en quelques lignes

En ces quelques lignes,

```js
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Bonjour le monde !");
  },
});
```

_Bun_ crée un serveur web qui écoute sur le port `3000`.

Le paramètre `request` est de type `Request` comme défini dans la Fetch API.

https://developer.mozilla.org/en-US/docs/Web/API/Request permet d'avoir un aperçu détaillé des propriétés de cet objet.

Donc grace à l'objet `request`, on peut détecter quelle url est demandée et donc faire le traitement associé.

La propriété `fetch` du paramètre de la fonction `Bun.serve` doit retourner un objet `Response` détaillé ici également https://developer.mozilla.org/en-US/docs/Web/API/Response.

La définition de la fonction `serve` est la suivante :

```js
interface Bun {
  serve(options: {
    fetch: (req: Request, server: Server) => Response | Promise<Response>,
    hostname?: string,
    port?: number,
    development?: boolean,
    error?: (error: Error) => Response | Promise<Response>,
    tls?: {
      key?:
        | string
        | TypedArray
        | BunFile
        | Array<string | TypedArray | BunFile>,
      cert?:
        | string
        | TypedArray
        | BunFile
        | Array<string | TypedArray | BunFile>,
      ca?: string | TypedArray | BunFile | Array<string | TypedArray | BunFile>,
      passphrase?: string,
      dhParamsFile?: string,
    },
    maxRequestBodySize?: number,
    lowMemoryMode?: boolean,
  }): Server;
}

interface Server {
  development: boolean;
  hostname: string;
  port: number;
  pendingRequests: number;
  stop(): void;
}
```

Nous pouvons constater que seule la méthode `fetch` est obligatoire. Il est également possible de spécifier :

- `hostname` : si l'on souhaite autre chose que `localhost`;
- `port` : par défaut à `80`, il est possible de donner une autre valeur;
- `development` : si cette valeur est à `true`, un affichage détaillé est disponible lors d'une erreur;
- `error` : dans le cas d'une erreur (page 404 par exemple), il est possible de préciser un comportement
- `tls` : afin de naviguer de manière sécurisée, nous devons spécifier les informations de certificats.

Par exemple :

```js
Bun.serve({
  fetch(req) {
    return new Response("Hello!!!");
  },
  tls: {
    key: Bun.file("./key.pem"), // path to TLS key
    cert: Bun.file("./cert.pem"), // path to TLS cert
    ca: Bun.file("./ca.pem"), // path to root CA certificate
  },
});
```

Finalement, faire un serveur avec _Bun_ est simple.

## "Chaussette, j'ai dit chaussette" - Le nain - 2001

J'ai connu un temps que les moins de 20 ans ne peuvent pas connaître, le monde sans WebSockets, un monde où l'on utilisait les XHR pour communiquer avec le serveur sans rafraichir la page.

Heureusement ce monde est révolu et les WebSockets sont arrivées, simplifiant grandement la communication entre les clients et le serveur.

Reprenons notre exemple si dessus :

```js
const server = Bun.serve({
  port: 3000,
  fetch(request) {
    return new Response("Bonjour le monde !");
  },
});
```

Nous souhaitons utiliser l'url `ws://localhost:3000` pour établir le canal de communication entre les clients et le serveur.

Pour cela, il faut modifier notre fonction `fetch` pour qu'elle puisse changer de protocole en fonction du canal utilisé (http(s) ou ws(s)).

```js
const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va retourner une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    return new Response("Bonjour le monde !");
  },
});
```

Aussi nous allons ajouter une propriété `websocket` à l'objet passé en paramètre de la fonction qui nous permettra de gérer les connexions, les disconnexions, les messages.

```js
const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va renvoyer une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    return new Response("Bonjour le monde !");
  },
  websocket: {
    open(ws) {
      ws.subscribe("mongroupe");
    },
    message(ws, message) {
      // on transmet le message à tout le groupe
      ws.publish("mongroupe", message);
    },
    close(ws) {
      ws.unsubscribe("mongroupe");
    },
  },
});
```

Juste avec les quelques lignes présentes ci-dessus, nous obtenons un serveur web et un serveur websocket performant.

A titre comparatif, sur son [site](https://bun.sh), _Bun_ montre un comparatif de ses performances avec les deux autres runtimes principaux du marché.


![Server-side rendering React benchmark](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uoahs0dlh48j9pxnn9u0.png)


![WebSocket chat server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkv4c520yshcoumc98st.png)

![WebSocket chat server](image.png)

## Comment gérer les routes ?

Comme nous pouvons le constater, le paramètre à fournir à la fonction `serve` n'a qu'une seule propriété `fetch` qui traite l'ensemble des requêtes web (http(s) et ws(s)).

Une des premières solutions seraient un traitement de la donnée `request`. Comme vu précédemment dans l'article, `request` est de type `Request` et dispose donc d'une propriété `url` qui nous permet de savoir quelle route nous souhaitons afficher.

Ainsi le code suivant peut être envisagé :

```js
const server = Bun.serve({
  port: 3000,
  fetch(request, server) {
    if (server.upgrade(request)) {
      // Bun va renvoyer une "101 - Switching Protocols"
      // si la mise à jour se passe bien
      return undefined;
    }
    switch (request.url) {
      case "/":
        return new Response("index");
      case "/search":
        return new Response("page de recherche");
      // ...
      default:
        return new Response("Bonjour le monde !");
    }
  },
  websocket: {
    open(ws) {
      ws.subscribe("mongroupe");
    },
    message(ws, message) {
      // on transmet le message à tout le groupe
      ws.publish("mongroupe", message);
    },
    close(ws) {
      ws.unsubscribe("mongroupe");
    },
  },
});
```

Quand on a quelques routes, cette solution peut convenir, mais elle n'est pas dynamique.

Pour cela, _Bun_ a implémenté un [système de routes "à la Next.js"](https://bun.sh/docs/api/file-system-router).

Prenons la structure de fichiers suivante :

```
pages
├── index.tsx
├── settings.tsx
├── blog
│   ├── [slug].tsx
│   └── index.tsx
└── [[...catchall]].tsx
```

_Bun_ dispose d'une classe `FileSystemRouter` qui permet ce traitement.

Ainsi nous pouvons configurer le système de la manière suivante :

```js
const router = new Bun.FileSystemRouter({
  style: "nextjs",
  dir: "./pages",
  origin: "http://localhost",
  assetPrefix: "_next/static/",
});
```

Nous constatons qu'un paramètre `style` existe, ce qui sous-entend que _Bun_ souhaite faire évoluer cette classe pour gérer d'autres modèles de gestion automatisée de routes.

Une fonction `match` permet de diriger correctement le traitement.

```js
router.match("/");
```

Nous pouvons l'intégrer dans notre exemple de la partie précédente de la manière suivante.

Créons un répertoire de travail et entrons dans ce répertoire.

```sh
mkdir myserver
cd myserver
```

Utilisons la commande `bun init` pour instancier le projet.

```sh
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (myserver):
entry point (index.ts): index.js

Done! A package.json file was saved in the current directory.
 + index.js
 + .gitignore
 + jsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.js
```

Ajoutons les bibliothèques `react` et `react-dom` :

```sh
bun add react react-dom
```

Modifions le fichier `index.js` par le contenu suivant :

```js
#!/usr/bin/env bun
import { renderToReadableStream } from "react-dom/server";

const router = new Bun.FileSystemRouter({
  style: "nextjs",
  dir: "./pages",
});

Bun.serve({
  port: 3000,
  async fetch(request) {
    const match = router.match(request.url);
    const Component = await import(match.filePath);
    const stream = await renderToReadableStream(<Component.default />);

    return new Response(stream, {
      headers: { "Content-Type": "text/html; charset=utf-8" },
    });
  },
});
```

Créons un fichier `pages/index.js` :

```js
import React from "react";
export default function Index() {
  return <p>Hello world!</p>;
}
```

Lançons notre serveur :

```sh
bun index.js
```

Allons sur notre navigateur préféré sur l'url `http://localhost:3000` et là, la magie opère, nous obtenons une page avec écrit "Hello world!".

Cette solution est bien plus évoluée et utilise toute la puissance de React pour faire une application SSR (Server Side Rendering).

## Et SliDesk dans tout ça ?

_SliDesk_ a besoin d'un serveur web et d'un serveur websocket pour afficher la présentation et la vue du présentateur, et également les faire interagir entre elles.

Le code du serveur est proche de la première méthode proposée. En effet _SliDesk_ n'est pas une SPA (Single Page Application), elle ne dispose pas de contenu qui s'affiche en fonction d'action, de paramètre. Il n'y a pas de base de données derrière. Il suffit "juste" de servir une page `.html` qui contient le css et le js nécessaire au bon fonctionnement. Seules les images sont les chemins inconnus.

Ainsi avec le code suivant, il est possible de gérer le multilingue (une page par langue), la page de notes et le serveur WebSockets qui permet de naviguer de manière synchronisée entre les "viewers".

```js
import {
  langPage,
  defaultPage,
  favicon,
  notePage,
  webSockets,
  defaultAction,
} from "../utils/server";

const { log } = console;

export default class Server {
  static create(html, options, path) {
    globalThis.html = html;
    globalThis.path = path;
    // eslint-disable-next-line no-undef
    globalThis.server = Bun.serve({
      port: options.port,
      fetch(req) {
        const url = new URL(req.url);
        if (url.pathname.match(/^\/--(\w+)--\/$/g))
          return langPage(url.pathname);
        switch (url.pathname) {
          case "/":
            return defaultPage();
          case "/favicon.svg":
            return favicon();
          case "/notes":
            return notePage(options);
          case "/ws":
            return webSockets(req);
          default:
            return defaultAction(req, options);
        }
      },
      websocket: {
        open(ws) {
          ws.subscribe("slidesk");
        },
        message(ws, message) {
          ws.publish("slidesk", message);
        },
        close(ws) {
          ws.unsubscribe("slidesk");
        },
      },
    });
    if (options.notes)
      log(
        `Your speaker view is available on: \x1b[1m\x1b[36;49mhttp://${options.domain}:${options.port}/notes\x1b[0m`
      );
    log(
      `Your presentation is available on: \x1b[1m\x1b[36;49mhttp://${options.domain}:${options.port}\x1b[0m`
    );
    log();
  }

  static setHTML(html) {
    globalThis.html = html;
    globalThis.server.publish("slidesk", JSON.stringify({ action: "reload" }));
  }

  static send(action) {
    globalThis.server.publish("slidesk", JSON.stringify({ action }));
  }
}
```

## Conclusion

Cet article nous a montré comment créer un serveur web et un serveur websocket en gérer des routes.

Le prochain article traitera de la partie `build` de _Bun_.