Article original : How to Create a REST API Without a Server

Si vous êtes un développeur Front-End et que vous souhaitez montrer vos compétences, cela peut être un problème si vous utilisez GitHub pages ou Netlify pour montrer vos applications.

Au lieu de cela, vous pouvez créer une API REST directement dans le navigateur sans avoir besoin d'un serveur. Avec cela, vous pouvez montrer vos compétences dans des applications qui interagissent avec un backend hébergé dans des endroits où vous ne pouvez pas accéder au côté serveur.

Notez que si vous recherchez "API sans serveur", vous pouvez trouver des articles sur le serverless (qui est toujours une sorte de serveur). Cet article est complètement différent et présente une API de navigateur relativement nouvelle. Continuez à lire si vous êtes intéressé.

Table des matières

Qu'est-ce qu'un Service Worker ?

L'API du navigateur qui vous permet de créer des réponses HTTP pures dans le navigateur aux requêtes HTTP s'appelle un Service Worker. Cette API a été principalement créée pour intercepter les requêtes HTTP provenant du navigateur et les servir à partir du cache.

Cela vous permet de créer des applications appelées PWA qui fonctionnent lorsque vous n'avez pas de connexion Internet. Vous pouvez donc les utiliser dans le train, où vous pouvez avoir un Internet instable. Lorsque vous êtes hors ligne, les requêtes HTTP peuvent être stockées et envoyées au serveur réel lorsque vous êtes de nouveau en ligne.

Mais ce n'est pas tout ce que les Service Workers peuvent faire. Avec eux, vous pouvez créer des requêtes HTTP qui n'ont jamais existé. Il peut intercepter n'importe quelle requête HTTP, par exemple lorsque vous ouvrez une image dans un nouvel onglet ou en utilisant AJAX (comme avec l'API fetch).

Comment enregistrer un Service Worker ?

Le service worker doit être écrit dans un fichier séparé (souvent appelé sw.js, mais vous pouvez le nommer comme vous le souhaitez).

L'emplacement de ce fichier est important. Il doit être situé à la racine de votre application, souvent à la racine du domaine.

Pour enregistrer un service worker, vous devez exécuter ce code :

if ('serviceWorker' in navigator) {
  var scope = location.pathname.replace(/\/[^\/]+$/, '/')
  navigator.serviceWorker.register('sw.js', { scope })
    .then(function(reg) {
       reg.addEventListener('updatefound', function() {
         var installingWorker = reg.installing;
         console.log('Un nouveau service worker est en cours d\'installation :',
                     installingWorker);
       });
       // l'enregistrement a fonctionné
       console.log('Enregistrement réussi. La portée est ' + reg.scope);
    }).catch(function(error) {
      // l'enregistrement a échoué
      console.log('L\'enregistrement a échoué avec ' + error);
    });
}

Cela installera un service worker qui peut commencer à intercepter les requêtes HTTP.

NOTE : Le service worker fonctionne uniquement avec HTTPS et localhost.

Comment créer une réponse HTTP de base

L'API du Service Worker est très simple – vous avez un événement appelé fetch et vous pouvez répondre à cet événement avec n'importe quelle réponse :

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);
    if (url.pathname === '/api/hello/') {
        const headers = {
            'Content-Type': 'text/plain'
        };
        const msg = 'Bonjour, Service Worker !'
        event.respondWith(textResponse(msg, headers));
   }
});

function textResponse(string, headers) {
    const blob = new Blob([string], {
        type: 'text/plain'
    });
    return new Response(blob, { headers });
}

Avec ce code, vous pouvez ouvrir l'URL /api/hello/ et il affichera le texte "Bonjour, Service Worker !" en tant que fichier texte.

De plus, une chose importante : si vous voulez utiliser le Service Worker immédiatement après son installation, vous devez ajouter ce code :

self.addEventListener('activate', (event) => {
  event.waitUntil(clients.claim());
});

Normalement, le Service Worker intercepte les requêtes uniquement après que vous avez actualisé la page. Ce code force l'acceptation des requêtes immédiatement après l'installation.

NOTE : Avec le service worker, vous pouvez également intercepter les requêtes envoyées à différents domaines. Si vous avez votre application sur GitHub pages, vous pouvez intercepter les requêtes vers n'importe quel domaine. Parce qu'il n'y a pas de vérifications de domaine, ce code :

await fetch('https://example.com/api/hello').then(res => res.text())

renverra également Bonjour, Service Worker !.

