Aller au contenu principal

Frontend React consommant l’API FastAPI

remarque

Ce chapitre est un bonus pratique à lire après le cœur backend. Il montre comment connecter un petit frontend React à l’API FastAPI, sans faire du frontend un prérequis pour comprendre le wiki.

Introduction​

Après les chapitres backend, l’API dispose déjà d’authentification, de routes protégées et d’un vrai flux utilisateur. Ce chapitre montre comment brancher un client React minimal mais réel sur ce backend : login, lecture des tâches, création de tâche et demande de reset.

Ce que ce chapitre va accomplir​

À la fin de ce chapitre, tu auras :

  • une base React + Vite + TypeScript,
  • un client API centralisé,
  • un AuthContext pour gérer le token,
  • un ProtectedRoute,
  • une page de login,
  • une page de demande de reset password,
  • un dashboard qui lit les tâches de l’utilisateur,
  • un formulaire de création de tâche,
  • la configuration CORS côté backend,
  • une compréhension claire de :
    • localStorage,
    • header Authorization,
    • react-router-dom,
    • @tanstack/react-query,
    • intégration frontend/backend locale.

Avant de commencer​

Ce chapitre reste un bonus, mais il a quelques prérequis précis :

  • les routes d’authentification doivent déjà fonctionner,
  • /user/me, /task/mine et /task/ doivent répondre côté backend,
  • CORS doit pouvoir être activé,
  • Celery n’est pas un prérequis pour suivre le frontend.

Autrement dit, lis ce chapitre après le cœur API, pas forcément après chaque détail d’infrastructure.

Sources et choix pédagogiques​

Ce chapitre s’appuie principalement sur :

  • 31-React JS/155-context.zip
  • 31-React JS/158-src.zip
  • 32-Frontend/166-app.zip

Les sources montrent trois niveaux d’évolution :

  • un contexte React simple,
  • une consommation API avec client typé,
  • un frontend plus complet avec auth et dashboard.

Pour le wiki, on garde une version volontairement plus petite :

  • Vite + React + TypeScript,
  • react-router-dom,
  • @tanstack/react-query,
  • axios,
  • AuthContext,
  • dashboard tâches,
  • login et reset password.

Architecture visée après ce chapitre​

On ajoute un dossier frontend séparé du backend :

fastapi-base-api/
├── main.py
├── config.py
├── api/
├── core/
├── database/
├── services/
├── worker/
├── templates/
└── frontend/
├── package.json
├── vite.config.ts
├── .env
├── index.html
└── src/
├── main.tsx
├── App.tsx
├── lib/
│ └── api.ts
├── context/
│ └── AuthContext.tsx
├── components/
│ ├── ProtectedRoute.tsx
│ ├── TaskCard.tsx
│ └── CreateTaskForm.tsx
└── pages/
├── LoginPage.tsx
├── ForgotPasswordPage.tsx
└── DashboardPage.tsx

Repères utiles avant le code​

Avant de commencer, retiens ces idées simples :

1. Le frontend et le backend sont deux projets séparés​

C’est important mentalement.

Le frontend :

  • appelle l’API,
  • ne remplace pas l’API,
  • ne doit pas embarquer la logique métier sensible.

2. Le login FastAPI ici attend du x-www-form-urlencoded​

C’est très important.

Notre route /user/token utilise OAuth2PasswordRequestForm. Donc côté React, tu ne peux pas envoyer n’importe quel JSON brut. Il faut envoyer un payload compatible formulaire.

3. Le token doit être centralisé​

Si tu gères le token dans dix composants différents, ton frontend devient vite fragile. C’est précisément le rôle de AuthContext.

4. Le dashboard doit lire l’API, pas simuler des données locales​

Le but n’est pas de faire une maquette fake. Le but est de montrer comment l’UI consomme vraiment :

  • /user/token
  • /user/me
  • /task/mine
  • /task/
  • /user/password-reset/request

5. CORS est un vrai prérequis​

Si ton backend refuse les appels depuis http://localhost:5173, rien ne marchera côté navigateur.

Étape 1 — créer le frontend React​

Place-toi à la racine du projet backend, puis lance :

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install react-router-dom @tanstack/react-query axios

Pourquoi cette stack ?​

Vite​

Rapide, simple, parfait pour un tutoriel pédagogique.

react-router-dom​

Pour avoir une vraie navigation frontend.

@tanstack/react-query​

Pour gérer les appels API, le cache et l’invalidation proprement.

axios​

