Polar Code 🎭

Command Palette

Search for a command to run...

10
Pièce N°10

Module 10 -- JSON et Requêtes HTTP : Les Langages de la Ville

La nuit, les données circulent. D'un service à l'autre. Comme des messages codés laissés dans des boîtes aux lettres. JSON est l'encre. HTTP est la poste. Fetch est le coursier.

JSON - Le Langage Universel

Qu'est-ce que JSON ?

JavaScript Object Notation.
Un format texte. Léger. Lisible. Universel.
Le lingua franca du web.

// En mémoire, c'est un objet JavaScript
const detective = {
  nom: "Marlowe",
  prenom: "Philip",
  age: 42,
  actif: true,
  cas_resolus: 147,
  specialites: ["surveillance", "interrogatoire", "filature"],
  bureau: {
    adresse: "615 Cahuenga Blvd",
    ville: "Los Angeles",
    etage: 3
  },
  dernier_cas: null
};

// Sérialisé en JSON, c'est une chaîne
const jsonString = JSON.stringify(detective);
console.log(jsonString);
// {
//   "nom": "Marlowe",
//   "prenom": "Philip",
//   "age": 42,
//   "actif": true,
//   "cas_resolus": 147,
//   "specialites": ["surveillance", "interrogatoire", "filature"],
//   "bureau": {
//     "adresse": "615 Cahuenga Blvd",
//     "ville": "Los Angeles",
//     "etage": 3
//   },
//   "dernier_cas": null
// }

Les Règles du Jeu

Syntaxe stricte :

  • Les clés toujours entre doubles guillemets
  • Chaînes entre doubles guillemets
  • Pas de commentaires
  • Pas de trailing commas
  • Pas de fonctions, dates, undefined
// VALIDE
const valide = {
  "nom": "Marlowe",
  "age": 42,
  "enquete": true,
  "temoins": ["Témoin A", "Témoin B"],
  "details": null
};

// INVALIDE
const invalide = {
  nom: "Marlowe",  // clés sans guillemets (OK en JS, pas en JSON pur)
  age: undefined,   // undefined n'existe pas en JSON
  date: new Date(), // Date devient une chaîne
  fn: function() {} // Les fonctions disparaissent
};

console.log(JSON.stringify(invalide));
// {"nom":"Marlowe","date":"2024-03-18T10:30:00.000Z"}
// age et fn ont disparu

JSON vs JavaScript Object

const obj = {
  name: "Marlowe",
  age: 42,
  // Méthode (ne sera pas dans JSON)
  sePresenter() {
    return `Je suis ${this.name}`;
  }
};

// Sérialisation
const json = JSON.stringify(obj);
console.log(json); // {"name":"Marlowe","age":42}

// Désérialisation
const obj2 = JSON.parse(json);
console.log(obj2.sePresenter); // undefined (perdu dans la sérialisation)

Méthodes JSON

JSON.stringify(value[, replacer[, space]])

const detective = {
  nom: "Marlowe",
  age: 42,
  dossier_noir: true,
  notes: "À surveiller"
};

// Simple
console.log(JSON.stringify(detective));
// {"nom":"Marlowe","age":42,"dossier_noir":true,"notes":"À surveiller"}

// Avec replacer (fonction)
const censure = JSON.stringify(detective, (key, value) => {
  if (key === 'notes') return 'CLASSIFIED';
  if (key === 'dossier_noir') return undefined; // Supprimé
  return value;
});
console.log(censure);
// {"nom":"Marlowe","age":42,"notes":"CLASSIFIED"}

// Avec replacer (tableau)
const partiel = JSON.stringify(detective, ['nom', 'age']);
console.log(partiel); // {"nom":"Marlowe","age":42}

// Avec indentation (pretty print)
const joli = JSON.stringify(detective, null, 2);
console.log(joli);
// {
//   "nom": "Marlowe",
//   "age": 42,
//   "dossier_noir": true,
//   "notes": "À surveiller"
// }

JSON.parse(text[, reviver])

const json = '{"nom":"Marlowe","age":42,"date_embauche":"2005-03-15T10:30:00.000Z"}';

// Simple
const obj = JSON.parse(json);
console.log(obj.date_embauche); // "2005-03-15T10:30:00.000Z" (string)

// Avec reviver pour transformer
const obj2 = JSON.parse(json, (key, value) => {
  if (key === 'date_embauche') return new Date(value);
  return value;
});
console.log(obj2.date_embauche); // Date object
console.log(obj2.date_embauche.getFullYear()); // 2005

Cas d'Usage Réels

Configuration :