Comment créer un projet de base

Vous allez créer quelque chose de plus utile en créant un projet React avec une authentification utilisateur très simple.

Notez que cela n'est en aucun cas sécurisé, car les informations utilisateur et les mots de passe seront visibles dans le code. Mais cela peut montrer que vous savez comment interagir avec une API dans React.

Configurer Vite

Tout d'abord, vous devez configurer une application React simple avec Vite.

Pour utiliser Vite, vous devez avoir Node.js installé. Si vous ne l'avez pas, vous pouvez lire comment l'installer à partir de cet article.

Ensuite, vous devez exécuter cette commande à partir du terminal :

npm create vite@latest

J'ai choisi le nom auth, React, et JavaScript. Voici le résultat que j'ai obtenu :

2714 Nom du projet : 2026 auth
2714 Sélectionner un framework : 203a React
2714 Sélectionner une variante : 203a JavaScript

Échafaudage du projet dans /home/kuba/auth...

Terminé. Maintenant, exécutez :

  cd auth
  npm install
  npm run dev

Ensuite, vous devez modifier le fichier vite.config.js, afin que Vite sache comment construire le fichier de service worker.

Voici le fichier de configuration que Vite a créé :

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

Vous devez modifier le fichier de configuration pour inclure ce code :

import { join } from "node:path";
import { buildSync } from "esbuild";

export default defineConfig({
  plugins: [
    react(),
    {
      apply: "build",
      enforce: "post",
      transformIndexHtml() {
        buildSync({
          minify: true,
          bundle: true,
          entryPoints: [join(process.cwd(), "src", "sw.js")],
          outfile: join(process.cwd(), "dist", "sw.js"),
        });
      },
    },
  ]
})

Vous devez inclure les deux imports et vous pouvez remplacer la configuration existante par celle ci-dessus. Vous pouvez également ajouter le code dans les accolades dans un tableau de plugins.

Utiliser la bibliothèque Wayne

Ensuite, vous devez créer un fichier Service Worker nommé sw.js. Vous allez utiliser la bibliothèque Wayne au lieu d'écrire les routes vous-même. Cela simplifiera le code.

Tout d'abord, vous devez installer Wayne :

npm install @jcubic/wayne

Ensuite, vous pouvez créer un fichier nommé sw.js (Note : vous avez mis le répertoire "src" dans le fichier vite.config.js, donc vous devez enregistrer le fichier dans ce répertoire).

import { Wayne } from '@jcubic/wayne';

const app = new Wayne();

app.get('/api/hello/', (req, res) => {
   res.text('Bonjour, Service Worker !');
});

Ce code fonctionnera exactement de la même manière que notre exemple précédent.

Installer le Service Worker

Maintenant, la dernière chose que vous devez faire pour configurer votre service worker est de l'enregistrer. Vous pourriez utiliser le code que vous avez vu précédemment, mais maintenant vous allez utiliser une bibliothèque pour cela.

Tout d'abord, vous devez l'installer :

npm install register-service-worker

Et mettre à jour src/main.jsx avec ce code :

import { register } from "register-service-worker";

register(`./sw.js`);

La dernière chose est de construire le projet en exécutant :

npm run build

NOTE : le mode dev ne fonctionnera pas avec le service worker – vous devez construire le projet.

Les instructions pour configurer un Service Worker avec Vite étaient basées sur cet articlee.

Tester sur le serveur Web

Pour tester votre projet, vous pouvez utiliser cette commande :

npx http-server -p 3000 ./dist/

Cela créera un serveur HTTP simple où vous pourrez tester votre application.

NOTE : si vous ouvrez le fichier index.html dans un navigateur (comme avec glisser-déposer), le service worker ne fonctionnera pas. Cela est dû au fait que le protocole file:// a beaucoup de restrictions. C'est pourquoi vous avez besoin d'un serveur web.

Si vous testez l'application dans le navigateur en ouvrant l'URL : http://127.0.0.1:3000, elle exécutera le code qui enregistre le service worker, et vous pourrez immédiatement accéder à notre faux point de terminaison HTTP : http://127.0.0.1:3000/api/hello/. Il devrait afficher le texte :

Bonjour, Service Worker !

NOTE : pour simplifier les tests, vous pouvez ajouter "http-server -p 3000 ./dist/" au fichier package.json dans scripts :

"serve": "http-server -p 3000 ./dist/",

N'oubliez pas que package.json est un fichier JSON, donc vous ne pouvez pas mettre de virgule finale si ce sera le dernier script.

