ENSET Mohammedia • Département Math & Info
Séance 1/2
À la fin de cette séance
Vous saurez créer des applications web modernes
avec séparation front/back
Rendu côté serveur (SSR)
// page.php
<?php
$data = mysqli_query($conn, "SELECT * FROM users");
while($row = mysqli_fetch_assoc($data)) {
echo "<tr><td>".$row['name']."</td></tr>";
}
?>
👎 Expérience limitée • Charge élevée
API REST + Client JS
// api.php
header('Content-Type: application/json');
echo json_encode(['users' => $users]);
// app.js
fetch('/api/users')
.then(res => res.json())
.then(data => updateUI(data));
✅ Interface réactive • Scalabilité
GET /api/products → Liste des produits
GET /api/products/42 → Produit #42
POST /api/products → Créer un produit
PUT /api/products/42 → Modifier #42
DELETE /api/products/42 → Supprimer #42
1. CLIENT
Envoie une requête HTTP
2. SERVEUR
Traite et retourne JSON
3. CLIENT
Met à jour l'interface
Récupérer des données
GET /api/users
GET /api/users/123
GET /api/users?role=admin
✓ Idempotent
✓ Cacheable
✓ Sans effet de bord
Créer une ressource
POST /api/users
Body: {
"name": "Ahmed",
"email": "ahmed@enset.ma"
}
✓ Non-idempotent
✓ Crée nouvelle ressource
✓ Retourne 201 Created
Modifier complètement
PUT /api/users/123
Body: {
"name": "Ahmed B.",
"email": "ahmed.b@enset.ma"
}
✓ Idempotent
✓ Remplace toute la ressource
✓ Crée si n'existe pas
Supprimer une ressource
DELETE /api/users/123
Réponse: 204 No Content
✓ Idempotent
✓ Supprime la ressource
✓ Pas de body requis
{
"success": true,
"data": {
"users": [
{
"id": 1,
"name": "Fatima Zahra",
"email": "fatima@enset.ma",
"courses": ["Web", "MongoDB"],
"active": true
},
{
"id": 2,
"name": "Mohammed Ali",
"email": "mohammed@enset.ma",
"courses": ["PHP", "JavaScript"],
"active": false
}
],
"total": 2,
"page": 1
},
"timestamp": "2024-01-15T10:30:00Z"
}
PHP: json_encode($data) / json_decode($json)
JS: JSON.stringify(obj) / JSON.parse(json)
Succès
200 OK
Requête réussie
201 Created
Ressource créée
204 No Content
Succès sans contenu
HTTP/1.1 201 Created
Location: /api/users/123
Erreur client
400 Bad Request
Requête invalide
401 Unauthorized
Non authentifié
403 Forbidden
Accès interdit
404 Not Found
Ressource introuvable
{"error": "Email invalide"}
Erreur serveur
500 Internal Error
Erreur serveur
502 Bad Gateway
Passerelle invalide
503 Unavailable
Service indisponible
{"error": "Erreur interne"}
// Ne jamais exposer les détails!
💡 Bonne pratique : Toujours retourner un code de statut approprié avec un message JSON explicite
<?php
// api/products.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type');
// Récupération de la méthode HTTP
$method = $_SERVER['REQUEST_METHOD'];
// Connexion à la base de données
$db = new PDO('mysql:host=localhost;dbname=shop', 'root', '');
// Router selon la méthode
switch($method) {
case 'GET':
handleGet($db);
break;
case 'POST':
handlePost($db);
break;
case 'PUT':
handlePut($db);
break;
case 'DELETE':
handleDelete($db);
break;
default:
http_response_code(405);
echo json_encode(['error' => 'Méthode non autorisée']);
}
💡 N'oubliez pas d'utiliser PDO avec des prepared statements!
function handleGet($db) {
$id = $_GET['id'] ?? null;
if ($id) {
$stmt = $db->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if ($product) {
echo json_encode($product);
} else {
http_response_code(404);
echo json_encode(['error' => 'Produit non trouvé']);
}
} else {
$stmt = $db->query("SELECT * FROM products");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($products);
}
}
function handlePost($db) {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['name']) || !isset($data['price'])) {
http_response_code(400);
echo json_encode(['error' => 'Données invalides']);
return;
}
$stmt = $db->prepare("INSERT INTO products (name, price) VALUES (?, ?)");
$stmt->execute([$data['name'], $data['price']]);
http_response_code(201);
echo json_encode([
'id' => $db->lastInsertId(),
'name' => $data['name'],
'price' => $data['price']
]);
}
Note : Pour récupérer le body JSON en PHP, utilisez file_get_contents('php://input')
fetch(url, options) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error(error));
// GET - Récupérer tous les produits
const products = await fetch('/api/products')
.then(res => res.json());
// GET - Un produit spécifique
const product = await fetch('/api/products/42')
.then(res => res.json());
// POST - Créer un produit
const newProduct = await fetch('/api/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'MacBook Pro',
price: 25000
})
}).then(res => res.json());
💡 Important : N'oubliez pas de vérifier response.ok avant de parser le JSON!
⚠️ Attention : Fetch ne rejette la Promise que pour les erreurs réseau, pas pour les codes HTTP 4xx/5xx !
// ❌ MAUVAIS - Ne gère pas les erreurs HTTP
fetch('/api/products/999')
.then(res => res.json()) // ⚠️ Crash si 404!
.then(data => console.log(data));
// ✅ BON - Gestion complète des erreurs
async function fetchProduct(id) {
try {
const response = await fetch(`/api/products/${id}`);
// Vérifier le statut HTTP
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'NetworkError') {
console.error('Erreur réseau:', error);
} else {
console.error('Erreur API:', error);
}
throw error; // Re-throw pour le caller
}
}
async function apiCall(url, options = {}) {
const res = await fetch(url, options);
if (!res.ok) {
const error = await res.json();
throw new Error(error.message || res.statusText);
}
return res.json();
}
// Classe pour gérer les produits
class ProductAPI {
constructor(baseURL = '/api/products') {
this.baseURL = baseURL;
}
// Helper pour toutes les requêtes
async request(endpoint = '', options = {}) {
const response = await fetch(this.baseURL + endpoint, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.status === 204 ? null : response.json();
}
// Récupérer tous les produits
async getAll() {
return this.request();
}
// Récupérer un produit
async getOne(id) {
return this.request(`/${id}`);
}
// Créer un produit
async create(product) {
return this.request('', {
method: 'POST',
body: JSON.stringify(product)
});
}
// Modifier un produit
async update(id, product) {
return this.request(`/${id}`, {
method: 'PUT',
body: JSON.stringify(product)
});
}
// Supprimer un produit
async delete(id) {
return this.request(`/${id}`, {
method: 'DELETE'
});
}
}
// Utilisation
const api = new ProductAPI();
const products = await api.getAll();
const newProduct = await api.create({ name: 'iPhone 15', price: 9999 });
💡 Cette approche orientée objet rend le code plus maintenable et réutilisable
// Afficher la liste des produits
async function displayProducts() {
const container = document.getElementById('products');
container.innerHTML = 'Chargement...
';
try {
const products = await api.getAll();
container.innerHTML = products.map(product => `
<div class="product-card" data-id="${product.id}">
<h3>${product.name}</h3>
<p>${product.price} DH</p>
<button onclick="editProduct(${product.id})">
Modifier
</button>
<button onclick="deleteProduct(${product.id})">
Supprimer
</button>
</div>
`).join('');
} catch (error) {
container.innerHTML = `
<p class="error">Erreur: ${error.message}</p>
`;
}
}
// Soumettre un formulaire via API
document.getElementById('productForm')
.addEventListener('submit', async (e) => {
e.preventDefault(); // Empêcher le rechargement
const formData = new FormData(e.target);
const product = Object.fromEntries(formData);
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.textContent = 'Envoi...';
try {
if (product.id) {
await api.update(product.id, product);
} else {
await api.create(product);
}
// Rafraîchir la liste
await displayProducts();
// Réinitialiser le formulaire
e.target.reset();
showMessage('Enregistré avec succès!', 'success');
} catch (error) {
showMessage(error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Enregistrer';
}
});
💡 Toujours donner du feedback visuel pendant les opérations asynchrones
// Structure de réponse cohérente
{
"success": true,
"data": {...},
"message": "Opération réussie"
}
// Debounce pour recherche
let timeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
search(e.target.value);
}, 300);
});
⚠️ Ne jamais exposer les informations sensibles (mots de passe DB, clés API) côté client !
Créez une API REST complète pour gérer des livres
books: id, title, author, isbn, year, available
loans: id, book_id, student_name, loan_date, return_date
• 30 min : Création BD + API
• 30 min : Interface JavaScript
• 20 min : Tests et améliorations
💡 Bonus : Ajoutez un système de filtrage par disponibilité et année de publication
Questions ? 🤔