Build a Weather Widget Using Next.js
Tags: nextjs, tutorial, webdev, react
Published: 2024-01-27 14:30:00

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](https://github.com/mlaposta/nextjs-weather-widget).

### 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:

```sh
yarn create next-app weather-widget --example with-typescript
```

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

```sh
npx create-next-app weather-widget --example with-typescript
```

Then, when prompted, use the following settings:

```sh
✔ 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]
```

> **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](https://wheresbaldo.dev/tech/vscode/fix-cannot-find-module-typescript-errors-in-vs-code).

### 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:

```tsx
// pages/_app.tsx

import '../styles/globals.css';
```

---

## 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](https://openweathermap.org/) and create an account.

Once you've created your account:

1. Go to the [API Keys](https://home.openweathermap.org/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:

   ```sh
   # .env.local

   OPENWEATHERMAP_API_KEY=[YOUR_API_KEY_HERE]
   ```

## 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:

```ts
// 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',
        });
    }
  }
};
```

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 ***res***ponse 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:

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

## 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:

```tsx
// 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;
```

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:

```tsx
// 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;
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwhf82qu5nz3hacdpr01.jpg)

## 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](https://openweathermap.org/current) for all the available data points.