TfL API TypeScript Client

Live demo of `tfl-ts` v2.3.1: a typed client for the TfL API, with line status and bus arrivals from the real service.

v0.0.1
๐Ÿฐ
๐Ÿƒ
๐ŸŽ“
Beginner

TfL API

Live TfL Status

Built with tfl-ts v2.3.1

Service Disruptions

Central

Minor DelaysMinor delays due to train cancellations.

Bus arrivals near you

Uses stopPoint.getByGeoPoint, stopPoint.search, and stopPoint.getArrivals.

Good Service(19 lines)

Northern

Jubilee

Piccadilly

District

Victoria

Circle

Hammersmith & City

Bakerloo

Metropolitan

Waterloo & City

DLR

Tram

Elizabeth line

Liberty

Lioness

Mildmay

Suffragette

Weaver

Windrush

Data from Transport for London via tfl-ts. Refreshes with each request.

Documentation

TfL API TypeScript Client

Live demo of tfl-ts v2.3.1: a typed TypeScript client for the Transport for London API. Friendly wrappers for everyday work, full raw endpoint coverage, and UI helpers for official line colours. The page above hits the real service for line status and bus arrivals.

What you get

Friendly wrappers for common work (client.line, client.stopPoint), plus client.raw.* when you need an endpoint the wrappers do not cover yet. Build-time metadata (line names, modes, severity labels) ships as constants; status, arrivals, and journeys hit the TfL API at runtime.

UI helpers ship with the package: official hex colours (getLineInlineStyles, getLineCssProps), severity sorting, and accessibility labels. Types are generated from the API. No runtime dependencies, so it runs in Node, the browser, and on the edge.

Installation

pnpm add tfl-tsLanguage: bash

Put free credentials from the TfL API Portal in .env.local:

TFL_APP_ID=your-app-idTFL_APP_KEY=your-app-keyLanguage: env

How this preview is built

The live demo above is a Next.js Server Component plus a client bus panel. Both talk to TfL only through tfl-ts. The important pieces:

1. Shared client (server-only)

Credentials stay on the server. A tiny helper constructs one client per call:

import TflClient from "tfl-ts";ย export const getTflClient = (): TflClient => {  const appId = process.env.TFL_APP_ID;  const appKey = process.env.TFL_APP_KEY;  if (appId && appKey) {    return new TflClient({ appId, appKey });  }  return new TflClient();};Language: ts

2. Line status (what powers the disruption / good-service cards)

Fetch modes, sort disruptions first, then paint with official line colours:

import {  sortLinesBySeverityAndOrder,  getSeverityClasses,  getLineCssProps,  getLineInlineStyles,  isNormalService,  hasNightService,} from "tfl-ts";import { getTflClient } from "./client";ย export async function LiveTflStatus() {  const client = getTflClient();  const lineStatuses = await client.line.getStatus({    modes: ["tube", "elizabeth-line", "dlr", "tram", "overground"],  });  const sorted = sortLinesBySeverityAndOrder(lineStatuses);  const disrupted = sorted.filter((line) => !isNormalService(line.lineStatuses ?? []));  const goodService = sorted.filter((line) => isNormalService(line.lineStatuses ?? []));ย   return (    <div>      {disrupted.map((line) => {        const styles = getLineInlineStyles(line.id ?? "");        const cssProps = getLineCssProps(line.id ?? "");ย         return (          <article key={line.id} style={cssProps}>            <h3 style={{ color: styles.color }}>{line.name}</h3>            <div              style={{                height: 6,                backgroundColor: "var(--line-color)",              }}            />            {line.lineStatuses?.map((status, i) => {              const severity = getSeverityClasses(status.statusSeverity ?? 10, true);              return (                <p key={i} className={severity.text}>                  {status.statusSeverityDescription}                  {status.reason}                </p>              );            })}          </article>        );      })}ย       {goodService.map((line) => (        <div key={line.id} style={getLineCssProps(line.id ?? "")}>          <span style={{ color: getLineInlineStyles(line.id ?? "").color }}>            {line.name}          </span>          {hasNightService(line.lineStatuses ?? []) && <span>Night service</span>}        </div>      ))}    </div>  );}Language: tsx

Key helpers from tfl-ts:

HelperRole in this preview
client.line.getStatusLive statuses for tube, Elizabeth line, DLR, tram, Overground
sortLinesBySeverityAndOrderDisruptions float to the top; lines keep TfL order within a severity band
isNormalService / hasNightServiceSplit disruption cards vs good-service grid; night badge
getLineInlineStyles / getLineCssPropsOfficial hex + CSS vars (--line-color, dark-mode outlines)
getSeverityClassesTailwind-friendly text / animation classes for severity copy

3. Bus arrivals (server actions + client UI)

The bus panel is a client component. Geolocation and search stay in the browser; every TfL call goes through a server action that uses stopPoint.*:

"use server";ย import { getTflClient } from "./client";ย /** Stops within 400m of the userโ€™s GPS (or a search hubโ€™s lat/lon). */export async function getNearbyBusStops(lat: number, lon: number) {  const client = getTflClient();  const response = await client.stopPoint.getByGeoPoint({    lat,    lon,    radius: 400,    modes: ["bus"],    returnLines: true,  });  return response.stopPoints ?? [];}ย /** Name / street search; prefer boardable `490โ€ฆ` stop IDs. */export async function searchBusStops(query: string) {  const client = getTflClient();  return client.stopPoint.search({    query,    modes: ["bus"],    maxResults: 6,  });}ย /** Live countdown board for one stop. */export async function getBusArrivals(stopId: string) {  const client = getTflClient();  return client.stopPoint.getArrivals({    stopPointIds: [stopId],    sortBy: "timeToStation",  });}Language: ts

On the client, โ€œUse my locationโ€ truncates GPS to ~100m (fewer cache-busting refreshes), calls getNearbyBusStops, then getBusArrivals for the nearest stop. Search does the same after stopPoint.search. If the hit is a hub (no arrivals), the demo expands it with getByGeoPoint around that hubโ€™s coordinates.

Basic usage (standalone)

Same building blocks without the Next.js wiring:

import TflClient, {  sortLinesBySeverityAndOrder,  getLineInlineStyles,  getLineCssProps,  getLineColor,} from "tfl-ts";ย const client = new TflClient();ย const statuses = await client.line.getStatus({ modes: ["tube"] });const sorted = sortLinesBySeverityAndOrder(statuses);ย const styles = getLineInlineStyles("central");// { color: '#E32017', backgroundColor: '#E32017', borderLeftColor: '#E32017' }ย <div style={{ ...getLineCssProps("central"), borderLeftWidth: 4, borderLeftStyle: "solid" }}>  <span style={{ color: getLineColor("elizabeth-line").hex }}>Elizabeth line</span>  <div style={{ backgroundColor: styles.backgroundColor, height: 6 }} /></div>Language: tsx

Links