Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1020 minIntermédiaire

Architecture API + SPA : React, Node.js et client lourd

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • distinguer une application rendue par le serveur d'une SPA qui appelle une API ;
  • organiser le front React et l'API Node.js d'un même projet ;
  • brancher un client lourd C# sur une API plutôt que sur la base ;
  • situer le rendu côté serveur et le BFF.

3 exercices corrigés1 schéma

Une architecture « API + SPA » sépare l'application en deux programmes : un front qui s'exécute dans le navigateur (une Single Page Application, ici en React) et une API sur le serveur (ici en Node.js avec Express), qui détient les règles et les données. Le front ne reçoit plus des pages HTML mais des données JSON, qu'il affiche lui-même. La même API peut alors servir d'autres clients : une application de bureau C#, une application mobile. Ce chapitre décrit cette architecture, la compare au rendu côté serveur, et montre comment brancher un client lourd sur l'API plutôt que sur la base.

Application rendue par le serveur ou SPA

Jusqu'ici, les applications PHP et Express du cours produisaient le HTML sur le serveur : chaque clic sur un lien envoie une requête, le serveur renvoie une page complète, le navigateur l'affiche. On parle d'application multipage (MPA) ou de rendu côté serveur.

Une SPA fonctionne autrement. Au premier chargement, le serveur envoie une page presque vide et un fichier JavaScript (le code React). Ensuite, le JavaScript affiche les écrans lui-même et, quand il a besoin de données, appelle l'API en fetch, qui répond en JSON. Changer d'écran ne recharge pas la page.

Application rendue par le serveur (MPA)SPA + API
Qui produit le HTMLLe serveur, à chaque requêteLe navigateur, avec React
Ce que renvoie le serveurDes pages HTMLUne fois le code du front, puis du JSON
NavigationRechargement complet à chaque pagePas de rechargement, interface fluide
Programmes à développerUnDeux : le front et l'API
Autres clients (mobile, bureau)Il faut ajouter une APIL'API existe déjà
Référencement par les moteurs de rechercheNaturelPlus délicat sans rendu côté serveur
Exemples de technologiesPHP, Express + EJS, ASP.NET Core MVCReact + Express, Angular + ASP.NET Core

L'architecture de ResaSalles en version SPA

les clientsSPA React 19dans le navigateurpages · api/Application WinFormsaccueil · HttpClientApplication mobileReact Native ou MAUIJSONHTTPSAPI RESTNode.js · Express 5routes · contrôleursservices : les règlesRepository : le SQL+ authentificationSQL · 3306MySQLles règles ne sont écrites qu'une fois, dans les services de l'API
Une API, trois clients : les règles ne sont écrites qu'une fois

C'est une architecture 3 tiers (navigateur, serveur d'application, base) dont le client est riche. L'organisation en couches du chapitre 6 s'applique telle quelle à l'API ; le front a sa propre organisation.

Organiser le dépôt

Le front et l'API sont deux projets Node.js distincts, avec leurs propres dépendances. On les place souvent dans le même dépôt Git (un monorepo), chacun dans son dossier :

text
resasalles/
├── api/                          ← Node.js 24 ou 26, Express 5
│   ├── package.json
│   └── src/
│       ├── app.js  server.js  db.js  erreurs.js
│       ├── routes/  controllers/  services/  repositories/
│       └── middlewares/authentifier.js
└── front/                        ← React 19, créé avec Vite
    ├── package.json
    ├── vite.config.js
    ├── index.html
    └── src/
        ├── main.jsx              ← point d'entrée, routeur
        ├── api/                  ← tout ce qui appelle l'API
        │   ├── client.js
        │   └── reservations.js
        ├── pages/                ← un composant par écran
        │   └── ReserverSalle.jsx
        └── components/           ← composants réutilisables

Le front se crée avec npm create vite@latest front -- --template react. Vite (version 8 en novembre 2026) sert le front en développement et le compile pour la production (npm run build produit un dossier dist/ de fichiers statiques). La navigation entre écrans est confiée à React Router. Le cours React présente ces outils.

Le front : présentation et appels à l'API

Le dossier api/ du front joue pour React le rôle que joue la couche d'accès aux données pour le service : c'est le seul endroit qui connaît les adresses de l'API et le format de ses erreurs.

js
// front/src/api/client.js : le seul fichier du front qui connaît l'adresse et le format de l'API.
export async function appelerApi(chemin, options = {}) {
  const reponse = await fetch(`/api${chemin}`, {
    ...options,
    headers: { 'Content-Type': 'application/json', ...options.headers },
  });
  const corps = reponse.status === 204 ? null : await reponse.json();
  if (!reponse.ok) {
    throw new Error(corps?.erreur ?? `Erreur ${reponse.status}`);
  }
  return corps;
}
js
// front/src/api/reservations.js : une fonction par opération de l'API.
import { appelerApi } from './client.js';