Pour le faire fonctionner, vous devez installer le package :

npm install http-server

Maintenant, vous pouvez exécuter le serveur avec npm run serve.

NOTE : si vous accédez à l'URL : http://127.0.0.1:3000/api/hello (vous pouvez lire ce qu'est 127.0.0.1 dans cet article), vous obtiendrez une erreur de http-server. Cela est dû au fait que la route que vous avez créée dans le service worker utilisait un slash final. Pour corriger cela, vous pouvez ajouter une redirection :

app.get('/api/hello', (req, res) => {
   res.redirect(301, req.url + '/');
});

Comment ajouter l'authentification React

Maintenant, après avoir tout configuré, vous pouvez ajouter un point de terminaison d'authentification réel et le connecter à votre application React.

Créer un jeton JWT

Nous allons utiliser un jeton JWT populaire pour l'authentification. Vous pouvez en lire plus à leur sujet dans cet article.

Tout d'abord, vous devez installer une bibliothèque JWT :

npm install jose

Ensuite, vous devez créer un nouveau fichier nommé jwt.js dans le répertoire src :

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(
  'cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2'
);

const alg = 'HS256';

const jwt = {
    sign: (payload) => {
        return new SignJWT(payload)
            .setProtectedHeader({ alg })
            .setIssuedAt()
            .setIssuer('https://freecodecamp.org')
            .setAudience('https://freecodecamp.org')
            .setExpirationTime('2h')
            .sign(secret)
    },
    verify: async (token) => {
        const { payload } = await jwtVerify(token, secret, {
            issuer: 'https://freecodecamp.org',
            audience: 'https://freecodecamp.org',
        });
        return payload;
    }
};

export default jwt;

Ce code est un module ES qui utilise la bibliothèque de jetons JWT jose pour créer un nouveau jeton, jwt.sign. Il vérifie que le jeton est correct avec jwt.verify, et il retourne également la charge utile, afin que vous puissiez extraire tout ce que vous sauvegardez dans le jeton.

Vous pouvez en lire plus sur la bibliothèque jose à partir de la documentation – les liens vers les docs sont dans le README.

NOTE : En raison de la limitation du Service Worker, nous ne pouvons pas créer une authentification réelle, où le jeton d'accès est stocké dans un cookie (les Service Workers ne permettent pas de créer des cookies) et utiliser des jetons de rafraîchissement pour mettre à jour le jeton d'accès.

Ajouter l'API d'authentification

Maintenant, vous pouvez utiliser les fonctions précédentes pour créer un point de terminaison API :

import jwt from './jwt';

app.post('/api/login', async (req, res) => {
    const { username, password } = await req.json() ?? {};
    if (username === 'demo' && password === 'demo') {
        const token = await jwt.sign({ username });
        res.json({ result: token });
    } else {
        res.json({ error: 'Nom d\'utilisateur ou mot de passe invalide' });
    }
});

Ce code vérifiera que le nom d'utilisateur et le mot de passe sont corrects (tous deux égaux à "demo"), et créera un nouveau jeton JWT. Si le nom d'utilisateur ou le mot de passe ne sont pas corrects, il retournera une erreur.

Ajouter l'authentification à React

Vous avez créé une application React avec Vite, donc vous devez utiliser JSX pour ajouter la logique d'authentification front-end.

Tout d'abord, vous créez une fonction helper qui enverra une requête HTTP au point de terminaison /api/login avec l'API Fetch :

function login(username, password) {
    return fetch('/api/login', {
        method: 'post',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username,
            password
        })
    }).then(res => res.json());
}

Ensuite, vous devez créer un formulaire de base :

<form>
  <div>
    <label for="user">nom d'utilisateur</label>
    <input id="user" />
  </div>
  <div>
    <label for="password">mot de passe</label>
    <input id="password" type="password" />
  </div>
  <button>connexion</button>
</form>

Et ajouter un peu de style :

form {
  display: inline-flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-end;
}

label::after {
  content: ":";
}

label {
  width: 100px;
  display: inline-block;
  text-align: right;
  margin-right: 10px;
}

Ensuite, vous avez besoin d'une fonction d'authentification que vous ajouterez à un événement onSubmit. Vous utiliserez deux variables d'état pour le jeton et l'erreur :

function App() {
  const [token, setToken] = useState(null);
  const [error, setError] = useState(null);

  async function auth(event) {
    event.preventDefault();

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
    } else if (res.error) {
      setError(res.error);
    }
  }