Pour centraliser le client HTTP simplement.

Étape 2 — ajouter CORS côté backend​

Avant même d’écrire React, il faut permettre au navigateur d’appeler l’API.

Fais évoluer main.py comme ceci :

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from scalar_fastapi import get_scalar_api_reference

from api.router import router
from core.exceptions import add_exception_handlers
from core.middleware import request_metrics_middleware
from database.session import create_db_tables


@asynccontextmanager
async def lifespan(app: FastAPI):
await create_db_tables()
yield


app = FastAPI(
title="Task Organizer API",
description="Une API FastAPI consommée par un frontend React",
lifespan=lifespan,
)

add_exception_handlers(app)
app.middleware("http")(request_metrics_middleware)

app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(router)


@app.get("/scalar", include_in_schema=False)
def get_scalar_docs():
return get_scalar_api_reference(
openapi_url=app.openapi_url,
title="Scalar API",
)

Pourquoi cette étape est indispensable​

Sans cela, le navigateur bloquera les requêtes cross-origin.

C’est un point classique quand on passe de :

  • curl
  • ou tests backend

à :

  • un vrai frontend web.

Étape 3 — préparer l’environnement frontend​

Crée frontend/.env :

VITE_API_URL=http://localhost:8000

Pourquoi passer par VITE_API_URL​

Parce que ça évite de hardcoder partout http://localhost:8000. Et plus tard, tu pourras remplacer plus facilement l’URL pour :

  • un staging,
  • un déploiement,
  • un reverse proxy.

Étape 4 — créer src/lib/api.ts​

Ici, on choisit volontairement un client manuel simple.

remarque

Les sources montrent aussi un client généré à partir d’OpenAPI. C’est très utile plus tard, mais pour apprendre, une version manuelle reste plus lisible.

Crée frontend/src/lib/api.ts :

import axios from "axios";

export type StatusTask = "to_do" | "in_progress" | "completed";

export interface UserRead {
id: number;
name: string;
email: string;
email_verified: boolean;
}

export interface TaskRead {
id: number;
title: string;
description: string;
status: StatusTask;
owner_id: number;
}

export interface TaskCreate {
title: string;
description: string;
}

export interface TokenResponse {
access_token: string;
token_type: string;
}

const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});

export function setAuthToken(token: string | null) {
if (token) {
api.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete api.defaults.headers.common.Authorization;
}
}

export async function loginUser(email: string, password: string) {
const form = new URLSearchParams();
form.append("username", email);
form.append("password", password);

const { data } = await api.post<TokenResponse>("/user/token", form, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});

return data;
}

export async function getMe() {
const { data } = await api.get<UserRead>("/user/me");
return data;
}

export async function getMyTasks() {
const { data } = await api.get<TaskRead[]>("/task/mine");
return data;
}

export async function createTask(payload: TaskCreate) {
const { data } = await api.post<TaskRead>("/task/", payload);
return data;
}

export async function requestPasswordReset(email: string) {
const { data } = await api.post("/user/password-reset/request", {
email,
});
return data;
}

Ce qu’il faut remarquer​

setAuthToken()​

Cette fonction centralise l’injection du header Authorization.

URLSearchParams​

C’est ici qu’on respecte le format attendu par /user/token.

Le frontend connaît les types utiles​

On définit seulement les types nécessaires au frontend. Cela suffit largement pour démarrer.

Étape 5 — créer src/context/AuthContext.tsx​

Crée frontend/src/context/AuthContext.tsx :

import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from "react";

import { getMe, loginUser, setAuthToken, type UserRead } from "../lib/api";

interface AuthContextType {
token: string | null;
user: UserRead | null;
isReady: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<UserRead | null>(null);
const [isReady, setIsReady] = useState(false);

useEffect(() => {
async function bootstrap() {
const savedToken = localStorage.getItem("token");

if (!savedToken) {
setIsReady(true);
return;
}

try {
setAuthToken(savedToken);
const me = await getMe();
setToken(savedToken);
setUser(me);
} catch {
localStorage.removeItem("token");
setAuthToken(null);
setToken(null);
setUser(null);
} finally {
setIsReady(true);
}
}

bootstrap();
}, []);

async function login(email: string, password: string) {
const data = await loginUser(email, password);

localStorage.setItem("token", data.access_token);
setAuthToken(data.access_token);
setToken(data.access_token);

const me = await getMe();
setUser(me);
}

function logout() {
localStorage.removeItem("token");
setAuthToken(null);
setToken(null);
setUser(null);
}

const value = useMemo(
() => ({ token, user, isReady, login, logout }),
[token, user, isReady],
);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
}

