Build a Real-Time UK Fuel Price Dashboard with Next.js and FuelInsight API

Building a Real-Time Fuel Price Dashboard with Next.js and FuelInsight API

The UK Fuel Price Intelligence platform (FuelInsight) exposes publicly mandated fuel pricing data from the Competition and Markets Authority (CMA) under the Data (Use and Access) Act 2025. Station operators are legally required to report price changes within 30 minutes, making this data incredibly fresh and reliable for building real-time applications.

In this guide, you'll learn how to create a live fuel price dashboard using Next.js that consumes FuelInsight's public analytics data, implements live price polling, and displays station-level insights across the UK.

Why Use FuelInsight Data in Your Next.js Application

Unlike proprietary fuel pricing APIs, FuelInsight aggregates publicly mandated data that station operators must submit to the CMA. This means:

  • 30-minute reporting requirement: Price changes appear in the system within 30 minutes of station updates
  • 100% compliance: Data sourced directly from legal UK Fuel Finder scheme reporting
  • No data manipulation: Platform stores and analyzes raw public sector data without editorial adjustment
  • Open Government License: Built on data licensed under Open Government Licence v3.0

This makes it ideal for developer projects, price comparison tools, or fleet management dashboards.

Setting Up Your Next.js Project

Start by creating a new Next.js 15+ application with TypeScript support:

npx create-next-app@latest fuel-dashboard --typescript --tailwind
cd fuel-dashboard
npm install axios zustand recharts date-fns

Install dependencies for API requests, state management, charting, and date utilities:

  • axios: HTTP client for FuelInsight API calls
  • zustand: Lightweight state management for price cache
  • recharts: React charting library for price trends
  • date-fns: Utility for formatting timestamps

Creating the FuelInsight API Service Layer

Create lib/fuelinsight.ts to handle all API interactions:

import axios from 'axios';

interface FuelPrice {
  fuelType: 'E10' | 'E5' | 'B7' | 'Premium';
  price: number;
  currency: 'GBP';
  timestamp: string;
}

interface Station {
  id: string;
  name: string;
  brand: string;
  postcode: string;
  prices: FuelPrice[];
  lastUpdated: string;
}

interface DashboardData {
  nationalAverage: Record<string, number>;
  biggestMovers: Array<{
    brand: string;
    change: number;
    fuelType: string;
  }>;
  cheapestStations: Station[];
  volatilityIndex: number;
}

const API_BASE = 'https://www.fuelinsight.co.uk/api';

export async function getDashboardMetrics(): Promise<DashboardData> {
  try {
    const response = await axios.get(`${API_BASE}/dashboard`, {
      params: {
        period: '24h',
        includeStale: false
      },
      timeout: 5000
    });
    
    return response.data;
  } catch (error) {
    console.error('FuelInsight API error:', error);
    throw new Error('Failed to fetch fuel price data');
  }
}

export async function getCheapestPetrol(postcode?: string): Promise<Station[]> {
  const params: Record<string, any> = {
    fuelType: 'E10',
    sortBy: 'price',
    reportedLastDays: 7
  };
  
  if (postcode) params.postcode = postcode;
  
  const response = await axios.get(`${API_BASE}/stations`, { params });
  return response.data.stations;
}

export async function getBrandComparison(days: number = 30): Promise<{
  brands: string[];
  data: Array<{
    date: string;
    [brand: string]: number;
  }>;
}> {
  const response = await axios.get(`${API_BASE}/brand-comparison`, {
    params: { days }
  });
  return response.data;
}

Managing Real-Time Price Updates with Zustand

Create store/priceStore.ts to cache and manage price data:

import { create } from 'zustand';
import { getDashboardMetrics } from '@/lib/fuelinsight';

interface PriceStore {
  metrics: any | null;
  loading: boolean;
  error: string | null;
  lastUpdated: Date | null;
  fetchMetrics: () => Promise<void>;
  setPollingInterval: (ms: number) => void;
}

export const usePriceStore = create<PriceStore>((set) => {
  let pollingInterval: NodeJS.Timer | null = null;
  
  const fetchMetrics = async () => {
    set({ loading: true });
    try {
      const data = await getDashboardMetrics();
      set({
        metrics: data,
        lastUpdated: new Date(),
        error: null,
        loading: false
      });
    } catch (error) {
      set({
        error: (error as Error).message,
        loading: false
      });
    }
  };
  
  const setPollingInterval = (ms: number) => {
    if (pollingInterval) clearInterval(pollingInterval);
    pollingInterval = setInterval(fetchMetrics, ms);
  };
  
  return {
    metrics: null,
    loading: true,
    error: null,
    lastUpdated: null,
    fetchMetrics,
    setPollingInterval
  };
});

Building the Dashboard Component

Create components/FuelDashboard.tsx with live price updates:

'use client';

import { useEffect, useState } from 'react';
import { usePriceStore } from '@/store/priceStore';
import { formatDistanceToNow } from 'date-fns';

const fuelTypes = [
  { code: 'E10', label: 'Unleaded (E10)', icon: '⛽' },
  { code: 'E5', label: 'Super Unleaded (E5)', icon: '🚗' },
  { code: 'B7', label: 'Diesel (B7)', icon: '🛢️' },
  { code: 'Premium', label: 'Premium Diesel', icon: '💎' }
];

