Créer une application météo avec une API gratuite en JavaScript

Une application météo paraît simple : un champ, le nom d'une ville, une température. Le navigateur ne connaît pourtant ni les coordonnées de Paris ni le temps qu'il y fait. Il faut transformer le texte saisi en latitude et longitude, demander les prévisions à un second service, puis rendre quatre états lisibles : attente, succès, ville inconnue et panne. C'est ce petit trajet complet que nous allons construire en JavaScript, sans framework.

Le résultat tient dans un fichier index.html. Il recherche une ville partout dans le monde, affiche la température, l'état du ciel et le vent actuels, et reste compréhensible si le réseau tombe. Si les formulaires sont encore nouveaux pour vous, gardez sous la main le guide du formulaire HTML : nous utiliserons sa soumission et sa validation native.

Tout le code a été exécuté le 23 septembre 2026 avec Node.js 20.19.4 et Playwright 1.63.0 dans Chromium 153.0.8010.12 sur Windows 11. Le banc a simulé une réponse nominale, une ville inconnue, une erreur réseau et une réponse météo incomplète. Les quatre scénarios passent ; les sorties exactes sont données en fin d'article.

Gratuite, mais dans un cadre précis

Nous utiliserons l'API de prévisions Open-Meteo. Elle ne demande aucune clé pour son offre libre et renvoie du JSON, ce qui la rend pratique pour apprendre. Sa gratuité n'est toutefois pas universelle : au 23 septembre 2026, l'offre gratuite est réservée aux usages non commerciaux.

Les conditions officielles d'Open-Meteo fixent aussi les plafonds à moins de 10 000 appels par jour, 5 000 par heure et 600 par minute. Les données sont sous licence CC BY 4.0, donc l'attribution visible dans notre pied de page est nécessaire. Un site avec publicité, abonnement ou usage promotionnel doit utiliser une offre commerciale ou une autre source adaptée.

Une seconde API du même service transforme « Paris » en coordonnées. La documentation du géocodage indique que le paramètre name accepte une ville ou un code postal et que count=1 limite la réponse au premier résultat. Aucun résultat ne signifie pas forcément une panne : la saisie peut simplement être inconnue.

Deux requêtes pour une seule recherche

La première requête part vers le géocodage avec le nom saisi. Elle revient avec une latitude et une longitude. La seconde envoie ces coordonnées à l'API météo et demande trois valeurs actuelles : temperature_2m, weather_code et wind_speed_10m. C'est une chaîne : la seconde requête ne peut commencer qu'après la première.

Flux vertical en quatre étapes : le formulaire envoie Paris au géocodage, les coordonnées alimentent l'API de prévisions, puis la température, l'état du ciel et le vent mettent la page à jour.
Une recherche visible déclenche deux requêtes successives. Chaque réponse fournit les paramètres de la suivante.

Le code météo est un nombre normalisé. Par exemple, 0 désigne un ciel dégagé, 2 un ciel partiellement nuageux et 95 un orage. Nous garderons une table de traduction locale plutôt que d'afficher ce nombre au visiteur.

La page pose quatre emplacements à remplir

Créez un fichier index.html avec le squelette ci-dessous. Insérez le bloc de style de la section suivante à la place du commentaire dans <head>, puis le bloc JavaScript à la place du commentaire juste avant </body>. Ainsi, les trois extraits forment bien un seul fichier directement ouvrable dans le navigateur.

<!doctype html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Météo de ma ville</title>
  <!-- Insérez ici le bloc <style> de la section suivante. -->
</head>
<body>
<main>
  <h1>Météo de ma ville</h1>
  <form id="recherche">
    <label for="ville" hidden>Ville</label>
    <input id="ville" name="ville" value="Paris" required
           autocomplete="address-level2" />
    <button type="submit">Rechercher</button>
  </form>

  <p id="statut" role="status" aria-live="polite"></p>

  <section id="resultat" hidden>
    <h2 id="lieu"></h2>
    <p class="temperature"><span id="temperature"></span> °C</p>
    <p id="description"></p>
    <p>Vent : <span id="vent"></span> km/h</p>
  </section>

  <footer>
    Données météo :
    <a href="https://open-meteo.com/" rel="noopener">Open-Meteo.com</a>
  </footer>