Pourquoi cette structure est saine​

  • le token est centralisé,
  • le user courant est centralisé,
  • le bootstrap recharge la session après refresh navigateur,
  • l’UI ne dépend pas de localStorage partout.

Étape 6 — créer src/components/ProtectedRoute.tsx​

Crée frontend/src/components/ProtectedRoute.tsx :

import { Navigate } from "react-router-dom";

import { useAuth } from "../context/AuthContext";

export function ProtectedRoute({ children }: { children: JSX.Element }) {
const { token, isReady } = useAuth();

if (!isReady) {
return <p>Chargement de la session...</p>;
}

if (!token) {
return <Navigate to="/login" replace />;
}

return children;
}

Pourquoi c’est utile​

Parce qu’on veut éviter que :

  • la page dashboard s’ouvre sans token,
  • chaque page protège la session à sa manière.

Étape 7 — créer src/components/TaskCard.tsx​

Crée frontend/src/components/TaskCard.tsx :

import type { TaskRead } from "../lib/api";

export function TaskCard({ task }: { task: TaskRead }) {
return (
<article
style={{
border: "1px solid #e5e7eb",
borderRadius: 16,
padding: 16,
background: "white",
}}
>
<p style={{ color: "#6b7280", marginBottom: 8 }}>Task #{task.id}</p>
<h3 style={{ marginBottom: 8 }}>{task.title}</h3>
<p style={{ marginBottom: 12 }}>{task.description}</p>
<strong>Status: {task.status}</strong>
</article>
);
}

Étape 8 — créer src/components/CreateTaskForm.tsx​

Crée frontend/src/components/CreateTaskForm.tsx :

import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";

import { createTask } from "../lib/api";

export function CreateTaskForm() {
const queryClient = useQueryClient();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");

const mutation = useMutation({
mutationFn: createTask,
onSuccess: () => {
setTitle("");
setDescription("");
queryClient.invalidateQueries({ queryKey: ["my-tasks"] });
},
});

async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();

if (!title.trim() || !description.trim()) {
return;
}

mutation.mutate({ title, description });
}

return (
<form
onSubmit={handleSubmit}
style={{
display: "grid",
gap: 12,
padding: 16,
border: "1px solid #e5e7eb",
borderRadius: 16,
background: "white",
}}
>
<h2>Créer une tâche</h2>

<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Titre"
/>

<textarea
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Description"
rows={4}
/>

<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Création..." : "Créer"}
</button>

{mutation.isError && <p>Échec de création de la tâche.</p>}
</form>
);
}

Ce que montre ce composant​

C’est l’équivalent pédagogique de ce qu’on voit dans les sources frontend avec :

  • formulaire,
  • mutation,
  • invalidation du cache,
  • UI branchée sur une vraie route backend.

Étape 9 — créer src/pages/LoginPage.tsx​

Crée frontend/src/pages/LoginPage.tsx :

import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";

import { useAuth } from "../context/AuthContext";

export function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
setLoading(true);

try {
await login(email, password);
navigate("/dashboard");
} catch {
setError("Connexion impossible. Vérifie les identifiants ou la validation de l’email.");
} finally {
setLoading(false);
}
}

return (
<main style={{ maxWidth: 420, margin: "48px auto", display: "grid", gap: 16 }}>
<h1>Connexion</h1>

<form onSubmit={handleSubmit} style={{ display: "grid", gap: 12 }}>
<input
type="email"
placeholder="email@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>

<input
type="password"
placeholder="Mot de passe"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
/>

<button type="submit" disabled={loading}>
{loading ? "Connexion..." : "Se connecter"}
</button>
</form>

{error && <p>{error}</p>}

<Link to="/forgot-password">Mot de passe oublié ?</Link>
</main>
);
}

Lien avec les sources​

Cette page reprend l’esprit de :

  • app/routes/seller/login.tsx
  • app/components/login-form.tsx
  • app/contexts/AuthContext.tsx

mais dans une version plus simple et plus lisible pour le wiki.

Étape 10 — créer src/pages/ForgotPasswordPage.tsx​

Crée frontend/src/pages/ForgotPasswordPage.tsx :

import { useState } from "react";

import { requestPasswordReset } from "../lib/api";

export function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [message, setMessage] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setLoading(true);
setMessage(null);

try {
await requestPasswordReset(email);
setMessage("Si le compte existe, un email de reset a été envoyé.");
} finally {
setLoading(false);
}
}