export default function FuelDashboard() {
  const { metrics, loading, error, lastUpdated, fetchMetrics, setPollingInterval } = usePriceStore();
  const [selectedFuel, setSelectedFuel] = useState('E10');
  
  useEffect(() => {
    // Initial fetch
    fetchMetrics();
    // Poll every 5 minutes (300000ms) to respect API rate limits
    setPollingInterval(300000);
  }, [fetchMetrics, setPollingInterval]);
  
  if (loading && !metrics) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="animate-pulse text-gray-400">Loading fuel prices...</div>
      </div>
    );
  }
  
  return (
    <div className="space-y-6 p-6 bg-gray-950">
      {/* Header with last update timestamp */}
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold text-gray-100">UK Fuel Price Dashboard</h1>
        {lastUpdated && (
          <p className="text-sm text-gray-400">
            Updated {formatDistanceToNow(lastUpdated, { addSuffix: true })}
          </p>
        )}
      </div>
      
      {error && (
        <div className="p-4 bg-red-900/20 border border-red-700 rounded text-red-300">
          Error: {error}
        </div>
      )}
      
      {/* Fuel type selector */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
        {fuelTypes.map(fuel => (
          <button
            key={fuel.code}
            onClick={() => setSelectedFuel(fuel.code)}
            className={`p-3 rounded border transition-colors ${
              selectedFuel === fuel.code
                ? 'bg-blue-600 border-blue-400 text-white'
                : 'bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600'
            }`}
          >
            <div className="text-2xl">{fuel.icon}</div>
            <div className="text-xs font-semibold">{fuel.label}</div>
          </button>
        ))}
      </div>
      
      {/* National average price card */}
      {metrics?.nationalAverage && (
        <div className="bg-gradient-to-r from-green-900/30 to-blue-900/30 border border-green-700/50 rounded-lg p-6">
          <h2 className="text-lg font-semibold text-gray-200 mb-2">National Average</h2>
          <div className="text-4xl font-bold text-green-400">
            £{metrics.nationalAverage[selectedFuel]?.toFixed(2)}/L
          </div>
        </div>
      )}
      
      {/* Biggest movers (price changes in last 24h) */}
      {metrics?.biggestMovers && metrics.biggestMovers.length > 0 && (
        <div className="bg-gray-900 border border-gray-800 rounded-lg p-6">
          <h3 className="text-lg font-semibold text-gray-200 mb-4">24h Price Movements</h3>
          <div className="space-y-3">
            {metrics.biggestMovers
              .filter((m: any) => m.fuelType === selectedFuel)
              .slice(0, 5)
              .map((move: any, idx: number) => (
                <div key={idx} className="flex justify-between items-center p-3 bg-gray-800 rounded">
                  <span className="text-gray-300">{move.brand}</span>
                  <span className={move.change > 0 ? 'text-red-400' : 'text-green-400'}>
                    {move.change > 0 ? '↑' : '↓'} {Math.abs(move.change).toFixed(1)}p
                  </span>
                </div>
              ))}
          </div>
        </div>
      )}
    </div>
  );
}

Polling Strategy and Rate Limiting

Important considerations when building polling intervals:

| Interval | Use Case | Pros | Cons | |----------|----------|------|------| | 30 seconds | Real-time trading apps | Catches all changes | High API load | | 5 minutes | Consumer fuel finder | Balanced freshness | May miss volatility | | 15 minutes | Analytics dashboard | Low bandwidth | Delayed insights | | 60 minutes | Historical tracking | Minimal overhead | Poor real-time UX |

We recommend 5-minute intervals for most consumer applications, aligning with the UK's 30-minute mandatory reporting window.

Handling Stale Data

FuelInsight marks prices as "stale" (amber) if they haven't changed in 3+ days. Filter these from your UI:

const isStale = (lastReported: Date) => {
  const daysSince = (Date.now() - lastReported.getTime()) / (1000 * 60 * 60 * 24);
  return daysSince >= 3;
};

const freshStations = stations.filter(s => !isStale(new Date(s.lastUpdated)));

Key Implementation Details

API Response Handling: FuelInsight's dashboard returns aggregated metrics. For station-level data, query the /stations endpoint with filters like reportedLastDays: 7 to ensure freshness.

Timestamp Precision: Prices are reported with second-level precision. Use date-fns for consistent formatting across timezones.

Error Recovery: Implement exponential backoff when API calls fail—CMA data feeds occasionally have maintenance windows.

Data Attribution: Always credit "UK Fuel Finder scheme" and comply with Open Government Licence v3.0 requirements when displaying prices publicly.

Deployment Considerations

When deploying to production:

  • Use Vercel's serverless functions to proxy API calls (avoid exposing FuelInsight endpoint directly)
  • Cache results in Vercel's Edge Config or Redis for 4-5 minutes
  • Add error boundary fallbacks for API outages
  • Monitor rate limits with Sentry or similar observability tools

This architecture ensures your fuel price dashboard stays responsive while respecting the FuelInsight service's capacity.

Recommended Tools

  • VercelDeploy frontend apps instantly with zero config
  • DigitalOceanCloud hosting built for developers — $200 free credit for new users