Setting up a GTFS app
This tutorial and its repository show how to build the core setup used by most of MobilityDataDev's GTFS projects. They contains starter code/configuration for:
- a frontend web application
- a NodeJS backend server
- a PostgreSQL database (containing GTFS data)
- and Docker containers to run it all.
Let's get started!
0. Prerequisites
First, make sure you have the required programmes and a repository available on your machine.
- Install Visual Studio Code.
- Install Docker Desktop.
- Install NodeJS.
- Create a new directory on your machine and open it in VSCode.
- Save your work on GitHub with:
git init
git branch -M main
git add .
git commit -m"v1.0.0"
git remote add origin https://github.com/[username]/[repository name].git
git push -u origin main
If you want, you can allocate a specific amount of resources to Docker. I'm using 4GB of memory and 2 processors, set with this command: " [wsl2]```n memory=4GB```n processors=2" | Out-File -FilePath "$env:USERPROFILE\.wslconfig" -Encoding utf8
followed by wsl --shutdown
. You can also check your current settings, if they exist, with cat "$env:USERPROFILE\.wslconfig"
.
Ok, you're all set up. Let's build the foundations for a GTFS application!
1. NodeJS app
Start with a super basic app that only logs a message, just to have visual confirmation that it works.
-
Create a
./src/
directory at the root of the repository. This will hold all the JavaScript (and later Typescript) files, which basically constitute the brain of the app. -
Inside that subdirectory, create the app's entry file, i.e.
./src/index.js
, containing the message:
console.log(`I'm an app!`);
- Run
npm init
to generate the default./package.json
file, giving./src/index.js
as the entry point when prompted.
Test it by running node ./src/index.js
. It will display the "I'm an app!" message in the terminal. Perfect.
Actually, nearly perfect. It would be better in a Docker container.
2. Docker containers
Create the following files at the root of the repository to build a NodeJS container.
Containerizing your Node application has numerous benefits. First, Docker's friendly, CLI-based workflow lets any developer build, share, and run containerized Node applications. Second, developers can install their app from a single package and get it up and running in minutes. Third, Node developers can code and test locally while ensuring consistency from development to production. – Source
./Dockerfile
A Dockerfile is a blueprint for a custom Docker image. Ours is the combination of NodeJS' default image and of the app's packages listed in ./packages.json
(i.e. the app's dependencies).
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. – Source
An image is a read-only template with instructions for creating a Docker container. Often, an image is based on another image, with some additional customization. – Source
Containers have everything that your code needs in order to run, down to a base operating system. – Source
A package in Node.js contains all the files you need for a module. Modules are JavaScript libraries you can include in your project. – Source
### Custom Node image for gtfs-app ###
# Uses Node's default image as the template.
FROM node:alpine
# Creates the app's directory inside the container.
WORKDIR /gtfs-app
# Copies the app's package(-lock).json files into the directory above.
# These files lists the app's dependencies/packages.
COPY package*.json ./
# Installs the listed dependencies in the app's directory.
RUN npm install
# Generates a new image based on the contents of the current directory
# (= the first dot) and saves it in the current directory (= the second dot).
COPY . .
./docker-compose.yml
This file defines all the containers used in the project. So far you only need a NodeJS container (made from the custom image defined in the ./Dockerfile
). Later, this will also include a PostgreSQL database and a pgAdmin user interface.
Docker Compose is a tool for defining and running multi-container applications. – Source
version: '3.9'
services:
node-app-service:
container_name: node-app-container
build:
context: .
dockerfile: Dockerfile
command: sh -c 'node ./src/index.js'
volumes:
- ./src/:/gtfs-app/src/
3. Launch!
Run docker-compose up --build
in VSCode's terminal. You should still see "I'm an app!" appear in the logs.
Congratulations, you now have a NodeJS app running on your machine through Docker!
How?
docker-compose up
triggers the creation of the container defined in ./docker-compose.yml
. By appending the --build
flag, it also triggers the creation of the Docker image defined in the ./Dockerfile
. Then, it runs the container's command
which starts the app from the ./src/
codebase.
Note that it takes time to download/install all the dependencies to generate the Docker image. So from now on, as long as the Docker image is still valid (i.e. no edits to ./Dockerfile
and no new packages), the --build
flag should be omitted.
Try it: run docker-compose up
. The "I'm an app!" message appears instantly this time.
4. TypeScript
Next, let's replace JavaScript with TypeScript. This really improves the developer experience.
TypeScript is JavaScript with syntax for types. TypeScript adds additional syntax to JavaScript to support a tighter integration with your editor. Catch errors early in your editor. – Source
-
Run
npm install typescript @types/node ts-node --save-dev
in VSCode's terminal to enable TypeScript. -
Create a
.gitignore
file at the root of the repository to exclude this directory created by npm and the build directory:
build/
node_modules/
- Run
tsc --init
to generate the default./tsconfig.json
file, then uncomment and edit the following lines:
{
"target": "es6",
"rootDir": "./src",
"moduleResolution": "node",
"baseUrl": "./src",
"outDir": "./build"
}
-
Change the extension of the app's entry file,
./src/index.js
, to.ts
. -
In
./package.json
, update the file extension in the line"main": "./src/index.js"
to.ts
and add two commands to thescripts
property:
{
"scripts": {
"build": "tsc",
"start": "node ./build/index.js"
}
}
- Update the command in
./docker-compose.yml
so that it compiles the TypeScript codebase into JavaScript, then launches the compiled code:
services:
gtfs-import-node-app-service:
command: sh -c "npm run build && npm run start"
- Create a
.dockerignore
file at the root of the repository to tell Docker what to omit when building an image:
.git/
.gitignore
.vscode/
build/
LICENCE
node_modules/
nodemon.json
public/
README.md
sample-data/
Re-run docker-compose up --build
. You should still get the "I'm an app!" message, but this time it's from a TypeScript app. Cool!
5. Nodemon
Final bit of environment setup: nodemon. This isn't necessary, but it makes the local development experience a bit nicer.
"nodemon is a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected." -- Source
-
Run
npm install nodemon --save-dev
. -
Create
./nodemon.json
to watch for changes in the codebase and automatically restart the NodeJS app when they occur:
{
"verbose": true,
"ignore": ["src/**/*.spec.ts"],
"watch": ["src/**/*.ts"],
"exec": "node --inspect=0.0.0.0:9229 --nolazy -r ts-node/register"
}
- In
./package.json
, add a "dev" command to thescripts
property:
{
"scripts": {
"dev": "nodemon -L ./src/index.ts",
"build": "tsc",
"start": "node ./build/index.js"
}
}
- Update the command in
./docker-compose.yml
to launch the app using the dev script, and add a volume for nodemon's configuration file:
services:
node-app-service:
command: sh -c "npm run dev"
volumes:
- ./nodemon.json:/gtfs-app/nodemon.json
Update the containers once more with docker-compose up --build
. You see the "I'm a NodeJS app!" message again, but also a bunch of new lines prefixed with [nodemon]
including this one: [nodemon] watching 1 file
. This "1 file" is ./src/index.ts
. Edit it to change the message to the standard "Hello World", save, and watch the app automatically reload and display the new message!
Phew. We're done with the foundations. Now for the fun part.
6. Express server
So far, your NodeJS app only outputs a message, which is pretty useless. The next step is to replace this with an Express server able to handle requests from the frontend and respond with GTFS data.
Express.js is the most popular web framework for Node.js. It is designed for building web applications and APIs and has been called the de facto standard server framework for Node.js. – Source
-
Run
npm install express
andnpm install @types/express --save-dev
. -
Overwrite
./src/index.ts
with Express' starter code, enriched with some TypeScript typing:
//
import express, { Request, Response, Application } from 'express';
//
const app: Application = express();
//
app.get('/', (request: Request, response: Response) => {
response.send(`I'm an app!`);
});
//
app.listen(4000, () => {
console.log(`Server listening on http://localhost:4000/`);
});
- Map your browser's 4000 port to the container's in
./docker-compose.yml
:
services:
node-app-service:
ports:
- '4000:4000'
-
Run
docker-compose up --build
again to include the new dependencies. The app loads and... there's no more message! It makes sense, it's gone from the codebase. But there's something else to see... -
Open http://localhost:4000/.
And there it is: "I'm an app!". The app now delivers content to the browser.
7. Frontend
Ok the browser is showing data from the backend, that's great but it's just ~an ugly~ a simple line of text. Time to turn this into a proper web page. We'll use the Pug template engine for that.
A template engine enables you to use static template files in your application. At runtime, the template engine replaces variables in a template file with actual values, and transforms the template into an HTML file sent to the client. – Source
-
Run
npm install pug
andnpm install @types/pg --save-dev
. -
Create two new directories at the root of the repository:
./views/
and./public/
. -
In
./docker-compose.yml
, add the new directories as volumes:
services:
node-app-service:
volumes:
- ./public/:/gtfs-app/public/
- ./views/:/gtfs-app/views/
- Create a
./views/index.pug
file containing basic HTML starter code and onetest
variable:
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title GTFS App
link(rel="stylesheet", href="/style.css")
body
p= test
- Create a
./public/style.css
file to make the text a bit nicer, and red for obvious visual confirmation that it works:
p {
font-family: Helvetica, Arial, sans-serif;
font-size: 15px;
color: red;
}
- Edit
./src/index.ts
to define pug as the template engine, set the views and public directories, and render./views/index.pug
instead of simply logging a message:
import express, { Request, Response, Application } from 'express';
//
import path from 'path';
const app: Application = express();
//
app.set('view engine', 'pug');
//
app.set('views', path.join(__dirname, '..', 'views'));
//
app.use(express.static(path.join(__dirname, '..', 'public')));
app.get('/', (request: Request, response: Response) => {
//
response.render('index', { test: `I'm an app!` });
});
app.listen(4000, () => {
console.log(`Server listening on http://localhost:4000/`);
});
Run docker-compose up --build
again, refresh the browser, the message is still there, in red this time. It works but it's still pretty bare.
- Use Bootstrap's starter template for a nice-looking base layout:
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title GTFS App
link(
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous")
link(rel="stylesheet", href="/style.css")
body
nav.navbar.navbar-dark.bg-dark
a.navbar-brand.px-2(href="/") GTFS App
div.main.container-fluid
div.row
div.col
h1.p-5.text-center= test
script(
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
)
It looks much better already. Now let's finally get some GTFS data in here instead of this random "I'm an app!" message.
8. ProgreSQL database
To run a database locally, you're going to need a new Docker container plus an extra package, because NodeJS doesn't have ready-made features to connect and interact with a database.
There are several databases available, including NoSQL (e.g. MongoDB), MySQL, and PostgreSQL. We recommend the latter: it's the richest, most powerful of the three options, and it comes with a PostGIS extension which is very handy to work on GTFS data. Next, there are yet more options to use that database in a NodeJS app, such as ORMs, pg-promise and node-postgres. The latter is the lowest level of all, which usually means it's more efficient, so that sounds good.
PostgreSQL: The World's Most Advanced Open Source Relational Database – Source
PostGIS extends the capabilities of the PostgreSQL relational database by adding support for storing, indexing, and querying geospatial data. – Source
Object Relational Mapping (ORM) is a technique used in creating a "bridge" between object-oriented programs and, in most cases, relational databases. – Source
[pg-promise is a] PostgreSQL interface for Node.js (...) built on top of node-postgres. – Source
node-postgres is a collection of node.js modules for interfacing with your PostgreSQL database. – Source
- In
./docker-compose.yml
, add a new container definition based on PostgreSQL's latest official image:
services:
postgresql-database-service:
container_name: postgresql-database-container
image: postgres
restart: always
ports:
- '5432:5432'
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
- POSTGRES_DB=root
- Load sample data on build by creating a new
./sample-data/
directory, filling it with SQL files from an existing database dump (like ours), and adding it as an "entry point" volume in./docker-compose.yml
:
services:
postgresql-database-service:
volumes:
- ./sample-data/:/docker-entrypoint-initdb.d/
- Edit
./docker-compose.yml
once more to share the new database's credentials with the NodeJS app, and make it a dependency:
services:
node-app-service:
depends_on:
- postgresql-database-service
environment:
- POSTGRES_HOST=postgresql-database-container
- POSTGRES_PORT=5432
- POSTGRES_USER=root
- POSTGRES_PASSWORD=root
- POSTGRES_DB=root
-
Install node-postgres with
npm install pg
andnpm install @types/pg --save-dev
. -
Create a new
./src/getDatabasePool.ts
file to create a database connection as described in pg's documentation:
import { Pool } from 'pg';
export const getDatabasePool = (): Pool => {
// "process" is a core module of NodeJS. Its "env" property hosts the
// environment variables set when the process was started (i.e. those given
// in ./docker-compose.yml).
const { POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD } =
process.env;
return new Pool({
host: POSTGRES_HOST,
port: parseInt(POSTGRES_PORT, 10),
database: POSTGRES_DB,
user: POSTGRES_USER,
password: POSTGRES_PASSWORD
});
};
- Import and use the connection in
./src/index.ts
to fetch GTFS stops from the database, wrapping the request in a try-catch for good measure:
import { getDatabasePool } from './getDatabasePool';
const pool = getDatabasePool();
app.get('/', async (request: Request, response: Response) => {
try {
const stops = await pool.query('SELECT stop_name FROM stops LIMIT 1');
const message = `I'm a GTFS stop. My name is ${stops.rows[0].stop_name}`;
response.render('index', { test: message });
} catch (error) {
console.error(error);
response.status(500).send('Internal Server Error');
}
});
You're nearly there! Run docker-compose up --build
, wait for the new components to get installed, then refresh http://localhost:4000/. And voilà. It's done!
You now have a web page displaying a GTFS stop name, that was fetched from a database through a backend server. Good job!
9. pgAdmin user interface
Last chapter, promise! It's an easy one, too.
It can be hard working on a database without a graphical user interface. So let's add one. The most popular option for a PostgreSQL database is pgAdmin, which you can run as a local web-based application using yet another container.
A graphical user interface (GUI) is a digital interface in which a user interacts with graphical components such as icons, buttons, and menus. In a GUI, the visuals displayed in the user interface convey information relevant to the user, as well as actions that they can take. – Source
- In
docker-compose.yml
, add a new container definition for pgAdmin and a couple of volumes to connect it to the database:
services:
postgresql-database-service:
volumes:
- local_pgdata:/var/lib/postgresql/data
pgadmin-gui-service:
container_name: pgadmin-gui-container
image: dpage/pgadmin4
restart: always
ports:
- '8001:80'
environment:
PGADMIN_DEFAULT_EMAIL: user@mail.com
PGADMIN_DEFAULT_PASSWORD: root
PGADMIN_CONFIG_WTF_CSRF_CHECK_DEFAULT: 'False'
PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False'
volumes:
- pgadmin-data:/var/lib/pgadmin
volumes:
local_pgdata:
pgadmin-data:
Start the containers one last time with docker-compose up --build
. It takes a bit of time for the new image to download, but when it's done the logs will display "pgadmin-gui-container (...) Listening at: http://[::]:80".
-
Log in using the credentials set in
./docker-compose.yml
, i.e. email "user@mail.com" and password "root". -
In the top navigation bar, click
Object > Register > Server...
then fill in the credentials from our PostgreSQL database:- General:
- Name: GTFS
- Connection:
- Host:
postgresql-database-container
- Port:
5432
- Username:
root
- Password:
root
- Host:
- General:
-
Click
Save
. -
In the sidebar, expand
Databases > root > Schemas > public > Tables
.
And there they are: GTFS tables with all of their columns and rows.
What's next?
Now, you can use this setup to build many different GTFS-based applications. Either build upon what you already have here, or fork our repository to start from a fresh copy.
A fork is a new repository that shares code and visibility settings with the original "upstream" repository. Forks are often used to iterate on ideas or changes before they are proposed back to the upstream repository, such as in open source projects or when a user does not have write access to the upstream repository. – Source