Build a Simple Next.js Interactive Web-Based Weather App

Written by

in

In today’s fast-paced digital world, weather information is readily accessible and incredibly useful. From planning your day to staying safe during extreme weather events, knowing the current and future conditions is essential. This tutorial will guide you through building a simple, yet functional, weather application using Next.js, a powerful React framework for building modern web applications. We’ll explore how to fetch data from a weather API, display it in a user-friendly format, and even add some basic styling to make it visually appealing. This project is perfect for beginners and intermediate developers looking to expand their skills in front-end development and API integration.

Why Build a Weather App?

Creating a weather app offers several benefits. Firstly, it provides a practical application of fundamental web development concepts, such as fetching data from APIs, handling asynchronous operations, and rendering dynamic content. Secondly, it allows you to learn about state management and component composition in React. Finally, it provides a tangible project that you can add to your portfolio, showcasing your ability to build interactive and data-driven web applications. This particular project will focus on simplicity and clarity, making it an excellent starting point for those new to Next.js or front-end development in general.

Prerequisites

Before we begin, ensure you have the following installed on your system:

  • Node.js and npm (Node Package Manager) or yarn installed.
  • A code editor (e.g., VS Code, Sublime Text, Atom).
  • Basic knowledge of HTML, CSS, and JavaScript.
  • Familiarity with React is helpful but not strictly required.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app weather-app

This command will create a new directory called `weather-app` with all the necessary files to get you started. Navigate into your project directory:

cd weather-app

Next, start the development server:

npm run dev

or

yarn dev

This will start the development server, typically on `http://localhost:3000`. You should see the default Next.js welcome page in your browser. Now, let’s clean up the default files to prepare for our weather application.

Project Structure and File Cleanup

The initial project structure created by `create-next-app` will look something like this:

weather-app/
├── node_modules/
├── pages/
│   ├── _app.js
│   ├── api/
│   │   └── hello.js
│   └── index.js
├── public/
│   └── ...
├── styles/
│   ├── globals.css
│   └── Home.module.css
├── .gitignore
├── next.config.js
├── package-lock.json
├── package.json
└── README.md

We’ll mainly be working within the `pages` directory. Let’s start by removing the contents of `pages/index.js` and replacing them with a basic structure for our weather app. Also, we will remove the `styles/Home.module.css` file as we will use a different approach for styling.

In `pages/index.js`, replace the existing code with the following:

import { useState, useEffect } from 'react';

function HomePage() {
  const [weatherData, setWeatherData] = useState(null);
  const [city, setCity] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // You can add a default city here if you want
    // fetchWeather('London'); 
  }, []);

  const fetchWeather = async (city) => {
    setLoading(true);
    setError(null);
    try {
      const apiKey = process.env.NEXT_PUBLIC_WEATHER_API_KEY; // Store your API key securely
      const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
      const response = await fetch(apiUrl);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      setWeatherData(data);
      setError(null);
    } catch (error) {
      setError(error.message);
      setWeatherData(null);
    } finally {
      setLoading(false);
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    await fetchWeather(city);
  };

  return (
    <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
      <h1>Weather App</h1>
      <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
        <input
          type="text"
          placeholder="Enter city"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
        />
        <button type="submit" style={{ padding: '8px 16px', borderRadius: '4px', backgroundColor: '#0070f3', color: 'white', border: 'none', cursor: 'pointer' }}>
          Search
        </button>
      </form>

      {loading && <p>Loading...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}

      {weatherData && (
        <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '8px' }}>
          <h2>{weatherData.name}, {weatherData.sys.country}</h2>
          <p>Temperature: {weatherData.main.temp} °C</p>
          <p>Weather: {weatherData.weather[0].description}</p>
          <p>Humidity: {weatherData.main.humidity}%</p>
          <p>Wind Speed: {weatherData.wind.speed} m/s</p>
        </div>
      )}
    </div>
  );
}

export default HomePage;

This code sets up the basic structure of our weather app, including a form to input the city, a loading state, an error state, and a display area for the weather data. We’ve also included placeholders for the weather data display.

Fetching Weather Data from an API

The core functionality of our weather app is fetching data from a weather API. We’ll use the OpenWeatherMap API for this tutorial. You’ll need to sign up for a free API key at https://openweathermap.org/api. Once you have your API key, you should store it securely. A good practice in Next.js is to use environment variables.

Create a `.env.local` file in the root of your project. This file is where you will store your API key. Add the following line, replacing `YOUR_API_KEY` with your actual API key:

NEXT_PUBLIC_WEATHER_API_KEY=YOUR_API_KEY

