Setup du projet Bootcode Hub

+ Authentification JWT complète

Vite · React 19 · TypeScript · Tailwind · Zustand · Axios

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

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

Plan du cours (~4h)

1

Architecture cible du projet

Monorepo frontend connecté au backend existant

2

Scaffolding Vite + React 19 + TypeScript

Configuration Tailwind, ESLint, Prettier

3

Setup des outils

React Router 6, Zustand, React Query, RHF + Zod

4

Flow JWT complet

Login, stockage token, intercepteur Axios, refresh, logout

5

Routes protégées

AuthGuard, redirection automatique

MODULE 1

Architecture cible du projet

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é

Architecture dossier

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 ↔ Backend

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!

Stack technique complète

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

⚠️ Piège courant #1

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

Scaffolding Vite + React 19 + TypeScript

Configuration Tailwind, ESLint, Prettier

Créer le projet avec Vite

# 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

Configuration TypeScript strict

// 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

Tailwind CSS

# 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

ESLint + Prettier

# 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/"

Structure de dossiers initiale

❌ 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

Setup des outils

React Router 6 · Zustand · React Query · RHF + Zod

React Router 6

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

Zustand — State Management

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()

TanStack Query (React Query)

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

React Hook Form + Zod

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

Récapitulatif des outils

npm install react-router-dom zustand @tanstack/react-query

npm install react-hook-form @hookform/resolvers zod

npm install axios

React Router Navigation SPA, routes imbriquées
Zustand State global (auth, UI), pas de Provider
React Query Server state, cache, loading/error states
RHF + Zod Formulaires performants + validation typée
Axios Client HTTP, intercepteurs, Bearer token

MODULE 4

Flow JWT complet

Login · Token · Interceptor · Refresh · Logout

C'est le cœur de l'authentification — chaque détail compte

Comment fonctionne JWT ?

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

Où stocker le 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

Auth Store — useAuthStore

// 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

Client Axios — Configuration

// 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

Intercepteur de réponse — 401 & Refresh

// 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

Page de Login — Composant complet

// 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

}

Login — JSX du formulaire

<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

Proxy Vite — Éviter les erreurs CORS

⚠️ 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

⚠️ Piège courant #2

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

⚠️ Piège courant #3

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

Flow JWT complet — Résumé

1️⃣

Login

Formulaire RHF + Zod → POST /auth/login → store token

2️⃣

Stockage

Zustand (mémoire) + persist middleware (localStorage)

3️⃣

Requêtes

Intercepteur Axios ajoute Bearer token automatiquement

4️⃣

Expiration (401)

Intercepteur → refresh token → retry ou logout

5️⃣

Logout

Vide le store + localStorage → redirect /login

MODULE 5

Routes protégées

AuthGuard · Redirection automatique

Composant AuthGuard

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

Configuration du router

// 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

Redirection automatique

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

⚠️ Piège courant #4

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!

À retenir !

🏗️

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 />

⚠️ Les 4 pièges courants

1

Coder des features avant le setup propre

Insister sur les fondations d'abord!

2

Confusion stockage mémoire vs localStorage

Zustand + persist middleware = les deux!

3

Oublier de gérer le token expiré (401)

Intercepteur de réponse obligatoire!

4

Ne pas configurer le proxy Vite

CORS en dev = proxy obligatoire

Prochaine étape

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!