return (
<main style={{ maxWidth: 420, margin: "48px auto", display: "grid", gap: 16 }}>
<h1>Mot de passe oublié</h1>

<form onSubmit={handleSubmit} style={{ display: "grid", gap: 12 }}>
<input
type="email"
placeholder="email@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>

<button type="submit" disabled={loading}>
{loading ? "Envoi..." : "Envoyer le lien"}
</button>
</form>

{message && <p>{message}</p>}
</main>
);
}

Ce que montre cette page​

Elle illustre parfaitement un flow utilisateur réel :

  • formulaire simple,
  • appel POST vers l’API,
  • réponse volontairement générique,
  • UX cohérente avec le chapitre notifications.

Étape 11 — créer src/pages/DashboardPage.tsx​

Crée frontend/src/pages/DashboardPage.tsx :

import { useQuery } from "@tanstack/react-query";

import { CreateTaskForm } from "../components/CreateTaskForm";
import { TaskCard } from "../components/TaskCard";
import { useAuth } from "../context/AuthContext";
import { getMyTasks } from "../lib/api";

export function DashboardPage() {
const { user, logout } = useAuth();

const { data, isLoading, isError } = useQuery({
queryKey: ["my-tasks"],
queryFn: getMyTasks,
});

return (
<main style={{ maxWidth: 980, margin: "32px auto", display: "grid", gap: 24 }}>
<header
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 16,
}}
>
<div>
<p style={{ color: "#6b7280" }}>Connecté en tant que</p>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>

<button onClick={logout}>Se déconnecter</button>
</header>

<CreateTaskForm />

<section style={{ display: "grid", gap: 16 }}>
<h2>Mes tâches</h2>

{isLoading && <p>Chargement des tâches...</p>}
{isError && <p>Impossible de charger les tâches.</p>}

{!isLoading && !isError && data?.length === 0 && (
<p>Aucune tâche pour le moment.</p>
)}

<div style={{ display: "grid", gap: 16 }}>
{data?.map((task) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</section>
</main>
);
}

Pourquoi ce dashboard est intéressant​

Il relie plusieurs briques du wiki :

  • auth,
  • protection frontend,
  • lecture des tâches possédées,
  • création de tâche,
  • invalidation React Query,
  • session frontend durable.

Étape 12 — créer src/App.tsx​

Crée frontend/src/App.tsx :

import { Navigate, Route, Routes } from "react-router-dom";

import { ProtectedRoute } from "./components/ProtectedRoute";
import { DashboardPage } from "./pages/DashboardPage";
import { ForgotPasswordPage } from "./pages/ForgotPasswordPage";
import { LoginPage } from "./pages/LoginPage";

function App() {
return (
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
);
}

export default App;

Étape 13 — créer src/main.tsx​

Crée frontend/src/main.tsx :

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import App from "./App";
import { AuthProvider } from "./context/AuthContext";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>,
);

Étape 14 — lancer le frontend​

Depuis frontend/ :

npm run dev

Le frontend devrait être disponible sur :

http://localhost:5173

Et le backend sur :

http://localhost:8000

Vérification guidée​

1. Vérifier le backend​

Avant le frontend, assure-toi que l’API répond toujours :

curl -i http://localhost:8000/docs

2. Vérifier le login backend à la main​

Tu peux tester le token avant même d’ouvrir React :

curl -i \
-X POST http://localhost:8000/user/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=enes@test.local&password=StrongPass123!'

3. Ouvrir le frontend​

Va sur :

http://localhost:5173

4. Tenter une connexion​

Si l’email n’est pas vérifié, tu dois voir un échec de connexion. Si le compte est vérifié, tu dois être redirigé vers /dashboard.

5. Vérifier le dashboard​

Une fois connecté :

  • le frontend doit appeler /user/me,
  • puis /task/mine,
  • puis afficher les tâches.

6. Créer une tâche​

Depuis le formulaire du dashboard :

  • une requête POST doit partir vers /task/,
  • la liste doit ensuite se rafraîchir.

7. Tester le mot de passe oublié​

Depuis /forgot-password :

  • envoie un email,
  • observe le message de confirmation générique.

Ce que ce chapitre ne fait pas encore​

Ce chapitre ne couvre pas encore :

  • un design system complet,
  • un back-office React riche,
  • SSR,
  • Remix/Next.js,
  • génération automatique complète du client API,
  • forms ultra-avancées,
  • gestion de refresh token.

