Creating an Interactive NYC Live MTA Bus Tracker: Reverse Engineering the MTA Bus Time API's Route Naming Convention, Fetching and Processing Live Bus Data with AWS Lambda, and Mapping with Mapbox GL JS
AWS Lambda function console where the Python code is deployed and triggered every time the map requests live bus data
Desktop view on initial load and after hovering over a QM2 bus
Desktop view showing the B1 route path and highlighted buses after selecting B1 from the Brooklyn dropdown
Mobile page on initial load, after selecting the Q1 route from the Queens dropdown, and after tapping on an X27 bus and minimizing the info panel
(For the GitHub repository, please click here, and for the interactive map, please click here.)
Summary
As an NYC resident and avid MTA bus rider, while I appreciate the MTA app, I wish it would actually visualize your bus options. Instead of showing them on their map, you have to pull up an info panel that just lists nearby routes. That’s why I decided to create this interactive map: to be able to see every single MTA bus currently in service across the city.
This map makes an API call to the MTA Bus Time API every 45 seconds. When a user hovers (desktop) or taps (mobile) on a bus, a popup appears showing the bus number, destination, and next few stops — along with the full route path on the map. Users can also select a route from the borough and express dropdowns, which highlights all buses on that route and displays the full route path.
The frontend is hosted on GitHub Pages, while the backend lives in an AWS Lambda function. Every 45 seconds, the browser makes a direct API call to the Lambda URL, which fetches the latest bus data from the MTA Bus Time API and returns it to the map.
Considerable time was spent reverse engineering the MTA Bus Time API's route naming convention. This is because the API returns a PublishedLineName field that's sometimes a string and sometimes a list, so I first had to normalize that inconsistency before parsing the route name.
NYC bus routes follow a prefix-based system that encodes both borough and service type. I learned that express routes get their own category regardless of borough, and the prefixes are “BXM” (Bronx express), “QM” (Queens express), “BM” (Brooklyn express), “SIM” (Staten Island express), and “X” (a general express prefix that’s apparently a legacy naming convention). For local routes, a single or double letter prefix indicates the borough: “M” for Manhattan, “B” for Brooklyn, “Q” for Queens, “S” for Staten Island, and “BX” for the Bronx.
In order to add the route shapes to the map, I downloaded GTFS (General Transit Feed Specification) data from the MTA, which includes a shapes.txt file in one of six folders - one for each borough and then the sixth for express buses in the MTA Bus Company folder. Those files contain sequences of latitutde/longitude coordinates defining the physical path of every bus route.
Then, I converted the shapes.txt data into GeoJSON format, which is what Mapbox GL JS understands for rendering lines on the map. The GeoJSON was hosted on GitHub and loaded into Mapbox as a source layer, so when a user hovers a bus or selects a route from the dropdown, Mapbox can instantly draw the route path on the map by filtering to that route's GeoJSON feature.
Tools Used
Python
MTA Bus Time API
AWS Lambda
Mapbox GL JS
GitHub Pages
Javascript
GTFS Shape Data converted to GeoJSON