Learn Postgres with Node
Learn how to use a Postgres database on a Node server
- js
- sql
- postgres
This workshop covers how to connect your Node server to a Postgres database using the node-postgres
library.
Setup
Before you begin make sure you have installed Postgres.
- Download the starter files
cd
into the directory- Run
npm install
The starter files include some dependencies and database setup. As you work through the workshop you should read the corresponding files and try to understand what the code does. Each file includes explanatory comments to help.
Database setup
In order to run our app locally we’ll need a Postgres database running on our machine to connect to.
Creating a local database
You can create a new Postgres user and a new Postgres database owned by that user with these two commands:
createuser learn_pg_user &&
createdb learn_pg --owner learn_pg_user
If this succeeds you shouldn’t see any output in your terminal. You can check it worked properly by listing your databases with this command:
psql --list
You should see the new learn_pg
database in the list, like this:
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
learn_pg | learn_pg_user | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
Populating the database
You can list all the tables in your new database with this command:
psql learn_pg --command "\dt"
You should see “Did not find any relations.” This is because our database is empty—we haven’t created any tables or inserted any data.
You can populate your database by running SQL commands to create tables and insert data. Doing this manually would be slow and repetitive, however you can run them from a file in the repo instead:
psql learn_pg --file "./database/init.sql"
The /database/init.sql
file contains SQL commands to create the tables we want, then insert some example data. You can re-run this command to wipe your DB and start from scratch whenever you need to.
This is dangerous. If you run this on your production database you’ll delete all your data.
Connecting to the database
We can query our DB manually from the terminal using psql
, but that doesn’t help us build an app. We need a way for our Node server to connect to the DB. To do this we use the node-postgres
library. You need to install this into the project with:
npm install pg
Our app needs to know the database’s address. Postgres runs a local server so you can talk to the DB. The full URL (also known as the “connection string”) for your database will be:
postgres://learn_pg_user:@localhost:5432/learn_pg
You could hard-code this URL into our app code, but this address is only correct for the database running on your computer. When someone else clones your repo they’ll have their own DB set up.
Environment variables
It’s best to read configuration like this from an “environment variable” (env vars). This is like a JS variable, but set in your terminal before a program runs. You can set them before you start your application, like this PORT=3000 node server.js
. You server can then read this value to know what port your app should listen on.
Take a look at the /database/connection.js
file. It imports the pg
library, then connects to the right DB by passing in the connection string. It reads this from the DATABASE_URL
env var, which means we must make sure this is set before starting our server.
Rather than type DATABASE_URL=postgres://... npm run dev
every time, we can rely on the popular dotenv
library. This allows us to define env vars in a file named .env
. We gitignore this file so each person who clones the repo can make their own with their own personal DB URL.
First install dotenv
as a dev dependency:
npm install --save-dev dotenv
Then create a .env
file at the root of your project containing:
DATABASE_URL='postgres://learn_pg_user:@localhost:5432/learn_pg'
Then change your dev
npm script in the package.json
file to this:
"nodemon -r dotenv/config server.js"
The -r dotenv/config
bit tells the dotenv
library to read the .env
file and pass all the values inside it to your application. You can then access them in your JS code with process.env.VAR_NAME
.
Using the database
Now our server knows how to talk to our database we can start using it in our route handlers. First let’s make our home route list all the users in the database.
Open workshop/routes/home.js
. To access our DB we need to import the pool object we created in connection.js
:
const db = require("../database/connection.js");
Querying for data
This db
object has a .query
method that sends SQL commands to the database. It takes a SQL string as the first argument and returns a promise. This promise resolves when the query result is ready.
Let’s get all the users in the DB:
const db = require("../database/connection.js");
function get(request, response) {
db.query("SELECT * FROM users").then((result) => {
console.log(result);
});
response.send("<h1>Hello world</h1>");
}
Refresh the home page and you should see a big object logged in your terminal. This is the entire result of our database query.
The bit we’re actually interested in is the rows
property. This is an array of each matching entry in the table we’re selecting from. Each row is represented as an object, with a key/value property for each column.
You should see an array of user objects, where each object looks like this:
{ id: 1, username: 'Sery1976', age: 28 }
Since DB queries return promises we need to make sure we send our response inside the .then
callback. Let’s send back a list of all users’ first names:
const db = require("../database/connection.js");
function get(request, response) {
db.query("SELECT * FROM users").then((result) => {
const users = result.rows;
const userList = users.map((user) => `<li>${user.username}</li>`).join("");
response.send(`<ul>${userList}</ul>`);
});
}
Refresh the page and you should see an unordered list containing each user’s first name.
Challenge
We’re currently querying for too much data: we only need the username
, but we’re getting every column. For very big data sets this could be a performance problem.
Amend your query so it only returns the column we need.
Updating data
Navigate to http://localhost:3000/create-user. You should see a form with fields for each column in our user database table. It submits a POST
request to the same path. The post
handler logs whatever data was submitted. Try it now to see it working.
We want to use the INSERT INTO
SQL command to create a new user based on the user-submitted information.
Safely handling user input
Including user-submitted information in a SQL query is dangerous. A malicious user could enter SQL syntax into an input. If we just inserted this straight into a query this would mean they could execute dangerous commands in our DB. This is one of the most common causes of major hacks, so it’s important to prevent it.
You should never directly insert user input into a SQL string:
// NEVER DO THIS!
db.query(`INSERT INTO users(username) VALUES(${username})`);
If the user typed ; DROP TABLE users;
into the username
input we’d end up running that command and deleting all our user data!
The pg
library uses something called “parameterized queries” to safely include user data in a SQL query. This allows it to protect us from injection. We can leave placeholders in our SQL string and pass the user input separately so pg
can make sure it doesn’t contain any dangerous stuff.
db.query("INSERT INTO users(username) VALUES($1)", [username]);
We use $1
, $2
etc as placeholders, then pass our values in an array as the second argument to query
. pg
will insert each value from the array into its corresponding place in the SQL command (after ensuring it doesn’t contain any SQL).
Challenge
Edit the post
handler function in create-user.js
to save the submitted user data in the database. Make sure to use a parameterized query.
Relational data
So far we’ve only touched the users
table. Let’s make the posts
visible too.
- Add a new route
GET /posts
- This should display a list of all the posts in your database
- Once that’s working amend the handler to also show the username of each post’s author.
Hint: You’ll need to use a join to get data from both tables in one query.