Next.js WeatherAPI Integration: SSR, API Routes & ISR

Next.js WeatherAPI Integration: SSR, API Routes & ISR

Next.js provides multiple rendering strategies that make it perfect for building weather applications with optimal performance. This tutorial covers implementing WeatherAPI with server-side rendering (SSR), API route proxying, and Incremental Static Regeneration (ISR) for efficient caching.

Setting Up WeatherAPI in Next.js

First, install the necessary dependencies and configure your environment:

npm install axios
# Create .env.local
WEATHER_API_KEY=your_api_key_here
NEXT_PUBLIC_BASE_URL=https://api.weatherapi.com/v1

Create a utility file for API calls:

// lib/weather.js
import axios from 'axios';

const BASE_URL = 'https://api.weatherapi.com/v1';
const API_KEY = process.env.WEATHER_API_KEY;

export const weatherAPI = axios.create({
  baseURL: BASE_URL,
  params: {
    key: API_KEY,
  },
});

export async function getCurrentWeather(location) {
  const response = await weatherAPI.get('/current.json', {
    params: { q: location, aqi: 'yes' }
  });
  return response.data;
}

export async function getForecast(location, days = 3) {
  const response = await weatherAPI.get('/forecast.json', {
    params: { q: location, days, aqi: 'yes', alerts: 'yes' }
  });
  return response.data;
}

Server-Side Rendering with getServerSideProps

For dynamic weather data that needs to be fresh on every request, use SSR:

// pages/weather/[location].js
import { getCurrentWeather, getForecast } from '../../lib/weather';

export default function WeatherPage({ current, forecast, location }) {
  return (
    <div>
      <h1>Weather in {location}</h1>
      <div>
        <h2>Current: {current.temp_c}°C</h2>
        <p>Feels like: {current.feelslike_c}°C</p>
        <p>Condition: {current.condition.text}</p>
        <p>UV Index: {current.uv}</p>
        <p>AQI: {current.air_quality['us-epa-index']}</p>
      </div>
      
      <div>
        <h2>3-Day Forecast</h2>
        {forecast.forecastday.map((day) => (
          <div key={day.date}>
            <h3>{day.date}</h3>
            <p>High: {day.day.maxtemp_c}°C</p>
            <p>Low: {day.day.mintemp_c}°C</p>
            <p>{day.day.condition.text}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export async function getServerSideProps({ params }) {
  const location = params.location;
  
  try {
    const [currentData, forecastData] = await Promise.all([
      getCurrentWeather(location),
      getForecast(location, 3)
    ]);

    return {
      props: {
        current: currentData.current,
        forecast: forecastData.forecast,
        location: currentData.location.name,
      },
    };
  } catch (error) {
    return {
      notFound: true,
    };
  }
}

API Route Proxying

Create API routes to proxy WeatherAPI requests, adding rate limiting and error handling:

// pages/api/weather/current.js
import { getCurrentWeather } from '../../../lib/weather';

export default async function handler(req, res) {
  if (req.method !== 'GET') {
    return res.status(405).json({ message: 'Method not allowed' });
  }

  const { q: location } = req.query;

  if (!location) {
    return res.status(400).json({ error: 'Location parameter required' });
  }

  try {
    const data = await getCurrentWeather(location);
    
    // Cache for 10 minutes
    res.setHeader('Cache-Control', 'public, s-maxage=600, stale-while-revalidate=300');
    res.status(200).json(data);
  } catch (error) {
    console.error('Weather API Error:', error.response?.data || error.message);
    res.status(error.response?.status || 500).json({
      error: 'Failed to fetch weather data'
    });
  }
}

Create a similar route for search functionality:

// pages/api/weather/search.js
import { weatherAPI } from '../../../lib/weather';

export default async function handler(req, res) {
  const { q: query } = req.query;
  
  if (!query || query.length < 3) {
    return res.status(400).json({ error: 'Query must be at least 3 characters' });
  }

  try {
    const response = await weatherAPI.get('/search.json', {
      params: { q: query }
    });
    
    res.setHeader('Cache-Control', 'public, s-maxage=3600');
    res.status(200).json(response.data);
  } catch (error) {
    res.status(500).json({ error: 'Search failed' });
  }
}

Incremental Static Regeneration (ISR)

For weather data that can be cached but needs periodic updates, use ISR:

// pages/forecast/[location].js
import { getForecast } from '../../lib/weather';

export default function ForecastPage({ forecast, location, lastUpdated }) {
  return (
    <div>
      <h1>14-Day Forecast for {location}</h1>
      <p>Last updated: {new Date(lastUpdated).toLocaleString()}</p>
      
      <div>
        {forecast.forecastday.map((day) => (
          <div key={day.date}>
            <h2>{day.date}</h2>
            <div>
              <strong>{day.day.maxtemp_c}°C / {day.day.mintemp_c}°C</strong>
              <p>{day.day.condition.text}</p>
              <p>Chance of rain: {day.day.daily_chance_of_rain}%</p>
            </div>
            
            <div>
              <h3>Hourly</h3>
              {day.hour.filter((_, i) => i % 3 === 0).map((hour) => (
                <span key={hour.time}>
                  {new Date(hour.time).getHours()}:00 - {hour.temp_c}°C
                </span>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

export async function getStaticProps({ params }) {
  try {
    const data = await getForecast(params.location, 14);
    
    return {
      props: {
        forecast: data.forecast,
        location: data.location.name,
        lastUpdated: new Date().toISOString(),
      },
      revalidate: 1800, // Revalidate every 30 minutes
    };
  } catch (error) {
    return {
      notFound: true,
      revalidate: 60, // Retry after 1 minute on error
    };
  }
}

export async function getStaticPaths() {
  // Pre-generate popular cities
  const popularCities = ['London', 'New York', 'Tokyo', 'Sydney'];
  
  return {
    paths: popularCities.map((city) => ({
      params: { location: city },
    })),
    fallback: 'blocking',
  };
}

Client-Side Data Fetching

For interactive components, use SWR or React Query with your API routes:

// components/WeatherSearch.js
import { useState } from 'react';
import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function WeatherSearch() {
  const [query, setQuery] = useState('');
  const [selectedLocation, setSelectedLocation] = useState(null);
  
  const { data: suggestions } = useSWR(
    query.length > 2 ? `/api/weather/search?q=${query}` : null,
    fetcher,
    { dedupingInterval: 300000 }
  );
  
  const { data: weather, error } = useSWR(
    selectedLocation ? `/api/weather/current?q=${selectedLocation}` : null,
    fetcher,
    { refreshInterval: 600000 }
  );

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search for a city..."
      />
      
      {suggestions && (
        <ul>
          {suggestions.map((location) => (
            <li
              key={location.id}
              onClick={() => setSelectedLocation(location.name)}
            >
              {location.name}, {location.country}
            </li>
          ))}
        </ul>
      )}
      
      {weather && (
        <div>
          <h2>{weather.location.name}</h2>
          <p>{weather.current.temp_c}°C</p>
        </div>
      )}
    </div>
  );
}

Performance Optimization

WeatherAPI’s fast response times (~200ms average) work perfectly with Next.js caching strategies. Use ISR for forecast pages that don’t need real-time updates, SSR for critical current conditions, and client-side fetching for interactive search features.

Ready to build weather applications with Next.js? Sign up for WeatherAPI and get 100,000 free API calls per month with no credit card required.

Scroll to Top