// config.json
{
  "api": {
    "baseUrl": "https://api.surveillance.local",
    "timeout": 5000,
    "retries": 3
  },
  "database": {
    "host": "localhost",
    "port": 5432,
    "name": "surveillance_db"
  },
  "features": {
    "analytics": true,
    "notifications": false
  }
}

// Chargement
const config = JSON.parse(fs.readFileSync('config.json', 'utf8'));

Échange entre services :

// Données d'un témoin
const temoin = {
  id: "TEM-2024-001",
  nom: "Smith",
  prenom: "John",
  deposition: "J'ai vu une voiture noire...",
  date_deposition: new Date().toISOString(),
  fichiers_joints: ["photo1.jpg", "enregistrement.mp3"]
};

// Envoi à l'API
const jsonTemoin = JSON.stringify(temoin);
// POST /api/temoins avec jsonTemoin dans le body

Fetch API - Le Coursier de la Nuit

Introduction à Fetch

Avant Fetch : XMLHttpRequest. Lourd. Verbose.
Fetch : moderne, basé sur Promises, simple.

// La syntaxe de base
fetch(url, options)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Erreur:', error));

Requête GET - Demander des Informations

// GET simple
fetch('https://api.surveillance.local/cas/123')
  .then(response => response.json())
  .then(cas => {
    console.log(`Cas ${cas.id}: ${cas.titre}`);
    console.log(`Statut: ${cas.statut}`);
    console.log(`Témoins: ${cas.temoins.length}`);
  })
  .catch(err => console.error('Erreur de réseau:', err));

// Avec async/await (plus lisible)
async function getCas(id) {
  try {
    const response = await fetch(`https://api.surveillance.local/cas/${id}`);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    const cas = await response.json();
    return cas;
  } catch (error) {
    console.error('Erreur:', error);
    return null;
  }
}

// Utilisation
const cas123 = await getCas(123);

Headers - Les En-têtes du Message

// Headers simples
fetch('https://api.surveillance.local/cas', {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
    'X-API-Key': 'surveillance-2024-secret'
  }
});

// Headers avec objet Headers
const headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Authorization', 'Bearer token123');

// Ou en une ligne
const headers = new Headers({
  'Content-Type': 'application/json',
  'Authorization': 'Bearer token123'
});

fetch(url, { headers });

Requête POST - Envoyer des Données

// Nouveau témoin
const nouveauTemoin = {
  nom: "Dubois",
  prenom: "Marie",
  telephone: "+33123456789",
  deposition: "J'étais au café à 21h...",
  confidential: true
};

async function ajouterTemoin(temoin) {
  const response = await fetch('https://api.surveillance.local/temoins', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${localStorage.getItem('token')}`
    },
    body: JSON.stringify(temoin)
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message || 'Erreur lors de l\'ajout');
  }
  
  return await response.json();
}

// Utilisation
try {
  const temoinCree = await ajouterTemoin(nouveauTemoin);
  console.log(`Témoin ${temoinCree.id} créé avec succès`);
} catch (error) {
  console.error('Échec:', error.message);
}

PUT et PATCH - Mettre à Jour

// PUT (remplacement complet)
async function mettreAJourCas(id, donnees) {
  const response = await fetch(`https://api.surveillance.local/cas/${id}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${getToken()}`
    },
    body: JSON.stringify(donnees)
  });
  
  return response.json();
}

// PATCH (mise à jour partielle)
async function mettreAJourStatut(id, nouveauStatut) {
  const response = await fetch(`https://api.surveillance.local/cas/${id}/statut`, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ statut: nouveauStatut })
  });
  
  return response.json();
}

DELETE - Supprimer