The `NEXT_PUBLIC_` prefix makes the variable accessible in the browser, which is necessary for our client-side fetching. Make sure to add `.env.local` to your `.gitignore` file if you’re using version control, to avoid committing your API key to a public repository.

Now, let’s modify the `fetchWeather` function in `pages/index.js` to fetch the weather data from the OpenWeatherMap API. The updated function should look like this:

  const fetchWeather = async (city) => {
    setLoading(true);
    setError(null);
    try {
      const apiKey = process.env.NEXT_PUBLIC_WEATHER_API_KEY; // Access the API key
      const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
      const response = await fetch(apiUrl);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      setWeatherData(data);
      setError(null);
    } catch (error) {
      setError(error.message);
      setWeatherData(null);
    } finally {
      setLoading(false);
    }
  };

This function takes the city name as an argument, constructs the API URL using the API key, and then fetches the data using the `fetch` API. It also includes error handling and a loading state to provide a better user experience.

Displaying Weather Data

Once we have fetched the weather data, we need to display it to the user. Inside the `HomePage` component, we’ll conditionally render the weather data based on the `weatherData` state. We’ll also handle the loading and error states.

Here’s how you can modify the return statement of your `HomePage` component to display the weather information:


  return (
    <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
      <h1>Weather App</h1>
      <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
        <input
          type="text"
          placeholder="Enter city"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
        />
        <button type="submit" style={{ padding: '8px 16px', borderRadius: '4px', backgroundColor: '#0070f3', color: 'white', border: 'none', cursor: 'pointer' }}>
          Search
        </button>
      </form>

      {loading && <p>Loading...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}

      {weatherData && (
        <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '8px' }}>
          <h2>{weatherData.name}, {weatherData.sys.country}</h2>
          <p>Temperature: {weatherData.main.temp} °C</p>
          <p>Weather: {weatherData.weather[0].description}</p>
          <p>Humidity: {weatherData.main.humidity}%</p>
          <p>Wind Speed: {weatherData.wind.speed} m/s</p>
        </div>
      )}
    </div>
  );

This code will display the city name, temperature, weather description, humidity, and wind speed. The `&&` operator is used for conditional rendering; the weather data will only be displayed if `weatherData` is not null.

Adding Basic Styling

To improve the visual appeal of our weather app, let’s add some basic styling. We’ll use inline styles in this example for simplicity. For larger projects, consider using CSS modules, styled-components, or a CSS framework like Tailwind CSS.

Here’s how you can add some basic styling to the component (the full code is included above, but here are the relevant parts):


    <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
      <h1 style={{ textAlign: 'center', marginBottom: '20px' }}>Weather App</h1>
      <form onSubmit={handleSubmit} style={{ marginBottom: '20px', display: 'flex', justifyContent: 'center' }}>
        <input
          type="text"
          placeholder="Enter city"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          style={{ padding: '8px', marginRight: '10px', borderRadius: '4px', border: '1px solid #ccc', width: '200px' }}
        />
        <button type="submit" style={{ padding: '8px 16px', borderRadius: '4px', backgroundColor: '#0070f3', color: 'white', border: 'none', cursor: 'pointer' }}>
          Search
        </button>
      </form>

      {loading && <p style={{ textAlign: 'center' }}>Loading...</p>}
      {error && <p style={{ color: 'red', textAlign: 'center' }}>Error: {error}</p>}

      {weatherData && (
        <div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '8px', maxWidth: '400px', margin: '0 auto' }}>
          <h2 style={{ textAlign: 'center' }}>{weatherData.name}, {weatherData.sys.country}</h2>
          <p>Temperature: {weatherData.main.temp} °C</p>
          <p>Weather: {weatherData.weather[0].description}</p>
          <p>Humidity: {weatherData.main.humidity}%</p>
          <p>Wind Speed: {weatherData.wind.speed} m/s</p>
        </div>
      )}
    </div>

This styling includes a basic font, padding, and centered text for the title and loading/error messages. The form is also styled, and the weather data display is given a border and padding. The addition of `maxWidth` and `margin: 0 auto` helps to center the weather data on the screen.

Handling Errors

Error handling is crucial for a robust application. Our code already includes basic error handling, but let’s review it and consider potential improvements.

The `fetchWeather` function includes a `try…catch` block to handle errors that may occur during the API call. If the API call fails (e.g., due to a network error or an invalid city name), the `catch` block will be executed, and the error message will be displayed to the user.

