In this project we will learn how to start up a Postgres database using Docker, create tables for our database, seed data into our database, and then build a basic API to access our database.
You should have already gone through the docker-1-mini project before starting this project.
-
Let's make sure we have
docker-composeinstalled. Run the following commanddocker-compose --versionIf you do not see a version output you may not have Docker installed correctly on your machine.
-
Let's get a bit familiar with
docker-compose.docker-composeis a complimentary tool related to Docker, it helps us run different containers based on a configuration file.- The configuration file specifying the Docker containers to be run is the
docker-compose.ymlfile. You can inspect it at the root directory of this repository. - You'll notice in the
docker-compose.ymlfile there is aservicesentry. This is were we declare various services we'd like to run in Docker containers for our project. Our Postgres database is declared under thedbservice. It's also the only service declaration.
-
Now that you've taken a look at the
docker-compose.ymllets try running a fewdocker-composecommands.-
The first command we'll run is
docker-compose up dbThis command is going to start our Postgres database in a docker container. You'll notice it took a lot fewer command options than a typical
docker runcommand, this is because the options are specified in thedocker-compose.ymlfile.It may take a minute for the container to start since you'll likely have to download the container image on the first run.
Eventually you'll start seeing logs output to your console from the running Postgres instance
-
You can see a list of currently running containers with normal docker commands
docker ps -
Let's now stop the running container.
You can either just use the
ctrl-ckey combination in the terminal window currently running the container, or in another terminal window you can run the following command.docker-compose downThis command will also shutdown the container.
If you had trouble with any of the above commands you may need to trouble shoot your installation of Docker.
-
We've added some
npmcommands you can utilize to perform these same actions. The start command is slightly different in that it starts the container as a daemon, so it doesn't take over the terminal you ran the command in.npm run db:start npm run db:stop
-
-
We now can run the database, but we need to make sure we are able to connect to it from other programs on our computer. We are going to test our database connection with a program called SQL Tabs (click link to download). This is a free program that allows us to connect to the database and run commands.
-
Obviously the database will need to be running to accept connections.
npm run db:start -
Now that the database is running lets connect to it. Open the SQL Tabs program. You'll see an address bar at the top of the window in that bar type in the following address.
postgres://postgres@localhost:5432/postgresThere are a few aspects of this URL we should discuss first.
postgres://- This is the protocol we're going to use, this is a postgres database so it uses thepostgresprotocol.postgres@- This specifies the username we're connecting with.localhost:5432- The address that the database is accepting connections from./postgres- This is the name of the database we're interested in connecting to. There can be more than one database for a postrgres instance.
-
You'll be prompted to enter a password to connect to the database. The password is...
node3dbEnter the password and hit Enter. You should now be connected to the database. If you encounter errors make sure your database is running and that you entered the password correctly.
-
-
Now that we're connected to the database we need to create a database specifically for our application. We can do that by running a SQL command in the SQL Tabs program. Run the following...
CREATE DATABASE node3; -
Our new database is now created, let's disconnect from the default
postgresdatabase and connect to the newly creatednode3database.-
Close the tab that has the connection to the
postgresdatabase. -
Change the connection string to point to the
node3database.postgres://postgres@localhost:5432/node3 -
Connect using SQL Tabs (the password will be the same)
-
We now have a database specifically for our application.
-
Relational databases require defined tables before we can save data in a safe manner. In this step we will define the tables that will make up our database. We're going to use the same entities that we used in the previous project.
-
We could write raw SQL queries to create the tables we'd like, but in many cases it's easier to use other tools to do alot of that work for us. We are going to use a tool called
node-pg-migrateto assist us with creating the tables for our database.node-pg-migrateis a tool that will allow us to declaremigrationsfor our database. You can think of migrations as code that will instruct the database what to do, it's a way of specifying database configuration in code, that can be rolled back, replayed or modified in the future. The idea of migrations is not unique to node.js, it's a common concept when interacting with databases. -
Let's install
node-pg-migratenpm install --save node-pg-migrate pg -
Now we need to add some configuration for
node-pg-migrateso that it knows how to connect to our database when running commands.-
create a directory called
config -
add a file named
default.jsoninside of theconfigdirectory -
add the following configuration to the
default.jsonfile.{ "db": { "user": "postgres", "password": "node3db", "host": "localhost", "port": 5432, "database": "node3" } }
-
-
One last thing before we write our database migrations. We're going to make it a little easier to run th
node-pg-migratetool.-
We installed
node-pg-migratewithnpm install --save node-pg-migratethis means to run the tool we'd have to type outnode_modules/.bin/node-pg-migratewhich is a bit long. To save our fingers from typing so much we're going to create an npm command for running it. -
open
package.json. In thescriptssection add the following..."scripts": { "migrate": "node-pg-migrate" } -
this will allow us to type
npm run migrateto run the tool.
-
-
Let's add some tables now. We'll use our new tool to create migrations for each table we want to create.
-
The first table we'll create will be for users in our application.
-
The schema for a user will look like the following
User { id: primary key, email: text, password: text, } -
Let's create a migration file where we can declare what the migration should do. Run...
npm run migrate create add-users-table -
After that command runs you'll now have a
migrationsdirectory and in that directory there will be a file where we can write our migration. The migration file will have a name somewhat like this (the x's indicating the time-stamp output the command adds)migrations/xxxx_add-users-table.js -
Open the new migration file and add the following...
migrations/xxxx_add-users-table.jsexports.up = pgm => { pgm.createTable('users', { id: { type: 'serial', primaryKey: true, }, email: { type: 'text', notNull: true, }, password: { type: 'text', notNull: true, }, }); };
-
-
Now lets run our migration and actually create the
userstable in the database.npm run migrate up -
You should get a message in you terminal that the migration ran successfully, we can verify that by querying the database using SQL Tabs. Connect to the
node3database and run the following query.SELECT * FROM information_schema.tables WHERE table_name='users';- You should then see the
userstable in query results
- You should then see the
-
-
Let's create the
user_profilestable. Refer to the last step if you need a step by step walk-through.-
schema
UserProfile { id: primary key, userId: foreign key, about: text, thumbnail: text, } -
solution:
migrations/xxxx_add-user-profiles-table.jsexports.up = pgm => { pgm.createTable('user_profiles', { id: { type: 'serial', primaryKey: true, }, userId: { type: 'integer', notNull: true, references: '"users"', // this is how we associate a profile with a specific user. }, about: { type: 'text', }, thumbnail: { type: 'text', }, }); };
-
-
Let's create a
poststable. Remember, you can refer to previous steps if you need a reminder of the commands.
-
schema
Post { id: primary key, userId: foreign key, content: text, } -
solution
migrations/xxxx_add-posts-table.jsexports.up = pgm => { pgm.createTable('posts', { id: { type: 'serial', primaryKey: true, }, userId: { type: 'integer', notNull: true, references: '"users"', }, content: { type: 'text', }, }); };
- Let's create a
commentstable.
-
schema
Comment { id: primary key, userId: foreign key, postId: foreign key, comment: text } -
solution
migrations/xxxx_add-comments-table.jsexports.up = pgm => { pgm.createTable('comments', { id: { type: 'serial', primaryKey: true, }, userId: { type: 'integer', notNull: true, references: '"users"', }, postId: { type: 'integer', notNull: true, references: '"posts"', }, comment: { type: 'text', notNull: true, }, }); };
Remember you have to run npm run migrate up to actually run the migration
files and create the tables.
- After you've run all the migrations you can check for the existence of those tables with the following SQL query.
SELECT * FROM information_schema.tables WHERE table_schema='public';You should then see all of the tables you specified in you migrations.
- Let's insert a
userjust to declare victory on our database setup.
INSERT INTO users (email, password) values ('first.user@example.com', 'fakepassword123');Check the user was inserted.
SELECT * FROM users;Now we've configured our database, but it's not very easy to interact only through SQL queries, so we are going to build an API around our database that allows us to interact with the data through an application.
Much like how we're using a tool to assist us in setting up our database tables, we're also going to use a tool to assist us when connecting to our database from our application. But it's not only going to assist us connecting, it will also assist us in making queries on our data.
-
Install
massivenpm install --save massive -
Install
expressSince we're going to be building an REST API we're also going to need a way to build a server. Let's use
expresssince we're familiar with it at this point.npm install --save express -
Setup
expressboilerplateYou should be familiar enough with express to setup a basic server, do that now.
-
solution
server/index.jsconst express = require('express'); const app = express(); const PORT = 3001; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
-
-
Setup
massiveWe need to instatiate
massivewith the details required to connect to the database we created.// server/index.js const express = require('express'); const massive = require('massive'); massive({ host: 'localhost', port: 5432, database: 'node3', user: 'postgres', password: 'node3db', }).then(db => { const app = express(); app.set('db', db); app.use(express.json()); const PORT = 3001; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); });
You'll notice that we had to move the instantiation of the express application in the the
.thenmethod frommassive. This is becausemassivehas to connect and inspect our database on startup, which is an asynchronous operation.Now we have access to the
massiveinstance throughout ourexpressapplication.
We're going to start out simple and create and endpoint that allows us to create
a new user in the database. The only thing different from previous projects is
that we now have to interact with a real database utilizing massive.
-
Create and endpoint that will receive a
postrequest at the/api/usersendpoint. -
Create a handler that will receive the new user data.
-
In your handler use
massiveto save the new user data to the database.// controllers/users.js function create(req, res) { const db = req.app.get('db'); const { email, password } = req.body; db.users // heres the new stuff, using massive to actually query the database. .save({ email, password, }) .then(user => res.status(201).json(user)) // returns a promise so we need to use .then .catch(err => { console.error(err); // if something happens we handle the error as well. res.status(500).end(); }); } module.exports = { create, }; // server/index.js - register the handler app.post('/api/users', users.create);
This should feel very familiar, all we've done is add
massiveand a database instead of our hacky solution just using Arrays and Objects from previous projects.You should glance over the massive documentation before we move on, to get a bigger picture of what is possible with this tool.
-
Test this endpoint, create a new user.
-
First we need to finish our user creation endpoint, if you remember back to our previous project, you'll remember that when we create a
user, we also need to create auserProfileat the same time.massivehas a way to do exactly this.Modify the user creation handler with the following
db.users .insert( { email, password, user_profiles: [ // this is what is specifying the object // to insert into the related 'user_profiles' table { userId: undefined, about: null, thumbnail: null, }, ], }, { deepInsert: true, // this option here tells massive to create the related object } ) .then(user => res.status(201).json(user)) .catch(err => { console.error(err); });
Now when we create a user they also get a
userProfilecreated. -
Create an endpoint for querying all users
-
URL:
/api/users -
solution:
controllers/users.js - get all usersfunction list(req, res) { const db = req.app.get('db'); db.users .find() .then(users => res.status(200).json(users)) .catch(err => { console.error(err); res.status(500).end(); }); }
server/index.jsconst express = require('express'); const massive = require('massive'); const users = require('./controllers/users.js'); massive({ host: 'localhost', port: 5432, database: 'node3', user: 'postgres', password: 'node3db', }) .then(db => { const app = express(); app.set('db', db); app.use(express.json()); app.post('/api/users', users.create); app.get('/api/users', users.list); const PORT = 3001; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); }) .catch(console.error);
-
-
Create an endpoint for querying a user by their
id- URL:
/api/users/:id - solution:
controllers/users.js - get user by idfunction getById(req, res) { const db = req.app.get('db'); db.users .findOne(req.params.id) .then(user => res.status(200).json(user)) .catch(err => { console.error(err); res.status(500).end(); }); }
server/index.jsconst express = require('express'); const massive = require('massive'); const users = require('./controllers/users.js'); massive({ host: 'localhost', port: 5432, database: 'node3', user: 'postgres', password: 'node3db', }) .then(db => { const app = express(); app.set('db', db); app.use(express.json()); app.post('/api/users', users.create); app.get('/api/users', users.list); app.get('/api/users/:id', users.getById); const PORT = 3001; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); }) .catch(console.error);
- URL:
-
Create an endpoint for querying a user's profile
- URL:
/api/users/:id/profile - solution:
controllers/users.js - get user profilefunction getProfile(req, res) { const db = req.app.get('db'); db.user_profiles .findOne({ userId: req.params.id, }) .then(profile => res.status(200).json(profile)) .catch(err => { console.error(err); res.status(500).end(); }); }
server/index.jsconst express = require('express'); const massive = require('massive'); const users = require('./controllers/users.js'); massive({ host: 'localhost', port: 5432, database: 'node3', user: 'postgres', password: 'node3db', }) .then(db => { const app = express(); app.set('db', db); app.use(express.json()); app.post('/api/users', users.create); app.get('/api/users', users.list); app.get('/api/users/:id', users.getById); app.get('/api/users/:id/profile', users.getProfile); const PORT = 3001; app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); }); }) .catch(console.error);
- URL:
Let's create the API endpoints for posts. You're going to be on your own for these endpoints.
- Create an endpoint to create
postsin the database. - Create an endpoint to get single
postfrom the database.- Also add a query parameter to this endpoint that would allow you to fetch
the
postalong with all of the comments for that post.
- Also add a query parameter to this endpoint that would allow you to fetch
the
- Create an endpoint to get all
postsfor a specific user from the database. - Create an endpoint to update a
post
- Create an endpoint that saves a
commentfor a specificpost - Create an endpoint that allows a user to edit a
comment
We've covered a lot of ground in this project but our API and database still has many flaws that would prevent it from being very useful in a production application.
Think about the problems surrounding uniqueness of data, and consistency of data. Postgres provides many features to help us as developers maintain the integrity of the data we save, we'll cover some of this in a future project but take some time to do some research about some of these capabilities on your own as well.