BODS Bus Tracker

Real-time Buses of Somerset vehicles in Bridgwater

Live Dashboard

Operator: FBSO

Set your BODS API key above to start tracking buses.

How it works

BODS Real-Time Bus Tracking

An end-to-end look at how this dashboard fetches, parses, and displays live bus data from the UK government's Bus Open Data Service.

1

The BODS API

The Bus Open Data Service provides real-time location data for every supported bus operator in Great Britain. The endpoint accepts an api_key query parameter and returns SIRI-VM (Service Interface for Real Time Information - Vehicle Monitoring) XML. SIRI-VM is the European standard for real-time vehicle data, representing each bus as a VehicleActivity containing a VehicleJourney with line, direction, operator, location, and bearing.

2

Server-side proxy

The API key is entered in the browser but never exposed to the client. A createServerFn in TanStack Start proxies the request server-side, so the key stays in the server environment. The server fetches the raw XML, parses it, filters to Operator Code FBSO (Buses of Somerset), and returns clean typed data to the client.

// Server function: proxies the BODS API call
// so the API key never reaches the browser.
export const fetchBusData = createServerFn({ method: "GET" })
  .handler(async ({ data }) => {
    try {
      const url = new URL("https://data.bus-open-data.gov.uk/api/v1/datafeed/");
      url.searchParams.set("api_key", data.apiKey);

      const response = await fetch(url.toString(), {
        headers: { Accept: "application/xml" },
        signal: AbortSignal.timeout(15_000),
      });

      const body = await response.text();
      if (!response.ok) {
        return { ok: false, error: `BODS returned ${response.status}` };
      }

      return { ok: true, vehicles: parseVehiclesFromXml(body) };
    } catch (err) {
      return { ok: false, error: err.message };
    }
  });
3

Parse, filter, display

fast-xml-parser converts the SIRI-VM XML into a JavaScript object. We iterate the VehicleActivity entries, skip any whose OperatorRef does not match FBSO, and build a flat array of typed bus objects. The client renders them in a simple list showing each vehicle's line, direction, coordinates, and bearing.

// Parse the SIRI-VM XML into typed vehicle objects.
// removeNSPrefix strips namespace prefixes like "siri:" so
// elements are accessible by their local name.
function parseVehiclesFromXml(xml: string): BusVehicle[] {
  const parser = new XMLParser({
    ignoreAttributes: false,
    removeNSPrefix: true,          // handle "siri:VehicleActivity" etc.
    isArray: (name) =>
      name === "VehicleActivity" || name === "VehicleMonitoringDelivery",
  });

  const doc = parser.parse(xml);
  const deliveries =
    doc.Siri.ServiceDelivery.VehicleMonitoringDelivery;

  for (const delivery of deliveries) {
    for (const activity of delivery.VehicleActivity) {
      const journey = activity.VehicleJourney;

      // Filter to Buses of Somerset (FBSO) only
      if (journey.OperatorRef !== "FBSO") continue;

      vehicles.push({
        lineName: journey.PublishedLineName ?? journey.LineRef,
        direction: journey.DirectionRef,
        latitude: journey.VehicleLocation.Latitude,
        longitude: journey.VehicleLocation.Longitude,
        bearing: journey.Bearing,
        vehicleRef: journey.VehicleRef,
      });
    }
  }
  return vehicles;
}
4

Client fetch pattern

On the client, a simple useState / useEffect (or a React Query hook) calls the server function, passing the user's API key. The loading and error states let the UI show a spinner or an error message, and a "Refresh" button lets the user re-fetch at any time.

// Fetch buses on the client. The server function returns
// a structured { ok, vehicles|error } result every time.
const [vehicles, setVehicles] = useState<BusVehicle[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

async function loadBuses() {
  setLoading(true);
  setError(null);
  try {
    const result = await fetchBusData({ data: { apiKey: savedKey } });
    if (result.ok) {
      setVehicles(result.vehicles);
    } else {
      setError(result.error);
    }
  } catch (err) {
    setError(err.message);
  } finally {
    setLoading(false);
  }
}