This is a GraphQL API that serves multiple GTFS static feeds from a PostgreSQL database with the PostGIS extension enabled, allowing fetching of geometry data created from the static feeds.
- Running the API
- Testing the API
- Configuring authentication
- Configuring the cache store (Redis)
- Configuring the database (PostgreSQL/PostGIS)
- Querying the GraphQL API
- Entity Relationship Diagram
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:covThis API requires an x-api-key header to be sent with a valid key. These keys are defined in an API_KEYS variable in .env, separated by commas:
API_KEYS=1XXXXXXXXXXXXXX,2XXXXXXXXXXXXXX,3XXXXXXXXXXXXXXI am using the Insomnia client, however, if you want to use the GraphQL Playground interface in your browser, you can send this header with ModHeader extension. If you use ModHeader, you can add an x-api-key request header, then add a Filter with a URL Pattern of http:\/\/localhost:4000\/graphql to authenticate.
This API uses Redis for caching and session management, which can be configured in .env:
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_AUTH=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXExample .env configuration:
DB_HOST=<hostname>
DB_PORT=5432
DB_USERNAME=<username>
DB_PASSWORD=<password>
DB_DATABASE=gtfsThis project depends on a PostgrSQL database populated using the gtfs-sql-importer. This requires a PostGIS-enabled PostgreSQL database:
# Switch to user "postgres"
sudo su postgres
# Run psql
psql
# From the psql prompt, select database
psql> \c gtfs_db
# Create PostGIS extension
CREATE EXTENSION postgisBasic usage of gtfs-sql-importer is as follows (executed from within the repo):
Export the following environment variables:
PGDATABASE=mydbname
PGHOST=example.com
PGUSER=username
PGPASSWORD=passwordThen, from the root directory of gtfs-sql-importer/:
make init
make load GTFS=/path/to/gtfs.zipWhere gtfs.zip is the name of the downloaded .zip file containing the GTFS data.
View the entity relationship diagram (ERD) generated for the created database here.
It should be fairly straight-forward to query GTFS data if you follow the specification laid out in detail here. You need to convert fields to camel-case (e.g., route_id => routeId), and use singular and plural depending on whether you want a single entity or multiple entities. Below are examples, along with cases where you can specify an addition parameter to query by, such as trips, which can take a routeId to only return trips for that route.
NOTE: You must always specify a feedIndex, unless you are querying for all feeds in the database.
NOTE: Generally, getting a group of entities will only return the top-level entity, except in cases such as Feeds where it might be useful to grab Agency and Route data. This may change, and I may add query parameters to make it easier to get more deeply nested structures if it seems useful and performant enough.
Get all feeds, along with the associated Agency and Routes - this is the initial request, as feedIndex is required on all subsequent queries:
{
feeds {
feedId
feedLang
agencies {
agencyId
agencyName
agencyUrl
agencyPhone
}
routes {
routeId
routeShortName
routeDesc
}
}
}Get all Routes:
{
routes(feedIndex: 1) {
routeId
routeDesc
routeColor
}
}Get a specific Route:
{
route(feedIndex: 1, routeId: "B") {
routeId
routeUrl
routeDesc
routeColor
routeShortName
routeLongName
routeType {
routeType
description
}
transfers {
fromStopId
toStopId
}
}
}Get all Trips (you can also specify serviceId):
{
trips(feedIndex: 1) {
tripId
tripHeadsign
routeId
tripType
directionId
}
}Get the next available trip by routeId and directionId (directionId can be 0 or 1, and defaults to 0 if unspecified), this returns useful information that can be used by the client to render a valid route, it's stops, and shapeId (see shape queries below):
{
nextTrip(feedIndex: 1, routeId: "7", directionId: 1) {
tripId
tripHeadsign
directionId
shapeId
route {
routeId
routeShortName
routeLongName
routeDesc
routeColor
}
stopTimes {
stopSequence
departureTime {
hours
minutes
seconds
}
stop {
stopName
geom {
coordinates
}
}
}
}
}Get a Trip, along with Route info, StopTimes with their associated stop and Point geometry:
{
trip(feedIndex: 1, tripId: "ASP21GEN-1037-Sunday-00_000600_1..S03R") {
tripId
tripHeadsign
tripShortName
route {
routeId
routeDesc
}
stopTimes {
stopId
stopSequence
stop {
stopName
stopDesc
parentStation
geom {
type
coordinates
}
}
departureTime {
hours
minutes
seconds
}
}
}
}Once we have trips, with their respective shapeIds, we can query for the actual shape geometry:
Multiple shapes:
(NOTE: You can also specify shapeIds: "7..N97R" to return a single shape, just know that it will still be returned in an array.)
{
shapes(shapeIds: ["7..N97R", "7..S97R"]) {
shapeId
geom {
type
coordinates
}
}
}A single shape:
{
shape(shapeId: "7..N97R") {
shapeId
length
geom {
type
coordinates
}
}
}NOTE: A Stop can have a parentStation defined. A parentStation might be have a stopId of 101, and two stops that have this as a parentStation might be 101N and 101S, indicating separate stops for each direction. We can specify all, only parents, and only children in the query using isParent: true or isChild: true, or omitting those values to get all stop entires:
Get all stops:
{
stops(feedIndex: 1) {
stopId
stopName
geom {
type
coordinates
}
}
}Get all stops that are Parent Stations:
{
stops(feedIndex: 1, isParent: true) {
stopId
stopName
geom {
type
coordinates
}
}
}Get all stops that are not Parent Stations:
{
stops(feedIndex: 1, isChild: true) {
stopId
stopName
geom {
type
coordinates
}
}
}Get a stop, along with its transfers (and transfer-types, just to illustrate what that actually gives us):
{
stop(feedIndex: 1, stopId: "127") {
stopId
stopName
parentStation
stopTimezone
geom {
type
coordinates
}
locationType {
locationType
description
}
transfers {
toStopId
fromStopId
minTransferTime
transferType {
transferType
description
}
}
}
}NOTE: If a stop isn't a parentStation, transfers will be empty (at least according to the MTA data). Querying a parent station should yield a populated transfers array, however, there is always a transfers table entry for the stop itself (perhaps to transfer from Inbound to Outbound), as you can see in the following data:
{
"data": {
"stop": {
"stopId": "127",
"stopName": "Times Sq-42 St",
"parentStation": null,
"stopTimezone": null,
"geom": {
"type": "Point",
"coordinates": [-73.987495, 40.75529]
},
"locationType": {
"locationType": 1,
"description": "station"
},
"transfers": [
{
"toStopId": "127",
"fromStopId": "127",
"minTransferTime": 0,
"transferType": {
"transferType": 2,
"description": "Transfer possible with min_transfer_time window"
}
},
{
"toStopId": "725",
"fromStopId": "127",
"minTransferTime": 180,
"transferType": {
"transferType": 2,
"description": "Transfer possible with min_transfer_time window"
}
},
{
"toStopId": "902",
"fromStopId": "127",
"minTransferTime": 180,
"transferType": {
"transferType": 2,
"description": "Transfer possible with min_transfer_time window"
}
},
{
"toStopId": "A27",
"fromStopId": "127",
"minTransferTime": 300,
"transferType": {
"transferType": 2,
"description": "Transfer possible with min_transfer_time window"
}
},
{
"toStopId": "R16",
"fromStopId": "127",
"minTransferTime": 180,
"transferType": {
"transferType": 2,
"description": "Transfer possible with min_transfer_time window"
}
}
]
}
}
}You can see in the locationType section above that the locationType is station. For "child" stops (e.g., 127N or 127S), you would have a locationType of stop. We've also recieved an array of "stations" that are available for transfer after the minTransferTime value (in seconds). These transfers can be queried together for additional detail as such:
{
stops(feedIndex: 1, stopIds: ["127", "725", "902", "A27", "R16"]) {
stopId
stopName
geom {
type
coordinates
}
}
}Which yields the following:
{
"data": {
"stops": [
{
"stopId": "127",
"stopName": "Times Sq-42 St",
"geom": {
"type": "Point",
"coordinates": [-73.987495, 40.75529]
}
},
{
"stopId": "725",
"stopName": "Times Sq-42 St",
"geom": {
"type": "Point",
"coordinates": [-73.987691, 40.755477]
}
},
{
"stopId": "902",
"stopName": "Times Sq-42 St",
"geom": {
"type": "Point",
"coordinates": [-73.986229, 40.755983]
}
},
{
"stopId": "A27",
"stopName": "42 St-Port Authority Bus Terminal",
"geom": {
"type": "Point",
"coordinates": [-73.989735, 40.757308]
}
},
{
"stopId": "R16",
"stopName": "Times Sq-42 St",
"geom": {
"type": "Point",
"coordinates": [-73.986754, 40.754672]
}
}
]
}
}If you have a station ID (e.g., 127, the parentStation of stop 127N and 127S), you can query for the stops associated with the transfers stations, for instance, if you are wanting to match a station with real-time tripUpdate and vehicle data, which is keyed by stopId:
{
transfers(feedIndex: 1, parentStation: "127") {
stopId
}
}Which yields:
{
"data": {
"transfers": [
{
"stopId": "127N"
},
{
"stopId": "127S"
},
{
"stopId": "725N"
},
{
"stopId": "725S"
},
{
"stopId": "902N"
},
{
"stopId": "902S"
},
{
"stopId": "A27N"
},
{
"stopId": "A27S"
},
{
"stopId": "R16N"
},
{
"stopId": "R16S"
}
]
}
}From this, you can easily filter the results from the real-time feed to get updates relevant to that particular station. NOTE that this query returns full stop entities, so you can also query for name and geometry.
