javascriptroom guide

Building a Weather App in Vue.js: A Practical Step-by-Step Guide

In today’s digital age, weather apps are essential tools for planning daily activities. Building one from scratch is a fantastic way to learn frontend development, and Vue.js—with its reactivity, component-based architecture, and ease of use—is the perfect framework for the job. In this guide, we’ll create a fully functional weather app that fetches real-time weather data, displays current conditions (temperature, humidity, wind speed), supports city search, and even persists user preferences. By the end, you’ll have hands-on experience with Vue.js fundamentals, API integration, state management, and deployment.

Table of Contents

  1. Prerequisites
  2. Setting Up the Vue.js Project
  3. Planning the App Structure
  4. Building Core Components
  5. Integrating a Weather API
  6. Styling the App
  7. Adding Advanced Features
  8. Testing the App
  9. Deploying the App
  10. Troubleshooting Common Issues
  11. Conclusion
  12. References

Prerequisites

Before diving in, ensure you have the following:

  • Node.js & npm/yarn: Install from nodejs.org (LTS version recommended).
  • Basic Vue.js Knowledge: Familiarity with Vue components, reactivity, and directives (e.g., v-model, v-if).
  • Code Editor: VS Code (with the Volar extension for Vue 3 support) or your preferred editor.
  • API Key: Sign up for a free API key at OpenWeatherMap (we’ll use their Current Weather Data API).

Setting Up the Vue.js Project

We’ll use Vue CLI to scaffold the project quickly.

Step 1: Install Vue CLI

If you don’t have Vue CLI installed, run:

npm install -g @vue/cli
# or
yarn global add @vue/cli

Step 2: Create a New Project

Run vue create weather-app and follow the prompts:

  • Select Vue 3 (recommended for this tutorial).
  • Choose Default ([Vue 3] babel, eslint) for simplicity.

Step 3: Navigate to the Project and Run the Dev Server

cd weather-app
npm run serve

Your app will run at http://localhost:8080. Open this in your browser to see the default Vue welcome page.

Planning the App Structure

A well-organized app is easier to maintain. Here’s our plan:

Key Components

  • SearchBar: Input field to search for cities.
  • WeatherDisplay: Shows weather data (temperature, description, icon, etc.).
  • ErrorAlert: Displays errors (e.g., invalid city).
  • App.vue: Parent component that manages state and API calls.

State Management

For simplicity, we’ll use Vue’s built-in reactivity (no Vuex/Pinia needed) since the app is small. We’ll store:

  • city: Current search query.
  • weatherData: Fetched weather data (temperature, humidity, etc.).
  • error: Error message (if any).

Building Core Components

Let’s start building the components.

1. The SearchBar Component

This component lets users input a city and trigger a search.

Create src/components/SearchBar.vue:

<template>
  <div class="search-bar">
    <input
      type="text"
      v-model="city"
      placeholder="Enter city name..."
      @keyup.enter="handleSearch"
    />
    <button @click="handleSearch">Search</button>
  </div>
</template>

<script>
export default {
  name: "SearchBar",
  data() {
    return {
      city: "", // Binds to input field
    };
  },
  methods: {
    handleSearch() {
      if (this.city.trim()) {
        // Emit event to parent (App.vue) with the city
        this.$emit("search", this.city.trim());
        this.city = ""; // Clear input after search
      }
    },
  },
};
</script>

<style scoped>
.search-bar {
  display: flex;
  gap: 8px;
  margin: 20px 0;
}

