DEV Community

Cover image for Build a Weather Widget Using Next.js
Michael La Posta
Michael La Posta

Posted on • Originally published at wheresbaldo.dev

Build a Weather Widget Using Next.js

In this Next.js tutorial, we'll learn how to create a simple weather widget using Next.js, React, and OpenWeatherMap.

I'll be keeping things simple, so styling will be done using plain old CSS, and the weather API calls will use the built-in fetch API.

Note: You can download the final code for this tutorial from my GitHub repo.

Why use Next.js?

This widget is easily doable using basic React, and for the most part, the code will be just basic React. However, we'll need a way to allow backend-only access to our OpenWeatherMap API key (for security purposes) and make the API calls, and Next.js provides us with an integrated way to do this by utilizing the pages/api folder.

Of course, alternatively, if you were using Node.js or another backend (Java, PHP, Python, Ruby, etc.), you would just place all of your API calls there instead of using the backend we'll be creating. That however is outside the scope of this tutorial, and so we'll be sticking with a full Next JS build for this one.

Alright, let's get started!

Bootstrap the Project (optional)

If you're using an existing project, then you've already bootstrapped your project, and can skip ahead to the Setup your OpenWeatherMap API Key section below.

Otherwise, the first step will be to set up the project boilerplate.

1. Create the Project

From the CLI, using yarn, run the following command:

yarn create next-app weather-widget --example with-typescript
Enter fullscreen mode Exit fullscreen mode

Or, if you're using npx, you can use the following:

npx create-next-app weather-widget --example with-typescript
Enter fullscreen mode Exit fullscreen mode

Then, when prompted, use the following settings:

✔ Would you like to use TypeScript? … [Yes]
✔ Would you like to use ESLint? … [Yes]
✔ Would you like to use Tailwind CSS? … [No]
✔ Would you like to use `src/` directory? … [No]
✔ Would you like to use App Router? (recommended)[No]
✔ Would you like to customize the default import alias (@/*)? … [No]
Enter fullscreen mode Exit fullscreen mode

Note: If you're using Yarn's PnP (no node_modules folder) along with VS Code as your IDE and are seeing "Cannot find module ..." errors in your TS files after the install, see my post here on how to fix those errors.

2. Delete Unnecessary Files

Delete the following files (but not the folders):

  • public/*
  • styles/*
  • pages/api/*
  • README.md

3. Delete Unnecessary Code

In pages/_app.tsx, delete the following import:

// pages/_app.tsx

import '../styles/globals.css';
Enter fullscreen mode Exit fullscreen mode

Setup your OpenWeatherMap API Key

OpenWeatherMap offers a free API tier, which allows for up to 60 calls per minute, and max 1,000,000 calls per month. So we've got plenty for the purposes of this tutorial.

To get your free key, go to openweathermap.org and create an account.

Once you've created your account:

  1. Go to the API Keys page and copy your API key.
  2. Create a new file in your weather widget project root folder called .env.local and add the following code, replacing [YOUR_API_KEY_HERE] with the API key you obtained from OpenWeatherMap:
   # .env.local

   OPENWEATHERMAP_API_KEY=[YOUR_API_KEY_HERE]
Enter fullscreen mode Exit fullscreen mode

Create the Weather Data API

The weather API is where we need Next.js to come in and help us out.

We'll be creating a basic API that will allow us to make calls to OpenWeatherMap from the frontend, without exposing our API key, by routing those calls through our backend (.js or .ts files in the pages/api folder).

In the pages/api folder, create a file called weather.ts, and add the following code:

// pages/api/weather.ts

// import the Next.js request and response types
import { NextApiRequest, NextApiResponse } from 'next';

// import the OpenWeatherMap API key from the .env.local file
const apiKey = process.env.OPENWEATHERMAP_API_KEY;
const apiUrl = 'https://api.openweathermap.org/data/2.5/weather';

export default async (req: NextApiRequest, res: NextApiResponse) => {
  // extract the query parameters from the request object
  const { q, lat, lon } = req.query;

  // if the API key isn't found in the .env.local file,
  // return a 500 error to the frontend
  if (!apiKey) {
    res.status(500).json({ error: 'API key not found.' });
    return;
  }

  // if either the city name (q), or coordinates (lat and lon) aren't
  // provided, return a 400 error to the frontend
  if (!q && (!lat || !lon)) {
    res.status(400).json({ error: 'Please provide either city or coordinates.' });
    return;
  }

  // because this is running server-side, we're wrapping the API call
  // in a try / catch block to catch any errors that may occur,
  // and if so, return a 500 error to the frontend
  try {
    const query = q ? `q=${q}` : `lat=${lat}&lon=${lon}`;
    const response = await fetch(`${apiUrl}?${query}&appid=${apiKey}&units=metric`);
    const data = await response.json();

    res.status(200).json(data);
  } catch (error: unknown) {
    if (error instanceof Error) {
      res.status(500).json({ error: error.message });
    } else {
      res
        .status(500)
        .json({
          error: error || 'Server error while trying to fetch weather data',
        });
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Just like Node.js, Next.js gives us access to the process.env object, which allows us to access environment variables, like our OPENWEATHERMAP_API_KEY constant.

After a few basic checks (check that the query parameter was passed with either city name q or coords lat and lon), we then use the fetch function to make the API call to OpenWeatherMap, and return the data to the frontend via the response method.

If the call was successful, we return the data to the frontend with an HTTP 200 status code, otherwise we return an error message with the generic HTTP server error code 500.

Define the Widget Styling

Next up, we'll define the styling for our widget.

In a project this small, we could just use a regular CSS file, which would make the CSS classes global.

I want to scope the CSS to the widget only though, which will avoid possible naming conflicts with other CSS files that could occur in larger projects.

So in the styles folder, create a CSS module file called weather.module.css, and add the following CSS:

/* styles/weather.module.css */

.weatherWidget {
  border: 1px solid #ccc;
  border-radius: 10px;
  max-width: 200px;
  padding: 20px;
  margin: 10px;
  text-align: center;
  background-color: #ccc;
}

.currentWeather {
  margin: 0 auto;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
}
.currentWeather div {
  display: inline-block;
  font-size: 2.5rem;
}

.feelsLike {
  font-size: 0.9rem;
  font-style: italic;
}

.weather {
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

Create the Weather Widget Component

Now it's time to create the actual widget component.

In your project root folder, create a new folder called components, and then inside that folder, either using bash or your code editor, create a file called weatherWidget.tsx.

In weatherWidget.tsx, add the following code:

// components/weatherWidget.tsx

import React, { useState, useEffect } from 'react';

import styles from '../styles/widget.module.css';

interface WeatherWidgetProps {
  city?: string;
  coordinates?: { lat: number; lon: number };
}

interface WeatherData {
  name: string;
  main: {
    temp: number;
    feels_like: number;
  };
  weather: {
    description: "string;"
    icon: string;
  }[];
}

const WeatherWidget: React.FC<WeatherWidgetProps> = ({ city, coordinates }) => {
  const [weatherData, setWeatherData] = useState<WeatherData | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        let query = '';

        if (city) {
          query = `q=${city}`;
        } else if (coordinates) {
          query = `lat=${coordinates.lat}&lon=${coordinates.lon}`;
        } else {
          console.error('Please provide either city or coordinates.');
          return;
        }

        const response = await fetch(`/api/weather?${query}`);
        const data: WeatherData = await response.json();

        setWeatherData(data);
      } catch (error) {
        console.error('Error fetching weather data:', error);
      }
    };

    fetchData();
  }, [city, coordinates]);

  return (
    <div className={styles.weatherWidget}>
      {!weatherData ? (
        <div>Loading weather ...</div>
      ) : (
        <>
          <h2>{weatherData.name}</h2>

          <p className={styles.weather}>{weatherData.weather[0].description}</p>

          <div className={styles.currentWeather}>
            <img
              src={`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`}
              alt={weatherData.weather[0].description}
            />
            <div>{Math.round(weatherData.main.temp)}°C</div>
          </div>

          <p className={styles.feelsLike}>
            Feels like: {Math.round(weatherData.main.feels_like)}°C
          </p>
        </>
      )}
    </div>
  );
};

