+ Authentification JWT complète
Vite · React 19 · TypeScript · Tailwind · Zustand · Axios
Utilisez les flèches, cliquez ou glissez pour naviguer
1. Projet React professionnel configuré
Vite + React 19 + TypeScript strict + Tailwind
2. Client API avec intercepteurs JWT
Axios, Bearer token, refresh automatique
3. Store d'authentification persistant
Zustand + middleware persist
4. Page de login fonctionnelle
React Hook Form + Zod, connectée au backend
5. Routes protégées avec redirection
AuthGuard, redirection automatique vers /login
Architecture cible du projet
Monorepo frontend connecté au backend existant
Scaffolding Vite + React 19 + TypeScript
Configuration Tailwind, ESLint, Prettier
Setup des outils
React Router 6, Zustand, React Query, RHF + Zod
Flow JWT complet
Login, stockage token, intercepteur Axios, refresh, logout
Routes protégées
AuthGuard, redirection automatique
MODULE 1
Monorepo frontend connecté au backend existant
Le projet final reprend TOUT ce qu'on a vu en semaines 1-6
C'est l'intégration — pas un nouveau cours isolé
L'organisation est cruciale — c'est la fondation de tout le projet
src/
features/
auth/
components/
hooks/
pages/
types/
dashboard/
shared/
components/
hooks/
layouts/
lib/
api.ts
auth-store.ts
types/
index.ts
features/
Par domaine métier
shared/
Composants réutilisables
lib/
Utilitaires & stores
types/
Types globaux TS
Frontend (React)
• Vite dev server sur :5173
• Envoie des requêtes HTTP
• Stocke le JWT en mémoire
• Gère l'UI et la navigation
Backend (API)
• Serveur sur :8000
• Vérifie le JWT sur chaque requête
• Retourne les données en JSON
• Gère login, refresh, logout
🔗 Le proxy Vite connecte les deux en développement
Sans proxy → erreurs CORS!
Vite
Bundler ultra-rapide, HMR
React 19
UI library, composants
TypeScript strict
Typage, sécurité
Tailwind CSS
Utility-first styling
React Router 6
Navigation SPA
Zustand
State management minimal
React Query
Server state, cache
React Hook Form
Formulaires performants
Zod
Validation schemas
Axios
Client HTTP avec intercepteurs — Bearer token, refresh, gestion 401
Coder des features avant le setup propre
❌ "Je code d'abord, je configure après"
→ Projet illisible, dépendances en conflit, refactor douloureux
✅ Les fondations d'abord, les features ensuite
→ Setup complet → Auth → Features — dans cet ordre!
MODULE 2
Configuration Tailwind, ESLint, Prettier
# Créer le projet React + TypeScript
npm create vite@latest bootcode-hub -- --template react-ts
cd bootcode-hub
npm install
react-ts
Template React + TypeScript strict
Vite
HMR instantané, build rapide
React 19
Dernière version stable
💡 Le template react-ts inclut déjà tsconfig.json avec strict: true
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
strict: true
Active toutes les vérifications strictes
paths: @/*
Imports propres : @/lib/api
# Installation
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
// src/index.css
@import "tailwindcss";
💡 Tailwind v4 : plus besoin de tailwind.config.ts — tout se configure en CSS
# Installation
npm install -D eslint prettier
npm install -D eslint-config-prettier
npm install -D @typescript-eslint/eslint-plugin
ESLint
Détecte les erreurs et mauvaises pratiques
Typescript-aware, React-specific
Prettier
Formatage automatique du code
Pas de débat sur le style!
// package.json scripts
"lint": "eslint src/",
"format": "prettier --write src/"
❌ Par défaut (Vite)
src/
App.tsx
App.css
index.css
main.tsx
// Tout au même endroit!
✅ Après réorganisation
src/
features/
shared/
lib/
types/
App.tsx
main.tsx
// Organisé par domaine!
🎯 Créez les dossiers tout de suite — même s'ils sont vides au début
MODULE 3
React Router 6 · Zustand · React Query · RHF + Zod
npm install react-router-dom
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />} >
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
)
}
💡 <Outlet /> dans le Layout affiche la route enfant
npm install zustand
// src/lib/counter-store.ts
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
🎯 Pas de Provider, pas de boilerplate — juste create()
npm install @tanstack/react-query
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
Server State
React Query — cache, revalidation, loading states
Client State
Zustand — auth, UI, préférences
npm install react-hook-form @hookform/resolvers zod
// Schéma Zod = validation + types en un seul endroit
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
type LoginForm = z.infer<typeof loginSchema>
🎯 z.infer génère le type TypeScript automatiquement depuis le schéma!
Un seul source of truth pour la validation ET les types
npm install react-router-dom zustand @tanstack/react-query
npm install react-hook-form @hookform/resolvers zod
npm install axios
MODULE 4
Login · Token · Interceptor · Refresh · Logout
C'est le cœur de l'authentification — chaque détail compte
1. Login
email + password
2. Serveur vérifie
credentials OK?
3. Retourne token
access + refresh
4. Client stocke
Zustand + localStorage
Access Token
Durée de vie courte (15min)
Envoyé dans chaque requête
Refresh Token
Durée de vie longue (7 jours)
Utilisé pour obtenir un nouvel access token
✅ Mémoire (Zustand)
• Accessible instantanément
• Disparaît au refresh de page
• Protégé contre XSS
• Utilisé pour les requêtes
✅ localStorage
• Persiste au refresh de page
• Survit à la fermeture du navigateur
• Vulnérable au XSS
• Utilisé pour le rehydrate
🎯 Les DEUX ! Zustand pour l'utilisation, localStorage pour la persistence
Le middleware persist de Zustand fait le pont automatiquement
// src/lib/auth-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
login: (user: User, token: string) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()
.persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (user, token) => set({ user, token, isAuthenticated: true }),
logout: () => set({ user: null, token: null, isAuthenticated: false }),
}),
{ name: 'auth-storage' }
)
)
🎯 persist synchronise automatiquement Zustand ↔ localStorage
// src/lib/api.ts
import axios from 'axios'
import { useAuthStore } from './auth-store'
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: { 'Content-Type': 'application/json' },
})
// Intercepteur de requête — ajoute le Bearer token
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
🎯 Chaque requête part automatiquement avec Authorization: Bearer xxx
// src/lib/api.ts (suite)
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const { data } = await axios.post('/auth/refresh')
useAuthStore.getState().login(data.user, data.token)
originalRequest.headers.Authorization = `Bearer ${data.token}`
return apiClient(originalRequest)
} catch {
useAuthStore.getState().logout()
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
⚠️ Si le refresh échoue aussi → logout + redirect vers /login
// src/features/auth/pages/LoginPage.tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/lib/auth-store'
import { apiClient } from '@/lib/api'
import { loginSchema } from '../types'
export default function LoginPage() {
const navigate = useNavigate()
const login = useAuthStore((s) => s.login)
const { register, handleSubmit, formState: { errors } } =
useForm({ resolver: zodResolver(loginSchema) })
const onSubmit = async (data) => {
const res = await apiClient.post('/auth/login', data)
login(res.data.user, res.data.token)
navigate('/dashboard')
}
// ... JSX avec register() sur les inputs
}
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input type="email" {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>Mot de passe</label>
<input type="password" {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">Se connecter</button>
</form>
register()
Connecte l'input au formulaire
zodResolver
Validation Zod → RHF
errors
Messages d'erreur auto
⚠️ Sans proxy : le navigateur bloque les requêtes vers localhost:8000
CORS = Cross-Origin Resource Sharing — sécurité du navigateur
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace('/^/api', ''),
}
}
}
})
🎯 En dev : /api/auth/login → proxy vers http://localhost:8000/auth/login
En production, le backend et le frontend sont sur le même domaine
Confusion mémoire vs localStorage
❌ Stocker le token SEULEMENT en localStorage
→ Il faut le lire à chaque requête = lent et risqué
❌ Stocker le token SEULEMENT en mémoire
→ Disparaît au refresh de page = mauvaise UX
✅ Zustand (mémoire) + middleware persist (localStorage)
→ Rapide ET persistant — le meilleur des deux mondes
Oublier de gérer le token expiré (401)
❌ L'utilisateur voit des erreurs bizarres sans explication
→ Son token a expiré mais l'app ne le sait pas
✅ Intercepteur 401 → refresh automatique
→ Si refresh échoue aussi → logout + redirect /login
Login
Formulaire RHF + Zod → POST /auth/login → store token
Stockage
Zustand (mémoire) + persist middleware (localStorage)
Requêtes
Intercepteur Axios ajoute Bearer token automatiquement
Expiration (401)
Intercepteur → refresh token → retry ou logout
Logout
Vide le store + localStorage → redirect /login
MODULE 5
AuthGuard · Redirection automatique
Un composant Layout qui vérifie l'authentification
Si pas connecté → redirect vers /login
Si connecté → affiche la page demandée
// src/shared/components/AuthGuard.tsx
import { Navigate, Outlet } from 'react-router-dom'
import { useAuthStore } from '@/lib/auth-store'
export default function AuthGuard() {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <Outlet />
}
💡 <Outlet /> rend la route enfant — AuthGuard est un Layout guard
// src/App.tsx — Routes publiques vs protégées
<Routes>
{/* Routes publiques */}
<Route path="/login" element={<LoginPage />} />
{/* Routes protégées */}
<Route element={<AuthGuard />}>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
</Route>
</Route>
</Routes>
Publiques
/login — accessibles sans token
Protégées
/dashboard, /profile — AuthGuard vérifie le token
Après login → page demandée
const from = location.state?.from?.pathname || '/dashboard'
navigate(from, { replace: true })
Après logout → /login
const logout = useAuthStore((s) => s.logout)
const handleLogout = () => {
logout()
navigate('/login')
}
🎯 location.state?.from mémorise la page demandée avant la redirection vers /login
Ne pas configurer le proxy Vite
❌ Requêtes vers localhost:8000 depuis localhost:5173
→ Erreur CORS dans la console
→ Rien ne fonctionne!
✅ Configurer server.proxy dans vite.config.ts
→ Le proxy Vite redirige les requêtes côté serveur
→ Pas de problème CORS!
L'architecture dossier est cruciale : features/, shared/, lib/, types/
JWT = Zustand (mémoire) + persist (localStorage) pour le refresh
L'intercepteur Axios ajoute automatiquement le Bearer token
401 → refresh automatique → si échec → logout + redirect /login
Routes protégées = AuthGuard + <Outlet />
Coder des features avant le setup propre
Insister sur les fondations d'abord!
Confusion stockage mémoire vs localStorage
Zustand + persist middleware = les deux!
Oublier de gérer le token expiré (401)
Intercepteur de réponse obligatoire!
Ne pas configurer le proxy Vite
CORS en dev = proxy obligatoire
Le projet final reprend TOUT ce qu'on a vu en semaines 1-6
C'est l'intégration de :
React
Composants, hooks
TypeScript
Types, interfaces
State
Zustand, React Query
Forms
RHF + Zod
HTTP
Axios, interceptors
Routing
Router, AuthGuard
🎯 Setup propre → Auth → Features — toujours dans cet ordre!