Consommer une API Django
| Durée estimée | 2h (une séance) |
|---|---|
| Prérequis | Avoir terminé le Lab 5 (Next.js App Router). Python 3.10+ installé. |
| Fichiers de départ | Votre projet annuaire-next/ du Lab 5 + le script setup_django.sh fourni |
| Livrables | Annuaire Next.js connecté au backend Django avec login et CRUD |
| Validation | Les données sont persistées : ajouter un étudiant, recharger, il est toujours là. |
À l’issue de ce lab, chaque étudiant sera capable de :
- Expliquer ce qu’est CORS et pourquoi le navigateur bloque certaines requêtes
- Utiliser des variables d’environnement pour configurer l’URL de l’API
- Centraliser les appels API dans un module
lib/api.js - Implémenter l’authentification par token (login + envoi dans les headers)
- Réaliser un CRUD complet (Create, Read, Delete) avec des données persistées
Partie 1 — Installer et explorer le backend
Lancer le backend Django fourni et tester l’API avec curl.
Lancer le backend
L’enseignant vous fournit un script setup_django.sh. Ce script crée un projet Django complet avec une base de données pré-remplie de 10 étudiants sénégalais.
chmod +x setup_django.sh
./setup_django.shPuis lancez le serveur Django :
cd annuaire-api
source venv/bin/activate
python manage.py runserverLe serveur tourne sur http://localhost:8000. Gardez ce terminal ouvert pendant tout le lab.
Les endpoints de l’API
| Méthode | URL | Description | Auth |
|---|---|---|---|
| POST | /api/auth/login/ |
Connexion \(\to\) token | Non |
| GET | /api/students/ |
Liste des étudiants | Token |
| POST | /api/students/ |
Créer un étudiant | Token |
| GET | /api/students/3/ |
Détail étudiant 3 | Token |
| DELETE | /api/students/3/ |
Supprimer étudiant 3 | Token |
Tester avec curl
Ouvrez un deuxième terminal. Commencez par le login :
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username":"prof","password":"le mot de passe défini dans setup_django.sh"}'Résultat :
{"token":"abc123def456...","username":"prof"}Copiez le token. Utilisez-le pour accéder aux données :
curl http://localhost:8000/api/students/ \
-H "Authorization: Token abc123def456..."Vous devez voir la liste des 10 étudiants au format JSON : Fatou Ndiaye, Moussa Diop, Aminata Sow…
Si vous oubliez le header Authorization, le backend répond :
{"detail":"Authentication credentials were not provided."}L’API est protégée. Chaque requête (sauf le login) doit envoyer le token.
Chaque étudiant a ces champs : id, prenom, nom, email, filiere (IABD, SIR), niveau (L1 à M2), date_naissance, telephone.
C’est différent du modèle JSONPlaceholder (name, email, company.name). On va adapter le frontend.
Partie 2 — Comprendre CORS
Comprendre pourquoi le navigateur bloque les requêtes entre origines différentes.
Le frontend tourne sur localhost:3000. Le backend sur localhost:8000. Ce sont deux origines différentes (ports différents). Par défaut, le navigateur interdit à un site d’envoyer des requêtes vers une autre origine. C’est la politique CORS (Cross-Origin Resource Sharing).
Quand quelqu’un sonne, le gardien vérifie la liste des visiteurs autorisés. Si le visiteur n’est pas sur la liste, il est refusé. CORS fonctionne pareil : le backend maintient une liste d’origines autorisées.
Le backend fourni a déjà django-cors-headers configuré : toutes les origines sont autorisées (mode développement). Vous n’avez rien à faire côté backend. En production, on limiterait la liste.
Partie 3 — Préparer le frontend
Créer la variable d’environnement, le module API et la gestion du token.
Ouvrez votre projet annuaire-next/ du Lab 5 dans VS Code. Lancez le serveur dans un troisième terminal :
cd annuaire-next
npm run devVérifiez que l’annuaire Lab 5 fonctionne sur http://localhost:3000.
Étape 1 — Variable d’environnement
À la racine du projet (pas dans src/), créez un fichier .env.local :
NEXT_PUBLIC_API_URL=http://localhost:8000/api| Préfixe | Accès |
|---|---|
NEXT_PUBLIC_ |
Accessible côté client et serveur |
| Sans préfixe | Serveur uniquement |
L’URL de l’API est utilisée dans des composants "use client". Elle a donc besoin du préfixe NEXT_PUBLIC_.
Au lieu d’écrire l’URL en dur dans chaque fetch, on écrira process.env.NEXT_PUBLIC_API_URL. Si l’URL change (en production par exemple), on modifie un seul fichier.
Étape 2 — Module lib/api.js
Créez le dossier src/lib/ et le fichier src/lib/api.js :
mkdir -p src/lib// src/lib/api.js
const API = process.env.NEXT_PUBLIC_API_URL;
export const login = async (username, password) => {
const res = await fetch(`${API}/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(
data.error || "Identifiants invalides"
);
}
return res.json();
};
export const getStudents = async (token) => {
const res = await fetch(`${API}/students/`, {
headers: { Authorization: `Token ${token}` },
});
if (!res.ok)
throw new Error("Erreur de chargement");
return res.json();
};
export const getStudent = async (token, id) => {
const res = await fetch(`${API}/students/${id}/`, {
headers: { Authorization: `Token ${token}` },
});
if (!res.ok)
throw new Error("Étudiant introuvable");
return res.json();
};
export const createStudent = async (token, data) => {
const res = await fetch(`${API}/students/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Token ${token}`,
},
body: JSON.stringify(data),
});
if (!res.ok)
throw new Error("Erreur de création");
return res.json();
};
export const deleteStudent = async (token, id) => {
const res = await fetch(`${API}/students/${id}/`, {
method: "DELETE",
headers: { Authorization: `Token ${token}` },
});
if (!res.ok)
throw new Error("Erreur de suppression");
};Au Lab 5, les URL JSONPlaceholder étaient écrites directement dans les composants. Si l’URL change, il faut modifier chaque fichier. Avec lib/api.js, on change une seule ligne (.env.local). Chaque fonction correspond à une opération du CRUD.
Étape 3 — Gestion du token
Créez src/lib/auth.js :
// src/lib/auth.js
"use client";
export const getToken = () => {
if (typeof window === "undefined") return null;
return sessionStorage.getItem("token");
};
export const setToken = (token) => {
sessionStorage.setItem("token", token);
};
export const removeToken = () => {
sessionStorage.removeItem("token");
};
export const isLoggedIn = () => {
return !!getToken();
};Le token est stocké dans sessionStorage. Il disparaît quand l’onglet est fermé (plus sûr que localStorage).
Partie 4 — Page de login
Créer une page de connexion et protéger les pages.
Créer le dossier et la page
mkdir -p src/app/loginCréez src/app/login/page.js :
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { login } from "@/lib/api";
import { setToken } from "@/lib/auth";
import styles from "./page.module.css";
export default function LoginPage() {
const router = useRouter();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [erreur, setErreur] = useState(null);
const [enCours, setEnCours] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setErreur(null);
setEnCours(true);
try {
const data = await login(username, password);
setToken(data.token);
router.push("/users");
} catch (error) {
setErreur(error.message);
}
setEnCours(false);
};
return (
<div>
<h1 className={styles.titre}>Connexion</h1>
<div className={styles.carte}>
<form onSubmit={handleSubmit}>
<div className={styles.groupe}>
<label className={styles.label}
htmlFor="username">
Nom d'utilisateur
</label>
<input className={styles.input}
type="text" id="username"
placeholder="prof"
value={username}
onChange={(e) =>
setUsername(e.target.value)
}
/>
</div>
<div className={styles.groupe}>
<label className={styles.label}
htmlFor="password">
Mot de passe
</label>
<input className={styles.input}
type="password" id="password"
placeholder="mot de passe"
value={password}
onChange={(e) =>
setPassword(e.target.value)
}
/>
</div>
{erreur && (
<p className={styles.erreur}>{erreur}</p>
)}
<button type="submit"
className={styles.bouton}
disabled={enCours}>
{enCours ? "Connexion..." :
"Se connecter"}
</button>
</form>
<p className={styles.hint}>
Identifiants de test : prof / le mot de passe affiché par setup_django.sh
</p>
</div>
</div>
);
}Créez src/app/login/page.module.css :
.titre { font-size: 1.5rem; color: #2d3436;
margin-bottom: 1.5rem; }
.carte { background: #fff;
border: 1px solid #dfe6e9;
border-radius: 10px; padding: 1.5rem;
max-width: 400px; }
.groupe { margin-bottom: 1.25rem; }
.label { display: block; font-size: 0.9rem;
font-weight: 600; color: #2d3436;
margin-bottom: 0.4rem; }
.input { width: 100%; padding: 0.65rem 0.9rem;
font-size: 0.95rem;
border: 1px solid #dfe6e9;
border-radius: 8px; }
.input:focus { outline: none;
border-color: #0984e3; }
.erreur { color: #d63031; font-weight: 600;
padding: 0.75rem; background: #ffeaa7;
border-radius: 6px;
margin-bottom: 1rem; }
.bouton { width: 100%; padding: 0.75rem;
background: #0984e3; color: #fff;
border: none; border-radius: 8px;
font-size: 1rem; font-weight: 600;
cursor: pointer; }
.bouton:hover { background: #0770c2; }
.bouton:disabled { background: #b2bec3;
cursor: not-allowed; }
.hint { color: #b2bec3; font-size: 0.8rem;
text-align: center; margin-top: 1rem; }Tester le login
Allez sur http://localhost:3000/login. Saisissez prof et le mot de passe que setup_django.sh a affiché. Si la connexion réussit, vous êtes redirigé vers /users. Si vous tapez un mauvais mot de passe, le message d’erreur s’affiche en rouge.
Après avoir créé .env.local, il faut arrêter et relancer npm run dev. Next.js ne recharge pas les variables d’environnement à chaud.
Partie 5 — Modifier les pages existantes
Adapter la liste, le détail, l’ajout et la Navbar pour utiliser l’API Django.
Les modifications suivent un principe simple : remplacer les appels directs à JSONPlaceholder par les fonctions de lib/api.js, et adapter les champs au modèle Student (prenom, nom, filiere… au lieu de name, company.name).
Modifier UserCard.jsx
Le composant reçoit maintenant un objet student (pas user). Remplacez le contenu de src/components/UserCard.jsx :
"use client";
import Link from "next/link";
import styles from "./UserCard.module.css";
const UserCard = ({ student }) => {
return (
<div className={styles.card}>
<div className={styles.header}>
<Link href={`/users/${student.id}`}
className={styles.lien}>
<h3 className={styles.nom}>
{student.prenom} {student.nom}
</h3>
</Link>
<span className={styles.badge}>
{student.filiere} {student.niveau}
</span>
</div>
<p className={styles.info}>
Email : {student.email}
</p>
{student.telephone && (
<p className={styles.info}>
Tél. : {student.telephone}
</p>
)}
<Link href={`/users/${student.id}`}
className={styles.voir}>
Voir le profil →
</Link>
</div>
);
};
export default UserCard;Ajoutez la classe .badge dans UserCard.module.css :
.badge {
background: #dfe6e9;
color: #2d3436;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}Modifier la page de liste
Remplacez le contenu de src/app/users/page.js :
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getStudents } from "@/lib/api";
import { getToken, isLoggedIn } from "@/lib/auth";
import UserCard from "@/components/UserCard";
import SearchBar from "@/components/SearchBar";
import styles from "./page.module.css";
export default function UserList() {
const router = useRouter();
const [users, setUsers] = useState([]);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
const [recherche, setRecherche] = useState("");
const [triAscendant, setTriAscendant] =
useState(true);
useEffect(() => {
if (!isLoggedIn()) {
router.push("/login");
return;
}
const charger = async () => {
try {
const data =
await getStudents(getToken());
setUsers(data);
} catch (error) {
setErreur(error.message);
}
setChargement(false);
};
charger();
}, [router]);
if (chargement)
return <p>Chargement en cours...</p>;
if (erreur)
return <p className={styles.erreur}>
Erreur : {erreur}</p>;
const usersFiltres = users
.filter((u) => {
const terme = recherche.toLowerCase();
const nom =
`${u.prenom} ${u.nom}`.toLowerCase();
return nom.includes(terme)
|| u.filiere.toLowerCase().includes(terme);
})
.sort((a, b) => {
const na = `${a.nom} ${a.prenom}`;
const nb = `${b.nom} ${b.prenom}`;
return triAscendant
? na.localeCompare(nb)
: nb.localeCompare(na);
});
return (
<div>
<h1 className={styles.titre}>
Liste des étudiants
</h1>
<SearchBar recherche={recherche}
onRecherche={setRecherche} />
<div className={styles.barre}>
<span className={styles.compteur}>
{usersFiltres.length} étudiant(s)
</span>
<button className={styles.boutonTri}
onClick={() =>
setTriAscendant(!triAscendant)
}>
{triAscendant ? "A → Z" : "Z → A"}
</button>
</div>
{usersFiltres.map((student) => (
<UserCard key={student.id}
student={student} />
))}
</div>
);
}- Le fetch direct est remplacé par
getStudents(getToken())(module centralisé + token). isLoggedIn()vérifie que l’utilisateur est connecté. Sinon, redirection vers/login.- Le filtre cherche dans
prenom + nomet dansfiliere(au lieu dename). - Le prop est
student(au lieu deuser).
Modifier la page de détail
Remplacez le contenu de src/app/users/[id]/page.js :
"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import { getStudent, deleteStudent } from "@/lib/api";
import { getToken, isLoggedIn } from "@/lib/auth";
import styles from "./page.module.css";
export default function UserDetail() {
const { id } = useParams();
const router = useRouter();
const [student, setStudent] = useState(null);
const [chargement, setChargement] = useState(true);
const [erreur, setErreur] = useState(null);
useEffect(() => {
if (!isLoggedIn()) {
router.push("/login");
return;
}
const charger = async () => {
try {
const data =
await getStudent(getToken(), id);
setStudent(data);
} catch (error) {
setErreur(error.message);
}
setChargement(false);
};
charger();
}, [id, router]);
const handleDelete = async () => {
if (!confirm("Supprimer cet étudiant ?"))
return;
try {
await deleteStudent(getToken(), id);
router.push("/users");
} catch (error) {
setErreur(error.message);
}
};
if (chargement) return <p>Chargement...</p>;
if (erreur) return (
<div>
<p>{erreur}</p>
<Link href="/users">Retour</Link>
</div>
);
return (
<div>
<Link href="/users" className={styles.retour}>
← Retour à la liste
</Link>
<div className={styles.profil}>
<div className={styles.avatar}>
{student.prenom.charAt(0)}
{student.nom.charAt(0)}
</div>
<h1 className={styles.nom}>
{student.prenom} {student.nom}
</h1>
<span className={styles.badge}>
{student.filiere} — {student.niveau}
</span>
</div>
<div className={styles.carte}>
<h2 className={styles.section}>
Informations
</h2>
<div className={styles.champ}>
<span className={styles.label}>Email</span>
<span>{student.email}</span>
</div>
<div className={styles.champ}>
<span className={styles.label}>Tél.</span>
<span>{student.telephone || "—"}</span>
</div>
<div className={styles.champ}>
<span className={styles.label}>
Naissance
</span>
<span>{student.date_naissance}</span>
</div>
</div>
<button className={styles.boutonSupprimer}
onClick={handleDelete}>
Supprimer cet étudiant
</button>
</div>
);
}Au Lab 5, cette page était un composant serveur (pas de "use client", fetch avec await directement). Maintenant, elle est un composant client car elle a besoin de getToken() (qui lit sessionStorage, côté client uniquement) et de handleDelete (événement onClick).
C’est un compromis : on perd le rendu serveur pour cette page, mais on gagne l’authentification et la suppression.
Ajoutez les classes .badge et .boutonSupprimer dans le CSS de la page de détail si elles n’y sont pas :
.badge {
display: inline-block;
background: #dfe6e9;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.boutonSupprimer {
margin-top: 1rem;
padding: 0.6rem 1.2rem;
background: #fff;
color: #d63031;
border: 1px solid #d63031;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
}
.boutonSupprimer:hover { background: #ffeaea; }Modifier le formulaire d’ajout
Remplacez src/app/users/add/page.js pour utiliser les champs du modèle Student (prénom, nom, filière, niveau, date de naissance) au lieu des champs JSONPlaceholder. Le formulaire appelle createStudent(getToken(), form) et vérifie isLoggedIn() au montage.
Les champs du formulaire :
| Champ | Type | Obligatoire |
|---|---|---|
prenom |
Texte | Oui |
nom |
Texte | Oui |
email |
Texte (avec @) | Oui |
date_naissance |
Date | Oui |
filiere |
Select (IABD, SIR) | Oui |
niveau |
Select (L1 à M2) | Oui |
telephone |
Texte | Non |
Le principe est identique au Lab 4 : état objet avec useState, handleChange avec clé dynamique, validation, e.preventDefault(), et redirection avec router.push() après succès.
Partie 6 — Tester la persistance
Vérifier que le CRUD fonctionne avec de vraies données persistées.
- Connectez-vous avec
profet le mot de passe quesetup_django.sha affiché. - La liste affiche les 10 étudiants sénégalais.
- Allez sur
/users/add. Ajoutez un étudiant (Ibrahima Sarr, SIR, L2). - Retournez sur la liste. Le nouvel étudiant apparaît.
- Rechargez la page (
F5). Il est toujours là. Les données sont en base. - Cliquez sur un étudiant, puis sur « Supprimer ». Confirmez.
- La liste se recharge. L’étudiant a disparu.
| Lab 5 | Lab 6 | |
|---|---|---|
| Source | JSONPlaceholder (fictif) | Django REST (réel) |
| Persistance | Aucune | Base de données SQLite |
| Auth | Aucune | Token (login/logout) |
| CRUD | Read + faux POST | Read, Create, Delete |
| Config URL | En dur | Variable d’environnement |
| Appels API | Dispersés | Centralisés (lib/api.js) |
Aide-mémoire
| Concept | Syntaxe |
|---|---|
| Variable d’env. | NEXT_PUBLIC_API_URL=... dans .env.local |
| Y accéder | process.env.NEXT_PUBLIC_API_URL |
| GET avec token | headers: { Authorization: `Token ${t}` } |
| POST | method: "POST" + headers + body |
| DELETE | method: "DELETE" + headers |
| Stocker le token | sessionStorage.setItem("token", t) |
| Lire le token | sessionStorage.getItem("token") |
| Supprimer | sessionStorage.removeItem("token") |
| Protéger une page | if (!isLoggedIn()) router.push("/login") |
| Module centralisé | src/lib/api.js |
Résumé et conclusion
Dans ce lab, vous avez :
- Installé et exploré une API Django REST pré-remplie.
- Compris CORS et pourquoi le navigateur contrôle les requêtes cross-origin.
- Créé des variables d’environnement et un module centralisé (
lib/api.js). - Implémenté l’authentification par token : login, stockage, envoi, déconnexion.
- Adapté le frontend au modèle Student (prenom, nom, filiere, niveau…).
- Vérifié le CRUD complet avec des données persistées.
| Étape | Statut | Ce que vous savez faire |
|---|---|---|
| DOM et événements | ✓ | Sélecteurs, événements |
| JS moderne | ✓ | Fonctions fléchées, async/await |
| React — les bases | ✓ | Composant, JSX, props, état |
| useEffect | ✓ | Montage, dépendances |
| Router et formulaires | ✓ | Routes, Link, inputs contrôlés |
| Next.js App Router | ✓ | Routing fichier, layout, server/client |
| API Django | ✓ | CORS, auth, CRUD, lib/api.js |
En 7 séances, vous êtes passés de la manipulation manuelle du DOM à une application Next.js complète connectée à un backend Django. Vous maîtrisez le JavaScript moderne, React (composants, état, effets, formulaires), Next.js (App Router, server/client) et la consommation d’API REST (CORS, authentification, CRUD).
{
6 — Consommer une API Django — Terminé ✓
Vous savez connecter un frontend Next.js à un backend Django,
gérer l’authentification et réaliser un CRUD complet.
Du DOM manuel à la production : 7 labs, un projet.
}
Ressources du chapitre
- TP · Lab 6 — Consommer une API Django (142 Ko)