export default WeatherWidget;
Enter fullscreen mode Exit fullscreen mode

In the above code, we're using the useState and useEffect hooks to fetch the weather data from our API, and then display it in the widget.

We're using useEffect so that the API call can be made if / when the city or coordinates props change. So if this was in an app with a select box of cities, the widget would update automatically when the user selects a new city.

Note: I've left out proper error handling for the weather API call, opting to just log the error to the console for the sake of this tutorial.

In a real app, you'd want to handle any errors more gracefully, perhaps by displaying a message to the user, and probably by using a state variable to track the error, and then displaying the error message in the widget.

Update the Index Page

Finally, we'll update the index.tsx page to use our new widget.

Simply replace the contents of the original boilerplate in pages/index.tsx with the following code:

// pages/index.tsx

import React from 'react';
import WeatherWidget from '../components/weatherWidget';

const Home: React.FC = () => {
  return (
    <div className="App">
      {/* Example using city name */}
      <WeatherWidget city="Montreal" />

      {/* Example using coordinates */}
      {/* <WeatherWidget coordinates={{ lon: -73.5878, lat: 45.5088 }} /> */}
    </div>
  );
};

export default Home;
Enter fullscreen mode Exit fullscreen mode

In the above code, I'm using "Montreal" as the city name, but you can of course replace that with any city name you want.

If you have trouble getting the weather for a particular city, you can comment out that line, and uncomment the second example which uses the coordinates for that city instead.


And that's it for the setup and tutorial!

The Final Result

Now, if you run yarn dev (or npm run dev), you should get something like the following widget showing in your browser:

Weather Widget

Additional Thoughts

The OpenWeatherMap API is pretty robust, and there are other paid API endpoints that you can access, such as 4-day, 8-day, and 16-day forecasts, among other things.

Because I'm using the free version of the API in this post however, the access is limited to current weather and a 5-day forecast.

For further info on the free "current weather" API we're using in this tutorial, refer to the Current weather data API documentation for all the available data points.

Top comments (0)