Projet : Plateforme de Gestion des Formations
Nous allons construire une application web complète : un site de gestion de formations avec inscription, administration et API REST. Ce projet intègre toutes les compétences du manuel.
Architecture du Projet
formations-app/
├── public/
│ ├── index.html # Page d'accueil publique
│ ├── formations.html # Liste des formations
│ ├── inscription.html # Formulaire d'inscription
│ └── admin/
│ └── dashboard.html # Interface d'administration
├── src/
│ ├── index.js # Serveur Express principal
│ ├── routes/
│ │ ├── formations.js # CRUD formations
│ │ ├── users.js # Gestion utilisateurs
│ │ └── auth.js # Authentification
│ ├── middleware/
│ │ ├── auth.js # Vérification JWT
│ │ └── validate.js # Validation des données
│ └── database.js # Connexion SQLite
├── migrations/
│ └── 001_schema.sql # Schéma de la BDD
├── .env # Variables d'environnement
├── .gitignore
└── package.json
Étape 1 : Schéma de Base de Données
-- migrations/001_schema.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'etudiant', -- etudiant | admin
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE formations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titre VARCHAR(200) NOT NULL,
description TEXT,
prix DECIMAL(10,2) DEFAULT 0,
duree INTEGER,
niveau VARCHAR(50) DEFAULT 'débutant',
image_url TEXT,
places_max INTEGER DEFAULT 20,
actif BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE inscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
formation_id INTEGER REFERENCES formations(id) ON DELETE CASCADE,
statut VARCHAR(20) DEFAULT 'en_attente',
inscrit_le DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, formation_id)
);
Étape 2 : API REST Complète
// src/index.js
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const db = require('./database');
const app = express();
app.use(cors(), express.json(), express.static('public'));
// ── Auth ──
app.post('/api/auth/register', async (req, res) => {
const { nom, email, password } = req.body;
const hash = await bcrypt.hash(password, 12);
const { lastID } = db.run(
'INSERT INTO users (nom, email, password) VALUES (?,?,?)',
[nom, email, hash]
);
res.status(201).json({ id: lastID, nom, email });
});
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = db.get('SELECT * FROM users WHERE email = ?', [email]);
if (!user || !await bcrypt.compare(password, user.password))
return res.status(401).json({ error: 'Identifiants incorrects' });
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET, { expiresIn: '24h' }
);
res.json({ token, user: { id: user.id, nom: user.nom, role: user.role } });
});
// ── Middleware Auth ──
const auth = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); }
catch { res.status(401).json({ error: 'Non autorisé' }); }
};
// ── CRUD Formations ──
app.get('/api/formations', (req, res) => {
const { search = '', niveau = '' } = req.query;
const formations = db.all(`
SELECT f.*, COUNT(i.id) as inscrits
FROM formations f LEFT JOIN inscriptions i ON i.formation_id = f.id
WHERE f.actif = 1
AND (f.titre LIKE ? OR f.description LIKE ?)
${niveau ? 'AND f.niveau = ?' : ''}
GROUP BY f.id ORDER BY f.created_at DESC
`, niveau ? [`%${search}%`, `%${search}%`, niveau] : [`%${search}%`, `%${search}%`]);
res.json({ success: true, data: formations });
});
app.post('/api/formations', auth, (req, res) => {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin requis' });
const { titre, description, prix, duree, niveau, places_max } = req.body;
const { lastID } = db.run(
'INSERT INTO formations (titre,description,prix,duree,niveau,places_max) VALUES(?,?,?,?,?,?)',
[titre, description, prix, duree, niveau, places_max]
);
res.status(201).json({ success: true, id: lastID });
});
// ── Inscription ──
app.post('/api/inscriptions', auth, (req, res) => {
const { formation_id } = req.body;
const formation = db.get('SELECT * FROM formations WHERE id = ?', [formation_id]);
if (!formation) return res.status(404).json({ error: 'Formation introuvable' });
const inscrits = db.get('SELECT COUNT(*) as n FROM inscriptions WHERE formation_id=?', [formation_id]);
if (inscrits.n >= formation.places_max)
return res.status(400).json({ error: 'Formation complète' });
db.run('INSERT INTO inscriptions (user_id, formation_id) VALUES (?, ?)',
[req.user.id, formation_id]);
res.status(201).json({ success: true, message: 'Inscription réussie !' });
});
app.listen(3000, () => console.log('✅ Serveur : http://localhost:3000'));
Étape 3 : Interface Frontend
<!-- public/formations.html -->
<div class="container py-5">
<div class="row g-3 mb-4">
<div class="col-md-8">
<input id="search" class="form-control form-control-lg"
placeholder="Rechercher une formation...">
</div>
<div class="col-md-4">
<select id="niveau" class="form-select form-select-lg">
<option value="">Tous les niveaux</option>
<option value="débutant">Débutant</option>
<option value="intermédiaire">Intermédiaire</option>
<option value="avancé">Avancé</option>
</select>
</div>
</div>
<div id="liste-formations" class="row g-4"></div>
</div>
<script>
async function charger() {
const search = document.getElementById('search').value;
const niveau = document.getElementById('niveau').value;
const { data } = await fetch(`/api/formations?search=${search}&niveau=${niveau}`)
.then(r => r.json());
document.getElementById('liste-formations').innerHTML = data.map(f => `
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge bg-primary mb-2">${f.niveau}</span>
<h5 class="card-title">${f.titre}</h5>
<p class="text-muted">${f.description?.slice(0, 100)}...</p>
<div class="d-flex justify-content-between align-items-center mt-3">
<strong class="text-primary fs-5">${f.prix} MAD</strong>
<button class="btn btn-sm btn-success" onclick="sInscrire(${f.id})">
S'inscrire
</button>
</div>
</div>
</div>
</div>
`).join('');
}
document.getElementById('search').addEventListener('input', charger);
document.getElementById('niveau').addEventListener('change', charger);
charger();
</script>
Déploiement sur Cloudflare Pages
1
Adapter pour Hono : Réécrire l'API Express en Hono (Cloudflare Workers compatible)
2
D1 Database : Créer la base D1 : wrangler d1 create mon-app-production
3
Migrations : Appliquer le schéma : wrangler d1 migrations apply mon-app-production
4
Build : npm run build puis wrangler pages deploy dist
5
Variables d'env : wrangler secret put JWT_SECRET pour les secrets
Récapitulatif des Compétences Acquises
| Technologie | Maîtrise | Usage principal |
| HTML5 | ⭐⭐⭐⭐⭐ | Structure sémantique, formulaires, accessibilité |
| CSS3 / Flexbox / Grid | ⭐⭐⭐⭐⭐ | Mise en page, animations, responsive |
| Bootstrap 5 | ⭐⭐⭐⭐☆ | Prototypage rapide, composants prêts |
| JavaScript ES6+ | ⭐⭐⭐⭐⭐ | Logique, DOM, APIs, async |
| Node.js / Express | ⭐⭐⭐⭐☆ | Backend, APIs REST, middleware |
| SQL / SQLite | ⭐⭐⭐⭐☆ | Persistance des données, relations |
| Sécurité Web | ⭐⭐⭐⭐☆ | XSS, SQL Injection, JWT, bcrypt |
| Git / GitHub | ⭐⭐⭐⭐⭐ | Versioning, collaboration, déploiement |
Prochaines étapes : Apprenez React.js (composants UI), TypeScript (typage statique), et les concepts de CI/CD (intégration continue) pour évoluer vers un poste de développeur Full Stack senior.
Question finale : Quelle technologie côté serveur avez-vous utilisée dans ce cours pour construire des APIs REST ?
Django (Python)
Node.js / Express ✓
Laravel (PHP)
Spring Boot (Java)