Vibe coding with AI course Β· Module 9: Discord Clone

API Integration - External Services

4 min read
In this lesson2

Pirate's Cove already has servers and roles, but it lives in its own world. The crew wants to check the weather before a voyage without leaving the app, and the guild wants to sell a VIP role for real money. You don't build either of these from scratch: you use external APIs, services your app talks to over the network.

Weather from an external API

OpenWeatherMap returns the current weather for a given city. The getWeather function fetches the data, and the WeatherWidget component displays it:

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>
  );
}

Fetching has two steps: await fetch(url) sends the request and waits for the response, and res.json() turns its body into an object. The weather description sits in an array, so you read it with weather.weather[0].description. The temperature comes in kelvins, hence subtracting 273.15, while the units=metric parameter in the address would return degrees Celsius right away.

useEffect has [city] in its dependency array, so it fetches the weather again every time the city changes. While weather is null, the component shows a loading message. OpenWeather has marked requests by city name as deprecated: they still work, but in new code it's better to first turn the name into coordinates with their Geocoding API.

One trap: fetch doesn't report an error for a 401 or 404 response; it rejects only on network errors. You have to check the status yourself:

const res = await fetch(url);
if (!res.ok) {
  throw new Error(`Weather API: ${res.status}`);
}

Without this check, a wrong key fails only later, where the component reads weather.main.temp, instead of with a clear message.

Captain Redbeard's tip: an API key written into component code can be seen by anyone who opens the browser's developer tools. Make the request with the key on the server, for example in a Route Handler, and keep the key in an environment variable without the NEXT_PUBLIC_ prefix, because only such variables stay out of the browser.

Payments with Stripe

For payments you install two packages, Stripe.js and the React components:

npm install @stripe/stripe-js @stripe/react-stripe-js

The card form consists of several elements from the Stripe library that work together in one component:

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>
  );
}

The steps go in this order: loadStripe('pk_test_...') creates stripePromise, we wrap the form in <Elements stripe={stripePromise}>, on submit we get the card field with elements.getElement(CardElement), and finally we call stripe.createPaymentMethod({ type: 'card', card }). A key with the pk_test_ prefix is public and safe in the browser, and the card details go straight to Stripe, not to your server.

What this code doesn't do matters too: createPaymentMethod doesn't take any money. It turns the card details into a PaymentMethod object that you pass to your server, and only the server, with a secret key, initiates the payment by creating a PaymentIntent. In test mode you use the card 4242 4242 4242 4242 with any future date and any CVC. Stripe now marks CardElement as a legacy integration and recommends the Payment Element for new projects, which also supports wallets and other payment methods, but the flow stays similar.

Remember: integrating an external API means a request, a check of the response and a safe place for the keys. The editor below is still the activity display from the rich presence lesson, where the game data is hard-coded in the activity object, while in a real app it would come from an API, just like the weather. Then solve the tasks.

Code for this lesson: App.jsx
// Rich Presence - Gaming Activity
const activity = {
  type: 'playing',
  name: 'Sea of Thieves',
  details: 'In a Battle',
  state: '4/4 players',
  timestamps: {
    start: Date.now() - 1800000 // 30 minutes ago
  },
  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 `${hours}:${(minutes % 60).toString().padStart(2, '0')} elapsed`;
  }
  return `${minutes}:${Math.floor((elapsed % 60000) / 1000).toString().padStart(2, '0')} elapsed`;
}

export default function App() {
  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <div className="max-w-md mx-auto">
        {/* User Profile */}
        <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">Captain Jack</h3>
            <p className="text-sm text-gray-400 mb-4">CaptainJack#1234</p>

            {/* Activity */}
            <div className="mb-4">
              <h4 className="text-xs font-semibold text-gray-400 uppercase mb-2">Activity</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">
                      Playing
                    </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]} of {activity.party.size[1]} in party
                    </div>

                    <div className="text-xs text-gray-400 mt-1">
                      {getElapsedTime(activity.timestamps.start)}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How does the getWeather function fetch data from the external Weather API?

  2. 2. Why does useEffect in WeatherWidget have [city] in its dependency array?

Hands-on tasks in the game

  • Horizontal ordering

    Arrange access to the weather description: weather.weather[0].description

  • Vertical ordering

    Arrange the payment steps in CheckoutForm (Stripe):

Useful articles