export const listerSalles = () => appelerApi('/salles');

export const reserver = (demande) =>
  appelerApi('/reservations', { method: 'POST', body: JSON.stringify(demande) });

Le composant de la page n'appelle jamais fetch directement : il utilise ces fonctions, affiche les données et les messages.

jsx
// front/src/pages/ReserverSalle.jsx : la présentation. Elle affiche et transmet, l'API décide.
import { useEffect, useState } from 'react';
import { listerSalles, reserver } from '../api/reservations.js';

export default function ReserverSalle() {
  const [salles, setSalles] = useState([]);
  const [message, setMessage] = useState('');

  useEffect(() => {
    listerSalles().then(setSalles).catch((e) => setMessage(e.message));
  }, []);

  async function envoyer(evenement) {
    evenement.preventDefault();
    const champs = Object.fromEntries(new FormData(evenement.currentTarget));
    try {
      const r = await reserver({
        salleId: Number(champs.salleId),
        debut: new Date(champs.debut).toISOString(),
        fin: new Date(champs.fin).toISOString(),
        objet: champs.objet,
        participants: Number(champs.participants),
      });
      setMessage(`Réservation n° ${r.id} enregistrée`);
    } catch (e) {
      setMessage(e.message);
    }
  }

  return (
    <form onSubmit={envoyer}>
      <select name="salleId">
        {salles.map((s) => (
          <option key={s.id} value={s.id}>{s.nom} ({s.capacite} places)</option>
        ))}
      </select>
      <input type="datetime-local" name="debut" required />
      <input type="datetime-local" name="fin" required />
      <input name="objet" maxLength={100} required />
      <input type="number" name="participants" min={1} defaultValue={1} />
      <button>Réserver</button>
      {message && <p role="status">{message}</p>}
    </form>
  );
}

Avec la salle Aiguille (4 places) et 6 participants, l'API répond 422 et le composant affiche « La salle Aiguille accueille 4 personnes au plus » ; avec 3 participants, il affiche « Réservation n° 42 enregistrée » (l'identifiant renvoyé par l'API). Le composant n'a pas eu besoin de connaître la règle : il affiche ce que l'API décide. new Date(champs.debut).toISOString() convertit l'heure saisie (heure locale du navigateur) en date ISO 8601 en temps universel : 14 h à Paris en décembre devient 2026-12-03T13:00:00.000Z, sans ambiguïté pour l'API.

L'API, elle, ajoute une route GET /api/salles (un contrôleur qui renvoie la liste en JSON et convertit visio en booléen, comme detailJson au chapitre 4) à la route POST /api/reservations du chapitre 6.

Le front et l'API sur la même origine

Le navigateur applique la politique de même origine : une page servie par http://localhost:5173 (le serveur de développement de Vite) ne peut pas lire librement les réponses de http://localhost:3000 (l'API), qui est une autre origine. Deux solutions :

  • Tout servir sous la même origine, la plus simple. En développement, le serveur de Vite transmet à l'API les requêtes qui commencent par /api ; en production, le serveur web (Nginx) sert les fichiers du front et transmet /api à Node.js. Le front appelle alors /api/... sans indiquer de serveur, comme dans client.js.
  • Autoriser l'origine du front dans l'API avec CORS, quand front et API sont vraiment sur des domaines différents (CORS).
js
// front/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { '/api': 'http://localhost:3000' },
  },
});
nginx
# Production : Nginx sert le front compilé et transmet /api à Node.js
server {
    listen 443 ssl;
    server_name resasalles.brevent.example;
    # ssl_certificate et ssl_certificate_key : le certificat du site

    root /var/www/resasalles/front/dist;

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
    }

    location / {
        try_files $uri /index.html;
    }
}

La directive try_files $uri /index.html renvoie index.html pour toute adresse qui ne correspond pas à un fichier : c'est indispensable pour une SPA, dont les adresses (/salles/3, /mes-reservations) sont gérées par React Router dans le navigateur, pas par des fichiers sur le serveur.

L'authentification entre le front et l'API

L'API doit savoir qui l'appelle. Deux familles de solutions :

  • une session côté serveur, identifiée par un cookie HttpOnly, Secure, SameSite : le navigateur l'envoie automatiquement, le JavaScript ne peut pas le lire. C'est le choix le plus sûr quand le front et l'API sont sur la même origine ;
  • un jeton (souvent un JWT) que le front envoie dans l'en-tête Authorization: Bearer …. C'est le choix naturel quand l'API sert aussi des clients qui ne sont pas des navigateurs (application de bureau, mobile, partenaires).

