DEV Community

syauqi fuadi
syauqi fuadi

Posted on

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! 🚀

Top comments (1)

Collapse
 
cameronjoseph profile image
HenryMattew

Very Useful Post thanks