Kurs vibe coding z AI · Moduł 9: Discord Clone
API Integration - External Services
W tej lekcji2
Pirate's Cove ma już serwery i rangi, ale żyje we własnym świecie. Załoga chce sprawdzić pogodę przed wyprawą bez wychodzenia z aplikacji, a gildia chce sprzedawać rangę VIP za prawdziwe pieniądze. Obu rzeczy nie budujesz od zera: korzystasz z zewnętrznych API, czyli usług, z którymi Twoja aplikacja rozmawia przez sieć.
Pogoda z zewnętrznego API
OpenWeatherMap zwraca aktualną pogodę dla podanego miasta. Funkcja getWeather pobiera dane, a komponent WeatherWidget je wyświetla:
async function getWeather(city: string) {
const res = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_KEY`
);
return res.json();
}
function WeatherWidget() {
const [weather, setWeather] = useState(null);
const [city, setCity] = useState("Warsaw");
useEffect(() => {
getWeather(city).then(setWeather);
}, [city]);
if (!weather) return <div>Loading...</div>;
return (
<div className="border rounded p-4">
<h3 className="font-bold">{weather.name}</h3>
<p className="text-4xl">{Math.round(weather.main.temp - 273.15)}°C</p>
<p>{weather.weather[0].description}</p>
</div>
);
}Pobieranie ma dwa kroki: await fetch(url) wysyła zapytanie i czeka na odpowiedź, a res.json() zamienia jej treść na obiekt. Opis pogody leży w tablicy, więc odczytujesz go przez weather.weather[0].description. Temperatura przychodzi w kelwinach, stąd odejmowanie 273.15, a parametr units=metric w adresie zwróciłby od razu stopnie Celsjusza.
useEffect ma w tablicy zależności [city], więc ponownie pobiera pogodę za każdym razem, gdy zmieni się miasto. Dopóki weather ma wartość null, komponent pokazuje napis o ładowaniu. OpenWeather oznaczył zapytania po nazwie miasta jako przestarzałe: nadal działają, ale w nowym kodzie lepiej najpierw zamienić nazwę na współrzędne przez ich Geocoding API.
Jedna pułapka: fetch nie zgłasza błędu przy odpowiedzi 401 czy 404, odrzuca tylko błędy sieci. Status trzeba sprawdzić samemu:
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Weather API: ${res.status}`);
}Bez tej kontroli zły klucz skończy się błędem dopiero tam, gdzie komponent czyta weather.main.temp, zamiast czytelnym komunikatem.
Rada Kapitana Redbearda: klucz API wpisany w kod komponentu zobaczy każdy, kto otworzy narzędzia przeglądarki. Zapytanie z kluczem wykonuj na serwerze, na przykład w Route Handlerze, a klucz trzymaj w zmiennej środowiskowej bez przedrostka NEXT_PUBLIC_, bo tylko takie zmienne nie trafiają do przeglądarki.
Płatności przez Stripe
Do płatności instalujesz dwie paczki: Stripe.js i komponenty dla Reacta:
npm install @stripe/stripe-js @stripe/react-stripe-jsFormularz karty składa się z kilku elementów biblioteki Stripe, które działają razem w jednym komponencie:
import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe('pk_test_...');
function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement!
});
if (!error) {
console.log('Payment Method:', paymentMethod);
}
};
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit" disabled={!stripe}>
Pay
</button>
</form>
);
}
function Payment() {
return (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
}Kroki idą w tej kolejności: loadStripe('pk_test_...') tworzy stripePromise, formularz owijamy w <Elements stripe={stripePromise}>, przy wysłaniu pobieramy pole karty przez elements.getElement(CardElement), a na koniec wywołujemy stripe.createPaymentMethod({ type: 'card', card }). Klucz z przedrostkiem pk_test_ jest publiczny i bezpieczny w przeglądarce, a dane karty trafiają prosto do Stripe, nie do Twojego serwera.
Ważne, czego ten kod nie robi: createPaymentMethod nie pobiera pieniędzy. Zamienia dane karty na obiekt PaymentMethod, który przekazujesz serwerowi, a płatność zleca dopiero serwer z tajnym kluczem, tworząc PaymentIntent. W trybie testowym użyjesz karty 4242 4242 4242 4242 z dowolną przyszłą datą i dowolnym CVC. Stripe oznacza dziś CardElement jako starszą integrację i w nowych projektach poleca Payment Element, który obsługuje też portfele i inne metody płatności, ale przepływ zostaje podobny.
Zapamiętaj: integracja z zewnętrznym API to zapytanie, sprawdzenie odpowiedzi i bezpieczne miejsce na klucze. Edytor poniżej to jeszcze wyświetlacz aktywności z lekcji o rich presence, w którym dane gry są wpisane na sztywno w obiekt activity, a w prawdziwej aplikacji przyszłyby z API tak jak pogoda. Potem rozwiąż zadania.
Kod do tej lekcji: App.jsx
// Rich presence - aktywność w grze
const activity = {
type: 'playing',
name: 'Sea of Thieves',
details: 'W bitwie',
state: '4/4 graczy',
timestamps: {
start: Date.now() - 1800000 // 30 minut temu
},
assets: {
largeImage: '',
largeText: 'Sea of Thieves'
},
party: {
size: [4, 4]
}
};
function getElapsedTime(startTime) {
const elapsed = Date.now() - startTime;
const minutes = Math.floor(elapsed / 60000);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `Czas gry: ${hours}:${(minutes % 60).toString().padStart(2, '0')}`;
}
return `Czas gry: ${minutes}:${Math.floor((elapsed % 60000) / 1000).toString().padStart(2, '0')}`;
}
export default function App() {
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="max-w-md mx-auto">
{/* Profil użytkownika */}
<div className="bg-gray-800 rounded-lg overflow-hidden">
<div className="h-24 bg-gradient-to-r from-blue-600 to-purple-600" />
<div className="p-4">
<div className="flex items-start gap-3 -mt-8 mb-4">
<div className="relative text-6xl">
◆
<div className="absolute bottom-0 right-0 w-6 h-6 rounded-full border-4 border-gray-800 bg-green-500"></div>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-1">Kapitan Jack</h3>
<p className="text-sm text-gray-400 mb-4">CaptainJack#1234</p>
{/* Aktywność */}
<div className="mb-4">
<h4 className="text-xs font-semibold text-gray-400 uppercase mb-2">Aktywność</h4>
<div className="bg-gray-700 rounded p-3">
<div className="flex items-start gap-3">
<div className="text-5xl">{activity.assets.largeImage}</div>
<div className="flex-1">
<div className="text-xs font-semibold text-gray-400 mb-1">
Gra w
</div>
<div className="font-semibold text-white text-sm mb-1">
{activity.name}
</div>
<div className="text-xs text-gray-400">{activity.details}</div>
<div className="text-xs text-gray-400">{activity.state}</div>
<div className="text-xs text-gray-400 mt-1">
{activity.party.size[0]} z {activity.party.size[1]} w drużynie
</div>
<div className="text-xs text-gray-400 mt-1">
{getElapsedTime(activity.timestamps.start)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jak w funkcji getWeather pobierane są dane z zewnętrznego Weather API?
2. Dlaczego w WeatherWidget useEffect ma w tablicy zależności [city]?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż dostęp do opisu pogody: weather.weather[0].description
- Układanie w pionie
Ułóż kroki obsługi płatności w CheckoutForm (Stripe):