Les deux mécanismes, leurs risques et leurs protections sont dans le cours API REST et le cours Authentification. Pour l'architecture, retiens que c'est un middleware de l'API qui vérifie l'identité avant les contrôleurs, et que les contrôleurs lisent l'utilisateur dans req.utilisateur.

Client lourd + API

L'application WinForms du chapitre 5 se connectait directement à MySQL : architecture 2 tiers, identifiants de la base sur chaque poste. En la branchant sur l'API, elle devient un client de plus, comme le navigateur.

csharp
// Data/ResaSallesApi.cs
using System.Net.Http.Json;
using ResaSalles.Models;

namespace ResaSalles.Data;

// Le client lourd ne parle plus à MySQL : il appelle l'API, qui détient les règles et la base.
public class ResaSallesApi
{
    private readonly HttpClient _http;

    public ResaSallesApi(HttpClient http)
    {
        _http = http;
    }

    public async Task<List<Salle>> SallesAsync() =>
        await _http.GetFromJsonAsync<List<Salle>>("api/salles") ?? [];

    // null si l'API a accepté, sinon le message d'erreur qu'elle renvoie.
    public async Task<string?> ReserverAsync(Reservation demande)
    {
        HttpResponseMessage reponse = await _http.PostAsJsonAsync("api/reservations", demande);
        if (reponse.IsSuccessStatusCode) return null;
        ErreurApi? erreur = await reponse.Content.ReadFromJsonAsync<ErreurApi>();
        return erreur?.Erreur ?? $"Erreur {(int)reponse.StatusCode}";
    }
}

public record ErreurApi(string Erreur);

GetFromJsonAsync et PostAsJsonAsync (espace de noms System.Net.Http.Json, inclus dans .NET) convertissent automatiquement entre objets C# et JSON. Par défaut, ils utilisent les conventions du web : les propriétés C# SalleId, Debut deviennent salleId, debut en JSON, et la lecture ignore la casse, si bien que la réponse {"erreur":"…"} remplit la propriété Erreur du record. Le client C# et l'API Node.js se comprennent sans configuration, à condition que l'API renvoie de vrais booléens ("visio":true, converti dans le contrôleur) : System.Text.Json refuse de lire 1 dans un bool.

Deux précautions encore pour que l'API traite le client lourd comme le front React. D'abord, envoie les dates en UTC : Debut = dtpDebut.Value.ToUniversalTime() (de même pour Fin) ; le JSON porte alors un Z, comme la date envoyée par React. Sans cela, "2026-12-03T14:00:00" n'a pas de fuseau, et l'API Node.js la lit à l'heure locale du serveur : sur un serveur Debian réglé en UTC, la réservation serait décalée d'une heure. Ensuite, la route POST /api/reservations est protégée par le middleware d'authentification : ajoute à chaque requête le jeton reçu à la connexion, _http.DefaultRequestHeaders.Authorization = new("Bearer", jeton);.

Le formulaire crée l'objet avec l'adresse de l'API (new HttpClient { BaseAddress = new Uri("https://resasalles.brevent.example/") }) dans Program.cs, et ses gestionnaires d'événements deviennent asynchrones (private async void btnReserver_Click(…) avec await _api.ReserverAsync(demande)). Envoyée à l'API du chapitre 6 avec la réservation existante de 9 h à 10 h 30 dans la salle Mont-Blanc, une demande de 10 h à 11 h renvoie « La salle est déjà réservée sur ce créneau » ; une demande de 11 h à 12 h est acceptée.

Client lourd + MySQL (2 tiers)Client lourd + API (3 tiers)
Identifiants de la baseSur chaque posteSeulement sur le serveur de l'API
Règles de gestionDans le client (à redéployer à chaque modification)Dans l'API, partagées avec le web et le mobile
Accès depuis l'extérieurIl faudrait exposer MySQL : à proscrireL'API en HTTPS, protégée par l'authentification
Ce qui reste dans le clientToutL'interface et l'accès au matériel local

C'est aussi ce qui permet de cocher, à l'E6, la case des « appels à des services applicatifs distants » avec un client lourd.

Une application mobile sur la même API

Une application mobile se branche de la même façon. Avec React Native et Expo, tu réutilises tes connaissances de React, et même le module api/ du front, à condition de remplacer l'adresse relative /api par l'adresse complète de l'API (par exemple une constante API_URL) : une application mobile n'a pas d'origine à laquelle rattacher une adresse relative. Avec .NET MAUI, tu réutilises le client ResaSallesApi écrit en C#. Dans les deux cas, l'application ne contient que de la présentation et des appels à l'API ; les règles de gestion ne sont pas recopiées.