input {
  padding: 8px 12px;
  flex: 1;
  border: 2px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

button {
  padding: 8px 16px;
  background: #42b983; /* Vue's brand color */
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.3s;
}

button:hover {
  background: #359469;
}
</style>

2. The WeatherDisplay Component

This component receives weatherData as a prop and displays it.

Create src/components/WeatherDisplay.vue:

<template>
  <div class="weather-display" v-if="weatherData">
    <h2>{{ weatherData.name }}, {{ weatherData.sys.country }}</h2>
    <div class="temp">
      {{ Math.round(weatherData.main.temp) }}°C
    </div>
    <div class="description">
      <img
        :src="`https://openweathermap.org/img/wn/${weatherData.weather[0].icon}@2x.png`"
        alt="Weather icon"
      />
      <p>{{ weatherData.weather[0].description }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: "WeatherDisplay",
  props: {
    weatherData: {
      type: Object,
      required: true,
    },
  },
};
</script>

<style scoped>
.weather-display {
  text-align: center;
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.temp {
  font-size: 48px;
  font-weight: bold;
  margin: 10px 0;
}

.description {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-size: 18px;
  text-transform: capitalize;
}
</style>

3. The ErrorAlert Component

Shows errors (e.g., “City not found”).

Create src/components/ErrorAlert.vue:

<template>
  <div class="error-alert" v-if="message">
    ⚠️ {{ message }}
  </div>
</template>

<script>
export default {
  name: "ErrorAlert",
  props: {
    message: {
      type: String,
      required: true,
    },
  },
};
</script>

<style scoped>
.error-alert {
  color: #dc3545;
  background: #f8d7da;
  padding: 12px;
  border-radius: 4px;
  margin: 10px 0;
  text-align: center;
}
</style>

4. Updating App.vue

Now, import and use the components in src/App.vue:

<template>
  <div class="app">
    <h1>Vue Weather App</h1>
    <SearchBar @search="fetchWeather" />
    <ErrorAlert :message="error" v-if="error" />
    <WeatherDisplay :weatherData="weatherData" v-if="weatherData" />
  </div>
</template>

<script>
import SearchBar from "./components/SearchBar.vue";
import WeatherDisplay from "./components/WeatherDisplay.vue";
import ErrorAlert from "./components/ErrorAlert.vue";

export default {
  name: "App",
  components: {
    SearchBar,
    WeatherDisplay,
    ErrorAlert,
  },
  data() {
    return {
      weatherData: null, // Holds fetched weather data
      error: "", // Holds error message
    };
  },
  methods: {
    async fetchWeather(city) {
      // We'll add API logic here next!
    },
  },
};
</script>

<style>
.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  text-align: center;
  color: #333;
}
</style>

Integrating a Weather API

We’ll use OpenWeatherMap’s API to fetch weather data.

Step 1: Get Your API Key

  1. Go to OpenWeatherMap API and sign up.
  2. After logging in, go to API Keys and generate a new key (it may take 10–15 minutes to activate).

Step 2: Fetch Weather Data in App.vue

Update the fetchWeather method in App.vue to call the API:

async fetchWeather(city) {
  const apiKey = "YOUR_API_KEY"; // Replace with your key!
  const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`;

  try {
    this.error = ""; // Clear previous errors
    const response = await fetch(url);
    if (!response.ok) throw new Error("City not found");
    const data = await response.json();
    this.weatherData = data; // Update weatherData
    // Save to localStorage for persistence
    localStorage.setItem("lastCity", city);
  } catch (err) {
    this.error = err.message;
    this.weatherData = null; // Clear stale data
  }
}

Step 3: Load Last Search on App Start

Add mounted() to App.vue to load the last searched city from localStorage:

mounted() {
  const lastCity = localStorage.getItem("lastCity");
  if (lastCity) {
    this.fetchWeather(lastCity); // Auto-load last search
  }
}

Styling the App

Let’s polish the UI with better styling. Update App.vue’s style:

.app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  font-family: "Segoe UI", Roboto, sans-serif;
  background: #f5f5f5;
  min-height: 100vh;
}

h1 {
  color: #2c3e50;
  margin-bottom: 30px;
  font-size: 2.5rem;
}

/* Center content vertically */
@media (min-width: 768px) {
  .app {
    padding: 40px;
  }
}

Update WeatherDisplay.vue’s style for a card-like look:

.weather-display {
  text-align: center;
  padding: 30px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  margin-top: 20px;
}

.temp {
  font-size: 4rem;
  font-weight: 700;
  color: #2c3e50;
  margin: 20px 0;
}

.description p {
  font-size: 1.2rem;
  color: #7f8c8d;
  text-transform: capitalize;
}

Adding Advanced Features

1. Show Additional Data (Humidity, Wind)

Update WeatherDisplay.vue to include humidity and wind speed:

<div class="details">
  <div class="detail-item">
    <span class="label">Humidity:</span>
    <span>{{ weatherData.main.humidity }}%</span>
  </div>
  <div class="detail-item">
    <span class="label">Wind:</span>
    <span>{{ weatherData.wind.speed }} m/s</span>
  </div>
</div>

Add styles for .details in WeatherDisplay.vue:

.details {
  display: flex;
  justify-content: space-around;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #eee;
}

.detail-item {
  color: #555;
}

.label {
  font-weight: bold;
  margin-right: 4px;
}

2. Unit Conversion (Celsius ↔ Fahrenheit)

Add a toggle to switch between Celsius and Fahrenheit.

In App.vue, add a isCelsius data property:

data() {
  return {
    // ... existing data
    isCelsius: true, // Track unit preference
  };
}

Add a button in App.vue’s template to toggle units:

<button class="unit-toggle" @click="isCelsius = !isCelsius">
  {{ isCelsius ? "°C" : "°F" }}
</button>

Update WeatherDisplay.vue to use the unit:

<div class="temp">
  {{ Math.round(isCelsius ? weatherData.main.temp : (weatherData.main.temp * 9/5) + 32) }}
  {{ isCelsius ? "°C" : "°F" }}
</div>

Pass isCelsius as a prop to WeatherDisplay:

<WeatherDisplay :weatherData="weatherData" :isCelsius="isCelsius" v-if="weatherData" />

Update WeatherDisplay’s props to accept isCelsius:

props: {
  // ... existing props
  isCelsius: {
    type: Boolean,
    required: true,
  },
},

Testing the App

Test the app by:

  • Searching for valid cities (e.g., “London”, “Tokyo”).
  • Searching for invalid cities (e.g., “Atlantis”) to trigger errors.
  • Toggling units and verifying temperatures update.
  • Refreshing the page to ensure the last city loads from localStorage.

Deploying the App

Deploy your app for free using Netlify:

  1. Push your code to a GitHub repo.
  2. Go to Netlify → “Add new site” → “Import an existing project” → Connect your GitHub repo.
  3. Set build settings:
    • Build command: npm run build
    • Publish directory: dist
  4. Click “Deploy site”. Netlify will generate a URL (e.g., your-app-name.netlify.app).

Troubleshooting Common Issues

  • API Key Not Working: Ensure your key is activated (wait 15 minutes) and correctly pasted.
  • CORS Errors: OpenWeatherMap’s API supports CORS, so this shouldn’t happen. If it does, use a proxy (e.g., https://cors-anywhere.herokuapp.com/ before the URL).
  • Stale Data: Clear weatherData on errors to avoid showing old data.

Conclusion

You’ve built a fully functional weather app with Vue.js! You learned:

  • How to structure Vue components and manage state.
  • Integrate third-party APIs with error handling.
  • Add persistence with localStorage.
  • Style and deploy a Vue app.

Next steps: Add a 5-day forecast, location-based weather (using Geolocation API), or dark mode!

References