Kurs vibe coding z AI · Moduł 9: Discord Clone

API Integration - External Services

4 min czytania
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-js

Formularz 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. 1. Jak w funkcji getWeather pobierane są dane z zewnętrznego Weather API?

  2. 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):

Przydatne artykuły