Rendu côté serveur et BFF

Deux variantes que tu croiseras en veille technologique :

  • Le rendu côté serveur des applications React (SSR). Des frameworks comme Next.js ou React Router en mode framework produisent le premier affichage sur le serveur (bon pour le référencement et la vitesse d'affichage), puis la page devient une SPA dans le navigateur. Les React Server Components de React 19 vont plus loin : certains composants ne s'exécutent que sur le serveur et peuvent lire la base directement. L'architecture redevient alors partiellement « rendue par le serveur ».
  • Le BFF (Backend For Frontend). Au lieu d'une API générale appelée par tous les clients, chaque front a sa petite API dédiée, qui agrège les appels aux services internes et ne renvoie que ce dont cet écran a besoin. Le BFF garde aussi les jetons côté serveur, ce qui évite de les exposer au JavaScript du navigateur.

Pour un projet de BTS, une SPA React servie par Vite et une API Express, ou une application rendue par le serveur en PHP, sont des choix tout aussi défendables. L'important est de savoir expliquer le tien.

Exercices

Exercice 1 sur 3 Où va ce code ? Réussi À revoir

Dans ResaSalles en version SPA, indique pour chaque élément s'il va dans le front React (et dans quel dossier) ou dans l'API (et dans quelle couche).

  1. La vérification qu'une salle n'est pas déjà réservée sur le créneau.
  2. La fonction qui appelle GET /api/salles/:id/reservations.
  3. Le calendrier de la semaine qui affiche les réservations d'une salle.
  4. La vérification que l'utilisateur connecté est administrateur avant de créer une salle.
  5. Le masquage du bouton « Créer une salle » pour les non-administrateurs.
Voir la réponseMasquer la réponse de l'exercice 1
  1. API, couche service (avec la requête dans le Repository).
  2. Front, dossier src/api/ (par exemple reservations.js).
  3. Front, un composant de src/components/ utilisé par une page de src/pages/.
  4. API : un middleware de contrôle d'accès avant la route, ou le service. C'est la vraie protection.
  5. Front, dans la page : c'est un confort d'affichage, qui ne protège rien. Le point 4 reste indispensable.

Tu avais trouvé ?

Exercice 2 sur 3 Diagnostiquer une page blanche Réussi À revoir

Après le déploiement de la SPA sur Nginx, la page d'accueil s'affiche. Mais quand un salarié ouvre directement l'adresse https://resasalles.brevent.example/salles/3 (depuis un favori), il obtient une erreur 404 de Nginx. En développement, tout fonctionnait. Explique la cause et la correction.

Voir la réponseMasquer la réponse de l'exercice 2

En ouvrant directement /salles/3, le navigateur demande ce chemin au serveur. Nginx cherche un fichier salles/3 dans dist/, ne le trouve pas et répond 404. Cette adresse n'existe que pour React Router, dans le navigateur. Le serveur de développement de Vite renvoie index.html pour ces chemins, d'où la différence.

Correction : try_files $uri /index.html; dans le bloc location /. Nginx renvoie alors index.html, React démarre, lit l'adresse et affiche la salle 3. Les requêtes /api/… sont traitées avant par le bloc location /api/.

Tu avais trouvé ?

Exercice 3 sur 3 Migrer le client lourd Réussi À revoir

L'accueil utilise l'application WinForms du chapitre 5, connectée directement à MySQL. On la branche sur l'API. Liste les classes du projet WinForms qui disparaissent, celles qui changent et celles qui restent, et dis ce qu'il faut faire côté serveur MySQL.

Voir la réponseMasquer la réponse de l'exercice 3
  • Disparaissent : les DAO (SalleDao, ReservationDao) et le paquet MySqlConnector ; les règles de gestion du contrôleur (elles sont dans l'API).
  • Apparaît : la classe ResaSallesApi, avec HttpClient.
  • Changent : le contrôleur, qui appelle ResaSallesApi au lieu des DAO (ou disparaît, si le formulaire appelle directement le client d'API), et les gestionnaires d'événements, qui deviennent async. Program.cs crée le HttpClient avec l'adresse de l'API au lieu de lire une chaîne de connexion.
  • Restent : les modèles (Salle, Reservation) et le formulaire.
  • Côté serveur : supprimer le compte MySQL utilisé par les postes, et fermer le port 3306 à tout le réseau sauf au serveur de l'API.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Monolithe, microservices et serverlessRevoir le sommaire du cours