Cours Vibe coding avec l'IA · Module 9: Clone de Discord
Intégration d'API - Services externes
Dans cette leçon2
Pirate's Cove a des serveurs et des rôles, mais vit en vase clos. L'équipage veut consulter la météo avant un voyage sans quitter l'appli, et la guilde veut vendre un rôle VIP contre de l'argent réel. Inutile de tout construire : tu utilises des API externes, des services avec lesquels ton appli dialogue sur le réseau.
La météo depuis une API externe
OpenWeatherMap renvoie la météo actuelle d'une ville donnée. La fonction getWeather récupère les données, et le composant WeatherWidget les affiche :
1async function getWeather(city: string) {
2 const res = await fetch(
3 `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_KEY`
4 );
5 return res.json();
6}
7
8function WeatherWidget() {
9 const [weather, setWeather] = useState(null);
10 const [city, setCity] = useState("Warsaw");
11
12 useEffect(() => {
13 getWeather(city).then(setWeather);
14 }, [city]);
15
16 if (!weather) return <div>Chargement...</div>;
17
18 return (
19 <div className="border rounded p-4">
20 <h3 className="font-bold">{weather.name}</h3>
21 <p className="text-4xl">{Math.round(weather.main.temp - 273.15)}°C</p>
22 <p>{weather.weather[0].description}</p>
23 </div>
24 );
25}La récupération se fait en deux étapes : await fetch(url) envoie la requête et attend la réponse, puis res.json() transforme son contenu en objet. La description de la météo se trouve dans un tableau, donc tu la lis avec weather.weather[0].description. La température arrive en kelvins, d'où la soustraction de 273.15 ; avec units=metric dans l'adresse, tu aurais directement des degrés Celsius.
useEffect a [city] dans son tableau de dépendances : il récupère à nouveau la météo chaque fois que la ville change. Tant que weather vaut null, le composant affiche un message de chargement. OpenWeather a déclaré obsolètes les requêtes par nom de ville : elles marchent encore, mais dans du code neuf, convertis d'abord le nom en coordonnées avec leur Geocoding API.
Un piège : fetch ne signale pas d'erreur pour une réponse 401 ou 404, il ne rejette que les erreurs réseau. Tu dois vérifier le statut toi-même :
1const res = await fetch(url);
2if (!res.ok) {
3 throw new Error(`Weather API: ${res.status}`);
4}Sans ce contrôle, une mauvaise clé plante plus loin, à la lecture de weather.main.temp, au lieu de donner un message clair.
Conseil du Capitaine Barbe-Rouge : une clé d'API écrite dans le code d'un composant est visible par quiconque ouvre les outils du navigateur. Fais la requête avec la clé côté serveur, par exemple dans un Route Handler, et garde la clé dans une variable d'environnement sans le préfixe NEXT_PUBLIC_, car seules ces variables restent hors du navigateur.
Les paiements avec Stripe
Pour les paiements, tu installes deux paquets : Stripe.js et les composants pour React :
1npm install @stripe/stripe-js @stripe/react-stripe-jsLe formulaire de carte se compose de plusieurs éléments de la bibliothèque Stripe qui fonctionnent ensemble dans un seul composant :
1import { loadStripe } from '@stripe/stripe-js';
2import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
3
4const stripePromise = loadStripe('pk_test_...');
5
6function CheckoutForm() {
7 const stripe = useStripe();
8 const elements = useElements();
9
10 const handleSubmit = async (e: React.FormEvent) => {
11 e.preventDefault();
12 if (!stripe || !elements) return;
13
14 const cardElement = elements.getElement(CardElement);
15 const { error, paymentMethod } = await stripe.createPaymentMethod({
16 type: 'card',
17 card: cardElement!
18 });
19
20 if (!error) {
21 console.log('Payment Method:', paymentMethod);
22 }
23 };
24
25 return (
26 <form onSubmit={handleSubmit}>
27 <CardElement />
28 <button type="submit" disabled={!stripe}>
29 Payer
30 </button>
31 </form>
32 );
33}
34
35function Payment() {
36 return (
37 <Elements stripe={stripePromise}>
38 <CheckoutForm />
39 </Elements>
40 );
41}Les étapes se suivent dans cet ordre : loadStripe('pk_test_...') crée stripePromise, on entoure le formulaire de <Elements stripe={stripePromise}>, à l'envoi on récupère le champ de carte avec elements.getElement(CardElement), et enfin on appelle stripe.createPaymentMethod({ type: 'card', card }). Une clé avec le préfixe pk_test_ est publique et sans danger dans le navigateur, et les données de carte vont directement chez Stripe, pas sur ton serveur.
Important : createPaymentMethod ne prélève aucun argent. Elle transforme les données de carte en objet PaymentMethod que tu transmets à ton serveur, et seul le serveur, avec une clé secrète, lance le paiement en créant un PaymentIntent. En mode test, tu utilises la carte 4242 4242 4242 4242 avec n'importe quelle date future et n'importe quel CVC. Stripe considère aujourd'hui CardElement comme une intégration ancienne et recommande le Payment Element pour les nouveaux projets, qui gère aussi les portefeuilles et d'autres moyens de paiement, mais le déroulé reste proche.
À retenir : intégrer une API externe, c'est une requête, une vérification de la réponse et un endroit sûr pour les clés. L'éditeur ci-dessous montre encore l'affichage d'activité de la leçon sur la présence enrichie, où les données du jeu sont écrites en dur dans l'objet activity, alors que dans une vraie appli elles viendraient d'une API, comme la météo. Ensuite, fais les exercices.
Code de cette leçon : App.jsx
1// Rich Presence - Activité de jeu
2const activity = {
3 type: 'playing',
4 name: 'Sea of Thieves',
5 details: 'In a Battle',
6 state: '4/4 players',
7 timestamps: {
8 start: Date.now() - 1800000 // il y a 30 minutes
9 },
10 assets: {
11 largeImage: '',
12 largeText: 'Sea of Thieves'
13 },
14 party: {
15 size: [4, 4]
16 }
17};
18
19function getElapsedTime(startTime) {
20 const elapsed = Date.now() - startTime;
21 const minutes = Math.floor(elapsed / 60000);
22 const hours = Math.floor(minutes / 60);
23
24 if (hours > 0) {
25 return `${hours}:${(minutes % 60).toString().padStart(2, '0')} écoulées`;
26 }
27 return `${minutes}:${Math.floor((elapsed % 60000) / 1000).toString().padStart(2, '0')} écoulées`;
28}
29
30export default function App() {
31 return (
32 <div className="min-h-screen bg-gray-100 p-8">
33 <div className="max-w-md mx-auto">
34 {/* Profil utilisateur */}
35 <div className="bg-gray-800 rounded-lg overflow-hidden">
36 <div className="h-24 bg-gradient-to-r from-blue-600 to-purple-600" />
37
38 <div className="p-4">
39 <div className="flex items-start gap-3 -mt-8 mb-4">
40 <div className="relative text-6xl">
41 ◆
42 <div className="absolute bottom-0 right-0 w-6 h-6 rounded-full border-4 border-gray-800 bg-green-500"></div>
43 </div>
44 </div>
45
46 <h3 className="text-xl font-bold text-white mb-1">Captain Jack</h3>
47 <p className="text-sm text-gray-400 mb-4">CaptainJack#1234</p>
48
49 {/* Activité */}
50 <div className="mb-4">
51 <h4 className="text-xs font-semibold text-gray-400 uppercase mb-2">Activité</h4>
52
53 <div className="bg-gray-700 rounded p-3">
54 <div className="flex items-start gap-3">
55 <div className="text-5xl">{activity.assets.largeImage}</div>
56
57 <div className="flex-1">
58 <div className="text-xs font-semibold text-gray-400 mb-1">
59 Joue à
60 </div>
61
62 <div className="font-semibold text-white text-sm mb-1">
63 {activity.name}
64 </div>
65
66 <div className="text-xs text-gray-400">{activity.details}</div>
67 <div className="text-xs text-gray-400">{activity.state}</div>
68
69 <div className="text-xs text-gray-400 mt-1">
70 {activity.party.size[0]} sur {activity.party.size[1]} dans le groupe
71 </div>
72
73 <div className="text-xs text-gray-400 mt-1">
74 {getElapsedTime(activity.timestamps.start)}
75 </div>
76 </div>
77 </div>
78 </div>
79 </div>
80 </div>
81 </div>
82 </div>
83 </div>
84 );
85}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment la fonction getWeather récupère-t-elle les données depuis l'API météo externe ?
2. Pourquoi useEffect dans WeatherWidget a-t-il [city] dans son tableau de dépendances ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne l'accès à la description météo : weather.weather[0].description
- Tri vertical
Ordonne les étapes de paiement dans CheckoutForm (Stripe) :