</main>
<!-- Insérez ici le bloc <script> de la section JavaScript. -->
</body>
</html>

aria-live="polite" demande au lecteur d'écran d'annoncer les changements sans interrompre ce qu'il lit. La section conserve hidden pendant le chargement et après une erreur : une ancienne température ne peut donc pas passer pour la réponse à la nouvelle recherche. Cette attention aux états est la même que dans le quiz interactif en JavaScript, où l'interface doit toujours refléter l'état du programme.

Un style court suffit pour rendre le projet utilisable

Placez ce style dans le <head>. La largeur reste lisible sur mobile, le formulaire utilise l'espace disponible et le thème sombre suit le réglage du système.

<style>
  :root { color-scheme: light dark; font-family: system-ui, sans-serif; }
  body {
    margin: 0; min-height: 100vh; display: grid; place-items: center;
    background: #dcecff; color: #172033;
  }
  main {
    width: min(36rem, calc(100% - 2rem)); padding: 2rem;
    box-sizing: border-box; border-radius: 1rem; background: white;
    box-shadow: 0 1rem 3rem #3453;
  }
  form { display: flex; gap: .5rem; }
  input { min-width: 0; flex: 1; padding: .75rem; font: inherit; }
  button { padding: .75rem 1rem; font: inherit; cursor: pointer; }
  #statut { min-height: 1.5em; }
  #resultat { border-top: 1px solid #ccd4e0; margin-top: 1rem; padding-top: 1rem; }
  .temperature { font-size: 2.5rem; font-weight: 700; margin: .25rem 0; }
  footer { margin-top: 2rem; font-size: .85rem; }
  @media (prefers-color-scheme: dark) {
    body { background: #101827; color: #e7edf7; }
    main { background: #1d293b; }
  }
</style>

Le JavaScript fabrique des URL sans concaténer de texte

Ajoutez le script juste avant </body>. La table associe les codes météo utiles à du texte. Une valeur future ou rare reste affichable grâce au cas de repli « Code météo 123 ».

<script>
  const descriptions = {
    0: "Ciel dégagé", 1: "Plutôt dégagé", 2: "Partiellement nuageux",
    3: "Couvert", 45: "Brouillard", 48: "Brouillard givrant",
    51: "Bruine faible", 53: "Bruine", 55: "Bruine forte",
    61: "Pluie faible", 63: "Pluie", 65: "Pluie forte",
    71: "Neige faible", 73: "Neige", 75: "Neige forte",
    80: "Averses faibles", 81: "Averses", 82: "Fortes averses",
    95: "Orage", 96: "Orage et grêle", 99: "Orage et forte grêle",
  };

  const form = document.querySelector("#recherche");
  const champVille = document.querySelector("#ville");
  const statut = document.querySelector("#statut");
  const resultat = document.querySelector("#resultat");
  const lieu = document.querySelector("#lieu");
  const temperature = document.querySelector("#temperature");
  const description = document.querySelector("#description");
  const vent = document.querySelector("#vent");

  async function lireJson(url) {
    const reponse = await fetch(url);
    if (!reponse.ok) throw new Error(`Réponse HTTP ${reponse.status}`);
    return reponse.json();
  }

  async function chercherMeteo(nomVille) {
    const geocodage = new URL(
      "https://geocoding-api.open-meteo.com/v1/search",
    );
    geocodage.search = new URLSearchParams({
      name: nomVille, count: "1", language: "fr", format: "json",
    });
    const villes = await lireJson(geocodage);
    if (!villes.results?.length) throw new Error("Ville introuvable");

    const ville = villes.results[0];
    const prevision = new URL("https://api.open-meteo.com/v1/forecast");
    prevision.search = new URLSearchParams({
      latitude: String(ville.latitude),
      longitude: String(ville.longitude),
      current: "temperature_2m,weather_code,wind_speed_10m",
      timezone: "auto",
    });
    const meteo = await lireJson(prevision);
    const actuelle = meteo.current;
    if (
      !actuelle ||
      !Number.isFinite(actuelle.temperature_2m) ||
      !Number.isFinite(actuelle.weather_code) ||
      !Number.isFinite(actuelle.wind_speed_10m)
    ) {
      throw new Error("Réponse météo incomplète");
    }
    return { ville, actuelle };
  }

  function afficher({ ville, actuelle }) {
    lieu.textContent = [ville.name, ville.country].filter(Boolean).join(", ");
    temperature.textContent = Math.round(actuelle.temperature_2m);
    description.textContent = descriptions[actuelle.weather_code]
      ?? `Code météo ${actuelle.weather_code}`;
    vent.textContent = Math.round(actuelle.wind_speed_10m);
    resultat.hidden = false;
  }

  form.addEventListener("submit", async (evenement) => {
    evenement.preventDefault();
    resultat.hidden = true;
    statut.textContent = "Chargement…";
    form.querySelector("button").disabled = true;
    try {
      afficher(await chercherMeteo(champVille.value.trim()));
      statut.textContent = "Prévisions mises à jour.";
    } catch (erreur) {
      statut.textContent = erreur.message === "Ville introuvable"
        ? "Cette ville est introuvable. Précisez le pays ou le code postal."
        : "La météo est indisponible. Réessayez dans quelques instants.";
    } finally {
      form.querySelector("button").disabled = false;
    }
  });
</script>

URLSearchParams encode correctement une saisie comme « Saint-Étienne, France ». La fonction lireJson vérifie response.ok, car fetch ne lève pas automatiquement d'erreur pour une réponse HTTP 400 ou 500. Enfin, finally réactive le bouton dans tous les cas.

Tester les pannes compte autant que tester Paris

Double-cliquer sur le fichier permet déjà de l'essayer. Pendant la rédaction, un fichier test.mjs séparé, conservé dans le banc de vérification du projet mais pas nécessaire à l'application publiée, a piloté Chromium. Pour éviter de dépendre de la météo réelle, ce banc intercepte les deux appels et fournit des réponses déterministes. Le scénario nominal renvoie 18,6 °C, le code 2 et 12,4 km/h ; l'interface doit afficher les valeurs arrondies 19 et 12.

Voici la sortie brute obtenue avec Node.js 20.19.4, Playwright 1.63.0 et Chromium 153.0.8010.12 :

OK nominal : Paris, France | 19 °C | Partiellement nuageux | vent 12 km/h
OK ville inconnue : message explicite, résultat masqué
OK erreur réseau : message explicite, bouton réactivé
OK réponse partielle : message d'indisponibilité, résultat masqué

Ce test ne prouve pas qu'un fournisseur externe sera toujours disponible. Il prouve que notre page interprète correctement le contrat attendu et qu'elle se comporte proprement quand ce contrat ne peut pas être obtenu. Pour approfondir cette frontière entre données reçues et données fiables, le guide de validation avec Zod montre comment contrôler toute la forme d'une réponse.

Ce que vous pouvez améliorer sans refaire l'application

Une prévision sur plusieurs jours demande les variables daily et une liste générée par JavaScript. Un bouton de géolocalisation peut fournir directement les coordonnées, à condition de demander l'autorisation et de conserver la recherche manuelle. Un historique peut stocker les trois dernières villes dans localStorage. Chaque ajout garde le même noyau : acquérir des coordonnées, demander des données, afficher un état.

Avant une mise en ligne, relisez surtout le cadre d'utilisation de l'API. Ce projet convient à l'apprentissage et à un site personnel non commercial dans les limites publiées le 23 septembre 2026. Si votre application devient monétisée ou très fréquentée, le choix du contrat fait partie du développement au même titre que le choix des fonctions.