Et c’est volontaire.

Le but ici est de montrer comment :

  • connecter React à ton API,
  • centraliser auth + token,
  • lire et écrire des données métier,
  • brancher un vrai dashboard simple.

Lien plus direct avec les sources frontend​

Ce qu’on a gardé​

  • l’idée de UserContext / AuthContext,
  • l’idée d’un client API centralisé,
  • l’idée de pages login / forgot-password / dashboard,
  • l’idée de formulaires connectés à l’API,
  • l’idée d’une app réellement consommatrice du backend.

Ce qu’on a simplifié​

  • on n’a pas gardé toute l’architecture UI avancée,
  • on n’a pas gardé tout le design system,
  • on n’a pas gardé la couche app-framework complète telle quelle.

Pourquoi ? Parce que pour le wiki, l’objectif est d’abord de faire comprendre le branchement frontend/backend.

Bonus — quand passer à un client généré ?​

La source 31-React JS/158-src.zip montre un client.ts généré. C’est très bien quand :

  • l’API grossit,
  • les schémas sont nombreux,
  • tu veux réduire le code HTTP manuel.

Mais pédagogiquement, il vaut mieux d’abord comprendre :

  • le login,
  • le header Authorization,
  • la récupération de /user/me,
  • les appels protégés.

Ensuite seulement, un client généré devient vraiment rentable.

Erreurs fréquentes​

Erreur 1 — blocage CORS dans le navigateur​

Cause fréquente :

  • oubli de CORSMiddleware.

Correction :

  • ajoute bien http://localhost:5173 dans allow_origins.

Erreur 2 — login en 422 ou 400​

Cause fréquente :

  • payload envoyé en JSON au lieu de x-www-form-urlencoded.

Correction :

  • utilise URLSearchParams et le bon header Content-Type.

Erreur 3 — dashboard vide alors que des tâches existent​

Cause fréquente :

  • token non injecté,
  • mauvais utilisateur,
  • endpoint incorrect.

Correction :

  • vérifie setAuthToken() et l’appel /task/mine.

Erreur 4 — après refresh, l’utilisateur est déconnecté alors que le token existe​

Cause fréquente :

  • oubli du bootstrap dans AuthContext.

Correction :

  • recharge le token depuis localStorage puis appelle /user/me.

Erreur 5 — la tâche est créée mais l’UI ne se met pas à jour​

Cause fréquente :

  • oubli de invalidateQueries({ queryKey: ["my-tasks"] }).

Correction :

  • invalide correctement le cache React Query.

Erreur 6 — VITE_API_URL n’est pas prise en compte​

Cause fréquente :

  • variable mal préfixée,
  • serveur Vite non relancé.

Correction :

  • garde le préfixe VITE_ et relance npm run dev.

Bonnes pratiques à retenir​

1. Garde le frontend séparé du backend​

Un dossier frontend/ dédié est beaucoup plus propre.

2. Centralise le client API​

Évite les appels fetch dispersés partout.

3. Centralise la session dans un contexte​

Évite de manipuler directement localStorage dans tous les composants.

4. Utilise React Query pour les données serveur​

C’est beaucoup plus propre que de multiplier les useEffect manuels.

5. Commence simple, puis raffine l’UI ensuite​

Le plus important ici est le flux technique, pas le maquillage final.

Checklist de validation​

À la fin, tu dois pouvoir cocher tout ceci :

  • j’ai créé le projet React avec Vite
  • j’ai ajouté react-router-dom, @tanstack/react-query et axios
  • j’ai activé CORS côté FastAPI
  • j’ai créé src/lib/api.ts
  • j’ai créé AuthContext
  • j’ai créé ProtectedRoute
  • je sais appeler /user/token correctement
  • je sais stocker et réinjecter le token
  • je sais lire /user/me
  • je sais lire /task/mine
  • je sais créer une tâche depuis le frontend
  • je sais déclencher une demande de reset password depuis React

Résumé final​

Dans ce chapitre bonus, tu as appris à faire vivre ton API FastAPI dans une vraie interface cliente React.

Le point le plus important à retenir est simple :

  • le backend garde la logique métier,
  • le frontend gère l’expérience utilisateur,
  • et les deux se parlent proprement à travers :
    • un client API,
    • un contexte d’auth,
    • un routage clair,
    • des appels réseau bien structurés.

Autrement dit : ce chapitre te fait passer de "j’ai une API" à "j’ai une API qui peut déjà alimenter un produit web".

Pour aller plus loin​