async function supprimerCas(id) {
  const response = await fetch(`https://api.surveillance.local/cas/${id}`, {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${getToken()}`
    }
  });
  
  if (!response.ok) {
    throw new Error(`Échec de suppression: ${response.status}`);
  }
  
  // DELETE retourne souvent 204 No Content
  if (response.status === 204) {
    return { success: true, message: 'Cas supprimé' };
  }
  
  return response.json();
}

// Avec confirmation
async function supprimerAvecConfirmation(id) {
  const confirmer = window.confirm('Supprimer ce cas ? Cette action est irréversible.');
  
  if (!confirmer) {
    return { cancelled: true };
  }
  
  try {
    const result = await supprimerCas(id);
    alert('Cas supprimé avec succès');
    return result;
  } catch (error) {
    alert(`Erreur: ${error.message}`);
    throw error;
  }
}

Gestion des Réponses

async function traiterReponse(response) {
  const contentType = response.headers.get('content-type');
  
  // Différents types de réponse
  if (contentType && contentType.includes('application/json')) {
    return await response.json();
  } else if (contentType && contentType.includes('text/')) {
    return await response.text();
  } else if (contentType && contentType.includes('image/')) {
    return await response.blob();
  } else {
    return await response.arrayBuffer();
  }
}

// Vérifier le statut
async function fetchAvecControle(url, options = {}) {
  const response = await fetch(url, options);
  
  if (response.status === 401) {
    // Non autorisé - rediriger vers login
    window.location.href = '/login';
    throw new Error('Session expirée');
  }
  
  if (response.status === 403) {
    // Interdit
    throw new Error('Accès refusé');
  }
  
  if (response.status === 404) {
    // Non trouvé
    throw new Error('Ressource non trouvée');
  }
  
  if (response.status === 429) {
    // Trop de requêtes
    const retryAfter = response.headers.get('Retry-After') || 60;
    throw new Error(`Trop de requêtes. Réessayez dans ${retryAfter} secondes.`);
  }
  
  if (!response.ok) {
    const error = await response.text();
    throw new Error(`HTTP ${response.status}: ${error}`);
  }
  
  return response;
}

Options Avancées

Mode CORS :

fetch('https://api.externe.com/data', {
  mode: 'cors', // Défaut pour les URLs externes
  credentials: 'include' // Inclure les cookies
});

Timeout manuel :

function fetchAvecTimeout(url, options = {}, timeout = 10000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), timeout)
    )
  ]);
}

Upload de fichier :

async function uploaderPreuve(casId, fichier) {
  const formData = new FormData();
  formData.append('preuve', fichier);
  formData.append('cas_id', casId);
  formData.append('description', 'Photo de la scène');
  
  const response = await fetch('https://api.surveillance.local/preuves/upload', {
    method: 'POST',
    body: formData
    // Pas de Content-Type header pour FormData
  });
  
  return response.json();
}

// Utilisation
const inputFichier = document.querySelector('#preuve-input');
inputFichier.addEventListener('change', async (event) => {
  const fichier = event.target.files[0];
  if (fichier) {
    const result = await uploaderPreuve(123, fichier);
    console.log('Fichier uploadé:', result);
  }
});

Streaming de réponse :

// Pour les grosses réponses
async function lireFluxCas() {
  const response = await fetch('https://api.surveillance.local/cas/stream');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value);
    const lignes = chunk.split('\n');
    
    for (const ligne of lignes) {
      if (ligne.trim()) {
        try {
          const cas = JSON.parse(ligne);
          afficherCasEnTempsReel(cas);
        } catch (e) {
          // Ignorer les lignes incomplètes
        }
      }
    }
  }
}

Gestion d'Erreurs Complète

class APIError extends Error {
  constructor(message, status, data) {
    super(message);
    this.name = 'APIError';
    this.status = status;
    this.data = data;
  }
}

class APIClient {
  constructor(baseURL, options = {}) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...options.headers
    };
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const headers = {
      ...this.defaultHeaders,
      ...options.headers
    };
    
    const config = {
      ...options,
      headers
    };
    
    try {
      const response = await fetch(url, config);
      
      // Gérer les réponses d'erreur
      if (!response.ok) {
        let errorData;
        try {
          errorData = await response.json();
        } catch {
          errorData = { message: await response.text() };
        }
        
        throw new APIError(
          errorData.message || `HTTP ${response.status}`,
          response.status,
          errorData
        );
      }
      
      // Gérer les réponses vides (204 No Content)
      if (response.status === 204) {
        return null;
      }
      
      // Détecter le type de contenu
      const contentType = response.headers.get('content-type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      } else {
        return await response.text();
      }
      
    } catch (error) {
      // Distinguer les erreurs réseau des erreurs API
      if (error instanceof APIError) {
        throw error;
      } else {
        throw new APIError(
          `Erreur réseau: ${error.message}`,
          0,
          { originalError: error.message }
        );
      }
    }
  }
  
  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }
  
  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// Utilisation
const api = new APIClient('https://api.surveillance.local');

try {
  // GET
  const cas = await api.get('/cas/123');
  
  // POST
  const nouveauTemoin = await api.post('/temoins', {
    nom: "Smith",
    deposition: "..."
  });
  
  // PUT
  await api.put(`/cas/${cas.id}`, {
    ...cas,
    statut: 'résolu'
  });
  
} catch (error) {
  if (error.status === 401) {
    // Rediriger vers login
    window.location.href = '/login?expired=true';
  } else if (error.status === 404) {
    showNotification('Cas non trouvé', 'error');
  } else {
    console.error('Erreur API:', error);
    showNotification(`Erreur: ${error.message}`, 'error');
  }
}

Cas Pratique : Système de Surveillance

class SurveillanceSystem {
  constructor(apiBase = 'https://api.surveillance.local') {
    this.api = new APIClient(apiBase);
    this.socket = null;
    this.realtimeCallbacks = [];
  }
  
  // Cas
  async getCas(id) {
    return this.api.get(`/cas/${id}`);
  }
  
  async searchCas(criteres) {
    return this.api.get('/cas/search', {
      headers: {
        'X-Search-Params': JSON.stringify(criteres)
      }
    });
  }
  
  async createCas(casData) {
    return this.api.post('/cas', casData);
  }
  
  async updateCas(id, updates) {
    return this.api.patch(`/cas/${id}`, updates);
  }
  
  async addEvidence(casId, evidence) {
    const formData = new FormData();
    formData.append('file', evidence.file);
    formData.append('description', evidence.description);
    formData.append('cas_id', casId);
    
    return fetch(`${this.api.baseURL}/cas/${casId}/evidence`, {
      method: 'POST',
      body: formData
    });
  }
  
  // Témoins
  async getTemoin(id) {
    return this.api.get(`/temoins/${id}`);
  }
  
  async getTemoinsParCas(casId) {
    return this.api.get(`/cas/${casId}/temoins`);
  }
  
  async protegerTemoin(temoinId) {
    return this.api.post(`/temoins/${temoinId}/protection`, {
      niveau: 'maximum',
      date_debut: new Date().toISOString()
    });
  }
  
  // Réal-time updates
  connectRealtime() {
    this.socket = new WebSocket('wss://api.surveillance.local/ws');
    
    this.socket.onopen = () => {
      console.log('Connecté aux mises à jour en temps réel');
      this.socket.send(JSON.stringify({
        type: 'subscribe',
        channels: ['cas_updates', 'alertes']
      }));
    };
    
    this.socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      this.realtimeCallbacks.forEach(callback => callback(data));
    };
    
    this.socket.onclose = () => {
      console.log('Déconnecté, reconnexion dans 5s...');
      setTimeout(() => this.connectRealtime(), 5000);
    };
  }
  
  onRealtimeUpdate(callback) {
    this.realtimeCallbacks.push(callback);
  }
  
  // Export des données
  async exportCas(id, format = 'json') {
    const response = await this.api.get(`/cas/${id}/export?format=${format}`);
    
    if (format === 'json') {
      const blob = new Blob([JSON.stringify(response, null, 2)], {
        type: 'application/json'
      });
      return blob;
    } else if (format === 'pdf') {
      return new Blob([response], { type: 'application/pdf' });
    }
  }
  
  // Statistiques
  async getStats(periode = '30j') {
    return this.api.get(`/stats?periode=${periode}`);
  }
}

// Utilisation
const surveillance = new SurveillanceSystem();

// S'abonner aux mises à jour
surveillance.onRealtimeUpdate((data) => {
  if (data.type === 'cas_update') {
    console.log(`Cas ${data.cas_id} mis à jour:`, data.changes);
    updateUI(data.cas_id, data.changes);
  }
  
  if (data.type === 'alerte') {
    showAlert(data.message, data.niveau);
  }
});

// Recherche avancée
const resultats = await surveillance.searchCas({
  statut: 'ouvert',
  date_debut: '2024-01-01',
  priorite: ['haute', 'critique'],
  assigne_a: 'marlowe'
});

// Export d'un cas
const cas = await surveillance.getCas(123);
const pdfBlob = await surveillance.exportCas(123, 'pdf');

// Téléchargement
const url = URL.createObjectURL(pdfBlob);
const a = document.createElement('a');
a.href = url;
a.download = `cas-${cas.id}-${cas.titre}.pdf`;
a.click();
URL.revokeObjectURL(url);

Conclusion du Module

JSON et Fetch sont les artères du web moderne.
JSON transporte les données. Fetch les fait circuler.

Les règles du coursier :

  1. Toujours valider les entrées - JSON.parse peut échouer
  2. Toujours gérer les erreurs - Les réseaux sont capricieux
  3. Toujours vérifier les statuts HTTP - 200 ≠ succès dans tous les cas
  4. Toujours sérialiser avec JSON.stringify avant l'envoi
  5. Toujours utiliser async/await pour la lisibilité

Maintenant, tu peux faire parler les services entre eux.
Faire circuler l'information.
La ville est connectée.

**Prochain module : LE DOM **