DEV Community

syauqi fuadi
syauqi fuadi

Posted on

12 1 1 1 1

Build a Weather Dashboard: Your First API Project with React

Hey there! Let's build a weather dashboard using React, TypeScript, and the OpenWeather API. This tutorial will teach you how to handle API requests, manage loading states, and create a clean UI for weather data.

What We'll Build

screenshot

Prerequisites

  • Basic React knowledge
  • Node.js installed
  • OpenWeather API key (I'll show you how to get one)

Getting Started

1. Set Up the Project

npm create vite@latest weather-dashboard -- --template react-ts
cd weather-dashboard
npm install
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

npm install axios
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

2. Get Your API Key

  1. Go to OpenWeather
  2. Sign up for a free account
  3. Get your API key from your dashboard

3. Set Up Environment Variables

Create .env file in root:

VITE_API_KEY=your_api_key_here
VITE_API_URL=https://api.openweathermap.org/data/2.5
Enter fullscreen mode Exit fullscreen mode

4. Create Types

Create src/types/index.ts:

export interface WeatherData {
  main: {
    temp: number;
    feels_like: number;
    humidity: number;
    pressure: number;
  };
  weather: Array<{
    main: string;
    description: string;
    icon: string;
  }>;
  wind: {
    speed: number;
  };
  name: string;
}

export interface WeatherError {
  message: string;
}
Enter fullscreen mode Exit fullscreen mode

5. Create API Service

Create src/services/weatherAPI.ts:

import axios from 'axios';
import { WeatherData } from '../types';

const API_KEY = import.meta.env.VITE_API_KEY;
const API_URL = import.meta.env.VITE_API_URL;

export const getWeather = async (city: string): Promise<WeatherData> => {
  try {
    const response = await axios.get(
      `${API_URL}/weather?q=${city}&appid=${API_KEY}&units=metric`
    );
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(error.response?.data.message || 'Failed to fetch weather data');
    }
    throw error;
  }
};
Enter fullscreen mode Exit fullscreen mode

6. Create Components

Create src/components/SearchBar.tsx:

import { useState } from 'react';

interface SearchBarProps {
  onSearch: (city: string) => void;
  isLoading: boolean;
}

export function SearchBar({ onSearch, isLoading }: SearchBarProps) {
  const [city, setCity] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (city.trim()) {
      onSearch(city);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="text"
        value={city}
        onChange={(e) => setCity(e.target.value)}
        placeholder="Enter city name..."
        className="flex-1 p-2 rounded-lg border focus:outline-none focus:border-blue-500"
      />
      <button
        type="submit"
        disabled={isLoading}
        className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 
                 disabled:bg-gray-400 disabled:cursor-not-allowed"
      >
        {isLoading ? 'Searching...' : 'Search'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create src/components/WeatherCard.tsx:

import { WeatherData } from '../types';

interface WeatherCardProps {
  data: WeatherData;
}

export function WeatherCard({ data }: WeatherCardProps) {
  return (
    <div className="bg-white rounded-lg shadow-lg p-6">
      <div className="flex justify-between items-center mb-4">
        <h2 className="text-2xl font-bold">{data.name}</h2>
        <img
          src={`http://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`}
          alt={data.weather[0].description}
          className="w-16 h-16"
        />
      </div>

      <div className="grid grid-cols-2 gap-4">
        <div>
          <p className="text-4xl font-bold">{Math.round(data.main.temp)}°C</p>
          <p className="text-gray-500">{data.weather[0].main}</p>
        </div>

        <div className="space-y-2">
          <p className="text-gray-600">
            Feels like: {Math.round(data.main.feels_like)}°C
          </p>
          <p className="text-gray-600">
            Humidity: {data.main.humidity}%
          </p>
          <p className="text-gray-600">
            Wind: {data.wind.speed} m/s
          </p>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

7. Update App Component

Update src/App.tsx:

import { useState } from 'react';
import { SearchBar } from './components/SearchBar';
import { WeatherCard } from './components/WeatherCard';
import { getWeather } from './services/weatherAPI';
import { WeatherData, WeatherError } from './types';

function App() {
  const [weather, setWeather] = useState<WeatherData | null>(null);
  const [error, setError] = useState<string>('');
  const [isLoading, setIsLoading] = useState(false);

  const handleSearch = async (city: string) => {
    try {
      setIsLoading(true);
      setError('');
      const data = await getWeather(city);
      setWeather(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
      setWeather(null);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-gray-100 py-8 px-4">
      <div className="max-w-md mx-auto space-y-4">
        <h1 className="text-3xl font-bold text-center mb-8">
          Weather Dashboard
        </h1>

        <SearchBar onSearch={handleSearch} isLoading={isLoading} />

        {error && (
          <div className="p-4 bg-red-100 text-red-700 rounded-lg">
            {error}
          </div>
        )}

        {weather && <WeatherCard data={weather} />}
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

8. Update Tailwind Config

Update tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Add Tailwind to your src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

9. Additional Features (Optional)

  1. Add loading skeleton: create this file src/component/LoadingSkeleton.tsx
export function LoadingSkeleton() {
  return (
    <div className="animate-pulse bg-white rounded-lg shadow-lg p-6">
      <div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
      <div className="space-y-3">
        <div className="h-6 bg-gray-200 rounded"></div>
        <div className="h-6 bg-gray-200 rounded w-3/4"></div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

update src/App.tsx

{isLoading && <LoadingSkeleton />}
{!isLoading && weather && <WeatherCard data={weather} />}
{!isLoading && error && (
 <div className="p-4 bg-red-100 text-red-700 rounded-lg">
   {error}
 </div>
)}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Tips

  1. Loading States
// Show different states based on loading
{isLoading && <LoadingSkeleton />}
{!isLoading && weather && <WeatherCard data={weather} />}
{!isLoading && error && <ErrorMessage message={error} />}
Enter fullscreen mode Exit fullscreen mode
  1. Unit Testing Example (using Vitest)
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { WeatherCard } from './WeatherCard';

describe('WeatherCard', () => {
  it('displays weather information correctly', () => {
    const mockData = {
      name: 'London',
      main: {
        temp: 20,
        feels_like: 22,
        humidity: 80,
        pressure: 1013
      },
      weather: [{
        main: 'Clear',
        description: 'clear sky',
        icon: '01d'
      }],
      wind: {
        speed: 5
      }
    };

    render(<WeatherCard data={mockData} />);
    expect(screen.getByText('London')).toBeInTheDocument();
    expect(screen.getByText('20°C')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Next Steps

You can enhance this project by:

  1. Adding 5-day forecast
  2. Saving favorite cities
  3. Adding geolocation support
  4. Adding weather maps
  5. Adding more weather details
  6. Adding unit conversion (Celsius/Fahrenheit)

Common Issues and Solutions

  1. CORS Issues
// Use proxy in vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.openweathermap.org',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode
  1. API Key Security
// Always use environment variables
// Never commit .env files
// Consider using a backend proxy
Enter fullscreen mode Exit fullscreen mode

That's it! You now have a functional weather dashboard that fetches real data from an API. This project teaches you about:

  • API integration
  • Error handling
  • Loading states
  • TypeScript with React
  • Environment variables
  • Component organization

Check the repository to get the full source

Happy coding! 🚀

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more