Pour obtenir le nom d'utilisateur et le mot de passe du formulaire, vous pouvez utiliser des refs. Vous pouvez également afficher le formulaire uniquement lorsque le jeton n'est pas défini :

function App() {
  const [token, setToken] = useState(null);
  const [error, setError] = useState(null);
  const userRef = useRef();
  const passwordRef = useRef();

  async function auth(event) {
    event.preventDefault();
    const username = userRef.current.value;
    const username = passwordRef.current.value;

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
    } else if (res.error) {
      setError(res.error);
    }
  }

  return (
    <div>
      <div className="card">
        {!token && (
          <form onSubmit={auth}>
            <div>
              <label for="user">nom d'utilisateur</label>
              <input id="user" ref={userRef}/>
            </div>
            <div>
              <label for="password">mot de passe</label>
              <input id="password" ref={passwordRef} type="password"/>
            </div>
            <button>connexion</button>
          </form>
        )}
        {error && <p className="error">{ error }</p>}
      </div>
    </div>
  );
}

Maintenant, vous pouvez tester l'application. Si vous tapez le nom d'utilisateur et le mot de passe, ils ne se réinitialisent pas.

Vous pouvez corriger cela en définissant la valeur de la ref sur une chaîne vide à la fin de la fonction :

userRef.current.value = '';
passwordRef.current.value = '';

Il y a une autre erreur. Si vous mettez un nom d'utilisateur ou un mot de passe incorrect, vous obtiendrez une erreur. Mais ensuite, si vous tapez le mot de passe correct, l'erreur n'est pas supprimée. Pour corriger ce problème, vous devez réinitialiser l'état de l'erreur lors de la définition du jeton :

  async function auth(event) {
    event.preventDefault();
    const username = userRef.current.value;
    const username = passwordRef.current.value;

    const res = await login(username, password);
    if (res.result) {
      setToken(res.result);
      setError(null);
    } else if (res.error) {
      setError(res.error);
    }
    userRef.current.value = '';
    passwordRef.current.value = '';
  }

La prochaine chose que vous pouvez faire est d'extraire le nom d'utilisateur du jeton. Cela vérifiera également que le jeton est correct dans votre application React. Vous devez utiliser le hook useEffect pour exécuter le code lorsque le jeton change :

import jwt from './jwt';

  // ...
  const [username, setUsername] = useState(null);

  useEffect(() => {
    jwt.verify(token).then(payload => {
      const { username } = payload;
      setUsername(username);
    }).catch(e => {
      setError(e.message);
    });
  }, [token]);

  // ...

Si vous exécutez ce code, vous obtiendrez une erreur : Compact JWS doit être une chaîne ou Uint8Array.

La raison est que le hook useEffect sera déclenché lorsque le jeton est null. Avant de vérifier, vous devez vérifier si le jeton a été défini :

  useEffect(() => {
    if (token !== null) {
      jwt.verify(token).then(payload => {
        const { username } = payload;
        setUsername(username);
      }).catch(e => {
        setError(e.message);
      });
    }
  }, [token]);

Ensuite, vous pouvez afficher le nom d'utilisateur après la connexion de l'utilisateur :

{token && (
  <div>
    <p>Bienvenue {username}</p>
  </div>
)}

Prochaines étapes

La dernière chose que nous pouvons faire est d'enregistrer le jeton dans localStorage et d'ajouter un bouton de déconnexion. Mais cela est laissé comme exercice au lecteur.

Vous pouvez lire à propos de localStorage dans cet article freeCodeCamp.

Vous pouvez améliorer cela et ajouter plus de points de terminaison, comme obtenir des données réelles que vous enregistrerez dans un fichier sw.js. Vous pouvez stocker les données dans IndexedDB, afin qu'elles soient persistantes comme dans une vraie application. Lisez plus sur IndexedDB dans cet article.

IndexedDB n'a pas une API très agréable, mais il existe des bibliothèques qui ajoutent une abstraction par-dessus. Ma préférée est la bibliothèque SQL AlaSQL, et idb par Jake Archibald.

Démonstration complète

Le code source complet est disponible sur GitHub dans le dépôt jcubic/react-wayne-auth. Vous pouvez tester une démonstration fonctionnelle sur GitHub pages.

Si vous aimez cet article, vous pouvez me suivre sur les réseaux sociaux : (Twitter/X et/ou LinkedIn) et vous pouvez également consulter mon site web personnel.