Common errors and how to handle them:

  • Invalid City Name: The API might return an error if the city name is not found. The `catch` block in our code currently handles this. Consider providing more specific error messages to the user.
  • Network Errors: If there’s a problem with the internet connection, the `fetch` call might fail. The `catch` block will also handle this.
  • API Rate Limits: Some APIs have rate limits, which means you can only make a certain number of requests within a given time. If you exceed the rate limit, the API will return an error. You might need to implement logic to handle rate limiting, such as displaying a message to the user or retrying the request after a delay. This is less of a concern for this simple app, but important in real-world scenarios.
  • API Key Errors: If your API key is invalid or has expired, the API will return an error. Double-check your API key and ensure it’s correctly configured in your `.env.local` file.

Here’s a slightly enhanced error handling example:


const fetchWeather = async (city) => {
  setLoading(true);
  setError(null);
  try {
    const apiKey = process.env.NEXT_PUBLIC_WEATHER_API_KEY;
    const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;
    const response = await fetch(apiUrl);

    if (!response.ok) {
      const errorData = await response.json(); // Attempt to parse the error response
      let errorMessage = `HTTP error! Status: ${response.status}`;
      if (errorData.message) {
        errorMessage = `Error: ${errorData.message}`; // Use the API's error message if available
      }
      throw new Error(errorMessage);
    }

    const data = await response.json();
    setWeatherData(data);
    setError(null);
  } catch (error) {
    setError(error.message);
    setWeatherData(null);
  } finally {
    setLoading(false);
  }
};

This improved version tries to parse the JSON response from the API, and if there’s a `message` field in the response (which is common for API errors), it uses that message to provide more specific feedback to the user.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building a weather app and how to fix them:

  • Incorrect API Key: Double-check that your API key is correct and that you’ve stored it securely in your `.env.local` file. Make sure you’ve restarted your development server after adding the API key.
  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, it means the API is not configured to allow requests from your domain. This is less likely with the OpenWeatherMap API, but if it happens, you might need to use a proxy server or configure CORS on the API side (if possible). For development, you might be able to use a browser extension to disable CORS temporarily, but this is not a solution for production.
  • Incorrect API URL: Verify that the API URL is correct, including the city name and API key. Typos in the URL are a common source of errors.
  • Missing Dependencies: Ensure you have installed all the necessary dependencies. If you’re using a package, run `npm install` or `yarn install` in your project directory.
  • Asynchronous Operations: Remember that `fetch` is an asynchronous operation. You need to use `async/await` or `.then()` to handle the response from the API correctly.
  • State Management: Make sure you are correctly updating the state variables (`weatherData`, `loading`, `error`) using `useState`. Incorrect state updates can lead to unexpected behavior.

Enhancements and Future Improvements

Once you’ve built the basic weather app, you can add many enhancements to make it more feature-rich and user-friendly. Here are some ideas:

  • Add a Search History: Store the last few searched cities in local storage and allow the user to quickly re-select them.
  • Implement Unit Conversion: Allow the user to switch between Celsius and Fahrenheit.
  • Display the Weather Icon: The OpenWeatherMap API provides weather icons. Display the appropriate icon based on the weather condition.
  • Add a Forecast: Display a multi-day weather forecast.
  • Implement Geolocation: Use the browser’s geolocation API to automatically detect the user’s location.
  • Improve Styling: Use CSS modules, styled-components, or a CSS framework like Tailwind CSS to create a more polished UI.
  • Add Error Handling for Empty Input: Prevent the user from submitting an empty search query.
  • Optimize for Mobile: Make the app responsive and ensure it looks good on different screen sizes.

Summary / Key Takeaways

In this tutorial, we’ve successfully built a simple weather application using Next.js. We’ve covered the essential steps, from setting up a Next.js project to fetching data from an API and displaying it to the user. You’ve learned how to use the `useState` and `useEffect` hooks, how to handle asynchronous operations with `async/await`, and how to implement basic error handling. The project demonstrates a practical application of front-end development principles, providing a foundation for building more complex web applications. Remember to store your API keys securely and consider adding enhancements to improve the user experience and functionality.

This weather app is more than just a functional tool; it’s a testament to the power and flexibility of Next.js and the React ecosystem. By understanding the fundamentals demonstrated here – fetching data, managing state, and rendering dynamic content – you’ve equipped yourself with valuable skills applicable to a wide range of web development projects. The journey doesn’t end here; consider the enhancements suggested, explore the OpenWeatherMap API documentation, and continue to experiment and refine your skills. The ability to build interactive, data-driven applications is a highly sought-after skill, and this project is a solid step in that direction.