Architecture Web Moderne


APIs REST & JavaScript Moderne


ENSET Mohammedia • Département Math & Info

Séance 1/2

Objectifs de la séance

🎯 Comprendre

  • Architecture client-serveur moderne
  • Principes REST et méthodes HTTP
  • Format JSON et échange de données

🚀 Maîtriser

  • Création d'API REST en PHP
  • Fetch API en JavaScript
  • Gestion asynchrone moderne

💡 Appliquer

  • CRUD complet via API
  • Gestion des erreurs HTTP
  • Interface dynamique sans rechargement

À la fin de cette séance

Vous saurez créer des applications web modernes
avec séparation front/back

Évolution de l'architecture web

Architecture traditionnelle

Rendu côté serveur (SSR)

  • PHP génère HTML complet
  • Rechargement à chaque action
  • Sessions pour maintenir l'état
// 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

Architecture moderne

API REST + Client JS

  • Séparation front/back
  • Mises à jour dynamiques
  • Stateless avec tokens
// 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é

Qu'est-ce qu'une API REST ?

Representational State Transfer

📋 Principes fondamentaux

  • Client-Serveur : Séparation des responsabilités
  • Stateless : Chaque requête est indépendante
  • Cacheable : Réponses peuvent être mises en cache
  • Interface uniforme : Standards HTTP

Exemple concret

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

🔄 Cycle requête/réponse REST

1. CLIENT

Envoie une requête HTTP

2. SERVEUR

Traite et retourne JSON

3. CLIENT

Met à jour l'interface

Les méthodes HTTP

GET

Récupérer des données

GET /api/users
GET /api/users/123
GET /api/users?role=admin

✓ Idempotent
✓ Cacheable
✓ Sans effet de bord

POST

Créer une ressource

POST /api/users
Body: {
  "name": "Ahmed",
  "email": "ahmed@enset.ma"
}

✓ Non-idempotent
✓ Crée nouvelle ressource
✓ Retourne 201 Created

PUT

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

DELETE

Supprimer une ressource

DELETE /api/users/123

Réponse: 204 No Content

✓ Idempotent
✓ Supprime la ressource
✓ Pas de body requis

PATCH pour modifications partielles • OPTIONS pour découvrir les méthodes • HEAD pour les en-têtes

Format JSON

JavaScript Object Notation

✅ Avantages

  • Léger et lisible
  • Natif en JavaScript
  • Supporté par tous les langages
  • Standard web (RFC 7159)

Types de données

  • string: "texte"
  • number: 42, 3.14
  • boolean: true, false
  • null: null
  • array: [1, 2, 3]
  • object: {"key": "value"}

Exemple pratique

{
  "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)

Codes de statut HTTP

2xx

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

4xx

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"}

5xx

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

Création d'une API REST en PHP

Structure de base

<?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']);
}

📝 Headers importants

  • Content-Type: application/json
  • CORS headers pour accès cross-origin

🔀 Routing simple

  • Switch sur REQUEST_METHOD
  • Une fonction par méthode HTTP

💡 N'oubliez pas d'utiliser PDO avec des prepared statements!

Implémentation CRUD

GET - Récupérer

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);
    }
}

POST - Créer

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 API - Côté client

API moderne pour les requêtes HTTP

✨ Avantages

  • Basé sur les Promises
  • Syntaxe simple et claire
  • Support async/await
  • Remplace XMLHttpRequest

Structure de base

fetch(url, options)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Exemples pratiques

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

Gestion des erreurs

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

📋 Helper réutilisable

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

🚨 Types d'erreurs

  • NetworkError : Pas de connexion
  • TypeError : URL invalide
  • AbortError : Requête annulée
  • HTTP 4xx/5xx : Erreur serveur

CRUD complet avec Fetch

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

Mise à jour dynamique de l'interface

Sans rechargement de page

// 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> `; } }

📱 Avantages UX

  • Pas de flash blanc
  • État préservé
  • Animations possibles
  • Feedback immédiat

Gestion des formulaires

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

Bonnes pratiques

🖥️ Côté serveur (PHP)

Sécurité

  • Valider toutes les entrées
  • Prepared statements (PDO)
  • Authentification JWT/Sessions
  • HTTPS en production

Structure

  • URLs RESTful cohérentes
  • Codes HTTP appropriés
  • Messages d'erreur clairs
  • Pagination pour les listes
// Structure de réponse cohérente
{
  "success": true,
  "data": {...},
  "message": "Opération réussie"
}

💻 Côté client (JS)

Expérience utilisateur

  • Loading states
  • Gestion d'erreurs gracieuse
  • Feedback immédiat
  • Debounce sur les recherches

Performance

  • Cache des données
  • Lazy loading
  • Annulation des requêtes
  • Optimistic UI updates
// 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 !

Travaux Pratiques

📚 Gestion de Bibliothèque

Créez une API REST complète pour gérer des livres

📋 Fonctionnalités à implémenter

  • Liste des livres avec pagination
  • Recherche par titre ou auteur
  • Ajout d'un nouveau livre
  • Modification des informations
  • Suppression d'un livre
  • Gestion des emprunts

Structure de la BD

books: id, title, author, isbn, year, available
loans: id, book_id, student_name, loan_date, return_date

🎯 Objectifs techniques

  1. API REST en PHP avec toutes les méthodes
  2. Interface web dynamique (sans rechargement)
  3. Gestion des erreurs complète
  4. Validation des données
  5. Messages de feedback utilisateur

⏱️ Timing

• 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

Pour aller plus loin

📚 Ressources

Documentation

  • MDN - Fetch API
  • REST API Tutorial
  • PHP.net - JSON functions
  • HTTP Status Codes

Outils de test

  • Postman / Insomnia
  • Browser DevTools
  • JSON Formatter
  • curl en ligne de commande

Questions ? 🤔

Raccourcis:
← → : Navigation
F : Plein écran
Esc : Quitter plein écran