May 3: Building a PostgreSQL API in JavaScript with Express and Sequelize (Part 1)
During my time at Flatiron School, I studied the basics of web APIs, databases, and object relationship mappers (ORMs) — which all work in harmony to give web applications the incredible functionality that helps us shape our world. We learned all this in Ruby on Rails, a wonderful framework that’s easy to use, relatively secure, and supported by a vibrant community and a wealth of free gems.
Sadly, although many products still rely on Rails, its use seems to be declining. In many circumstances Rails can be rigid, inflexible and slow, though recent versions have added features to address some of these concerns. Nevertheless, the job market seems to be moving toward JavaScript, Python and PHP.
With this in mind, and in a spirit of constant improvement and education, I decided after graduating from Flatiron to branch out and learn other web application frameworks… but of course there aren’t a ton of beginner-friendly resources out there for doing so. So what follows is my attempt at a JavaScript web API cookbook, aimed at those who are transitioning from Rails.
This is a huge topic, and I’ve no doubt this tutorial won’t even come close to doing this subject justice. In fact, there’s so much to cover that I’ll have to split this tutorial into several parts; this first part will just guide you through setting up your project, config and environment.
Background
Please note this tutorial assumes the following:
- You’re familiar with JavaScript syntax and bash commands; and
- You have a working PostgreSQL installation. Download an installer and read installation instructions here.
Sequelize can work with many dialects of SQL, including MySQL, MariaDB, SQLite3 and Microsoft SQL Server. For the sake of brevity I’ll use PostgreSQL in this tutorial because it plays well with the cloud services I use and it’s what I’m most familiar with. Please comment if you’re interested in a tutorial on using Sequelize with another flavor of SQL and I might be inclined to write it.
1: Initialize NPM in a new directory and install packages
Get started in your terminal by making a directory for your API and cd
ing into it. Initialize a new Node project with yarn init --yes
, and a wild package.json
will appear in your directory. The --yes
flag just generates package.json
automatically; feel free to omit it, but I find it easier since you can always edit it manually later.
If you’re using Github (strongly recommended!), your remote will include dependencies in every push if they’re not .gitignore
d in your initial commit. That’s redundant and slows things down, so this is a good time to create a .gitignore
file in the root of your project directory with just the following two lines:
node_modules/
.env
After creating a .gitignore
file, initialize and push your initial commit in the terminal with git init && git add . && git commit -m "initial commit" && git push
.
Finally, install the dependencies you’ll need in the terminal with yarn add bcryptjs cors dotenv express jsonwebtoken pg pg-hstore sequelize --save && yarn add nodemon sequelize-cli --dev
. This upsettingly long terminal command will install the dependencies listed below, ordered alphabetically (not by importance!):
Production dependencies
bcryptjs
: for hashing sensitive data, especially passwordscors
: for allowing cross-origin resource sharing (CORS), to ensure your front-end and back-end are sharing data safely and properlydotenv
: for coordinating and loading environment variables — not strictly necessary but incredibly helpfulexpress
: our application framework — read more about Express herejsonwebtoken
: for implementing JSON web tokens, which standardizes the data we’re sending aroundpg
andpg-hstore
: for connecting to a PostgreSQL server and serializing the data we store/retrieve from itsequelize
: an object-relationship mapper, just like ActiveRecord in Ruby on Rails — read more about Sequelize here
Development dependencies
nodemon
: enables “hot-loading,” so we can see changes immediately instead of having to shut down and restart our serversequelize-cli
: for creating Sequelize projects and defining Sequelize objects/relationships
After running the command above, you should see a node_modules
folder appear, and your package.json
file should look something like this:
{
"name": "My Express API",
"version": "1.0.0",
"description": "A simple Express API",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Josh Frank",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"pg": "^8.6.0",
"pg-hstore": "^2.3.3",
"sequelize": "^6.6.2"
},
"devDependencies": {
"nodemon": "^2.0.7",
"sequelize-cli": "^6.2.0"
}
}
add
, commit
and push
to commit your progress.
2: Create the Express application
Create a file, index.js
, in your root directory, and fill it with some basic code to get your Express app started:
const express = require( "express" );
const cors = require( "cors" );
const app = express();const port = 3000;app.use( express.json() );
app.use( express.urlencoded(.{ extended: true } ) );
app.use( cors( { origin: `http://localhost:${ port }` } ) );app.get( "/", ( request, response ) => response.send( "Test" ) );app.listen( port, () => console.log( `Listening: port ${ port }` ) );
A quick line-by-line breakdown:
- We
require
the"express"
and"cors”
packages, which listen for requests, verify them, and return responses; - We create an Express
app
object and aport
number to listen on; - We tell our
app
to organize and parse request data as.json()
data, to ignore request data that isn’t properly.urlencoded()
, and allow requests coming fromlocalhost
so we can test as we develop; - We define a default
GET
route withapp.get()
, which takes a path ("/"
) and a callback as arguments; - In our
app.get()
function, we write a callback with two arguments,request
andresponse
, to tell Express what to do about aGET
request to"/"
— in this case, kick back a string,"Test"
; - Finally, we listen for requests on the
port
we defined,console.log
ging a message when we start listening.
Now, when you run node index.js
in your terminal, you’ll see a message, Listening on port 3000
, and you’ll see your terminal freeze as the app listens. Open up a second terminal with your app running and run curl localhost:3000
to GET
your test route, and you’ll see Test
come back as a response. Success! You now have a working Express app!
Eventually, of course, our API will grow more complex and do more interesting things than just spitting out Test
. So I highly recommend trying out routes with a REST client instead of using a browser or curl
. The most popular seems to be Postman; I prefer ARC because it’s open-source.
It’s interesting to note at this point that if you change index.js
, your saved changes won’t change your app’s behavior until you shut the app down and restart it. See for yourself by changing your app’s response
on line 11:
...
app.get( "/", ( request, response ) => response.send( "A different test" ) );
If you run curl localhost:3000
in a terminal without restarting app.js
, you’ll still see a response of Test
. It’s tedious to repeatedly shut down and restart our server, so let’s use nodemon
to force our app to “hot-load” and reflect saved changes immediately. In your package.json
file, delete the auto-generated test
script and replace it with this:
...
"scripts": {
"dev": "nodemon index.js"
},
...
You’ve just defined a Node script — a shortcut for terminal commands you use frequently when building your app. Running nodemon app.js
in your terminal will start your Express app with nodemon
instead of directly with node
, so that it restarts with every save. But thanks to your script, you can do the same thing by running yarn dev
, resulting in:
yarn run v1.22.10
$ nodemon app.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Listening: port 3000
Now, your app will automatically refresh each time changes are saved. We’ll be adding many more Node scripts as we build our Express app.
3. Configure dotenv
to provision environment variables
Before we continue, let’s learn about process.env
, what it represents, and how to make the most of it with the dotenv
package we installed.
Every Node project has a process.env
global variable that represents the state of the system environment your application is in. We need it because we need to deploy an app to a server if we actually want to use it, and different servers represent different environments. We might test an app out locally on127.0.0.1
, and then deploy it to a server at 51.16.0.9
, but then have to switch for some reason to a server at 172.0.7.0
. With process.env
, we can connect properly in any environment without having to write code for each one.
We can provision (define & use)process.env
variables several ways — the simplest is at the application level, with code like below:
const process.env.TEST = "Test";
console.log( process.env.TEST );
You can also use the export
terminal command: run export TEST="Test"
in your terminal, and using process.env.TEST
anywhere in your project will yield the string "Test”
.
Both these ways work fine, but provisioning on-the-fly can get out of hand really quickly. Enter dotenv
stage right, which allows us to define and load all our environment variables in one convenient place. Not only does this spare us brain cells and therapy bills, it also makes our app faster and much more usable — so using dotenv
is strongly recommended even though, as I said above, it isn’t strictly necessary.
Since we already installed dotenv
, all we have to do is create a .env
file in the root of our project and add code like this to it:
PORT = 3000
TEST = "dotenv test"
.env
is just a raw text file, not JavaScript, so don’t use semicolons.
Next, in package.json
, let’s tell our Node script all about the environment variables we just defined:
...
"scripts": {
"dev": "nodemon -r dotenv/config index.js"
},
...
Finally, at the top of index.js
, let’s require( "dotenv" )
, delete our port
variable and use those environment variables instead:
require( "dotenv" ).config();
const express = require( "express" );
const cors = require( "cors" );
const app = express();app.use( express.json() );
app.use( express.urlencoded( { extended: true } ) );
app.use( cors( { origin: `http://localhost:${ process.env.PORT }` } ) );app.get( "/", ( request, response ) => response.send( process.env.TEST ) );app.listen( process.env.PORT, () => console.log( `Listening: port ${ process.env.PORT }` ) );
Save your changes so nodemon
will restart your app, and when you curl localhost:3000
in another terminal, you’ll get dotenv test
as a response — confirming that dotenv
is now successfully configured and provisioning!
If you’re using Github, now is a good time to add
, commit
and push
. You’ll remember that we added .env
to our .gitignore
file; that’s because we’ll be adding sensitive information about our database and authorization tokens to our .env
file as we continue building our app. We definitely don’t want that stuff on a public Github repository.
At this point, the file structure of your app should look something like this:
├── node_modules
│ └── ...a LOT of packages in folders
├── .env
├── .gitignore
├── index.js
├── package.json
└── yarn.lock
4. Use sequelize-cli
to set up Sequelize
We’re now at a point where we can start setting up our app to interact with a PostgreSQL database. We’ll do that with help from sequelize-cli
, a command-line interface that lets us generate projects, models and migrations with easy commands.
First, configure sequelize
by making a new file in your project’s root directory called .sequelizerc
and filling it with the code below:
const path = require( "path" );module.exports = {
"config": path.resolve( "./app/config", "database.config.js" ),
"models-path": path.resolve( "./app/models" ),
"seeders-path": path.resolve( "./app/seeders" ),
"migrations-path": path.resolve( "./app/migrations" )
};
All this does is export a a JSON object that directs Sequelize to some files it needs. We haven’t made them yet, but these files have names that should be self-explanatory: config
will hold database configuration, models
will hold our models, seeders
holds our project’s seed (sample/test) data and migrations
holds migrations.
Now, make a folder in the root directory called app
, cd
into it in your terminal, hold onto your hat and run sequelize-cli init
. Voilá! Your Sequelize project now has its config
, models
, seeders
and migrations
files. Your app’s file structure should now look like this:
├── node_modules
│ └── ...package folders
├── app
│ └── config
│ │ └── config.json
│ └── models
│ │ └── index.js
│ └── migrations
│ └── seeders
├── .env
├── .sequelizerc
├── index.js
├── package.json
└── yarn.lock
Before we continue, let’s take a detour to our .env
file and add some important variables Sequelize needs to communicate with PostgreSQL:
PORT = 3000
DEV_DATABASE_HOST = "127.0.0.1"
DEV_DATABASE_USERNAME = "postgres"
DEV_DATABASE_PASSWORD = "postgres"
A quick note:
Configuring PostgreSQL can be very frustrating, and it’s a topic that’s unfortunately beyond this tutorial’s scope. If you’re testing your app on your local machine with
localhost
,HOST
will be127.0.0.1
, and yourUSERNAME
andPASSWORD
will be the username/password of your preferred local PostgreSQL user; if you’ve never changed them, they’ll both be the default values"postgres”
. If you’re using an external host, your hosting company almost certainly provides a set of instructions for configuring a PostgreSQL database on their server, and a “Settings” or “Config” panel listing the host address/username/password. Helpful instructions abound online; comment and I might write some.
The next tasks on our to-do list will concern the files we auto-generated in our app folder. We’ll start with config/config.json
because we need it to match our .sequelizerc
paths and use our environment variables. Adjust it like so:
- Rename the file to
database.config.js
; - Add
module.exports = { ... }
before theconfig
object and refactor it from a JSON object to a JavaScript object by removing the quotes surrounding all the keys; - Add the two lines of code below at the top of the file, to
require( "dotenv" )
and destructure those important database variables we just created from our.env
ironment; and - Change the
database:
name to the name of your project, and the SQLdialect:
to"postgres"
.
When you’re done, database.config.js
should look like this:
require( "dotenv" ).config();const { DEV_DATABASE_HOST, DEV_DATABASE_USERNAME, DEV_DATABASE_PASSWORD } = process.env;module.exports = {
development: {
username: "root",
password: null,
database: "my_express_app_development",
host: "127.0.0.1",
dialect: "postgres"
},
test: {
username: "root",
password: null,
database: "my_express_app_test",
host: "127.0.0.1",
dialect: "postgres"
},
production: {
username: "root",
password: null,
database: "my_express_app_production",
host: "127.0.0.1",
dialect: "postgres"
}
}
Finally, use those .env
ironment variables in our development:
object. test:
and production:
won’t come into play until we’re ready to deploy, so feel free to comment them out and ignore them for now.
...
module.exports = {
development: {
username: DEV_DATABASE_USERNAME,
password: DEV_DATABASE_PASSWORD,
database: "my_express_app_development",
host: DEV_DATABASE_HOST,
dialect: "postgres"
},
...
}
Next let’s take a peek at our other auto-generated file, models/index.js
. Change line 8 so our config
variable points to the right place—database.config.js
and not config.json
anymore. Look lower down and you’ll see the logic that actually uses our config
to start a new Sequelize()
:
'use strict';const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require( __dirname + "/../config/database.config.js" )[ env ];
const db = {};let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
Run yarn dev
in the terminal. If you see errors, go back through this tutorial and make sure all the changes to boilerplate sequelize
code were made correctly with no typos. If all has gone according to plan and the gods are smiling, you should see a familiar Listening: port 3000
message, and no errors. add
, commit
and push
to commit your progress.
On the next episode
Our Express app is created, our environment is provisioned, and our Sequelize configuration is in place. We’ve covered a lot of ground and we’re ready for part 2 of this tutorial, where we’ll create models, associations and migrations.
Later, we’ll briefly cover signing up users, hashing their passwords with bcryptjs
, and authenticating their connections with jsonwebtoken
s. All this will give you a basic but important base of knowledge for building a robust and professional JavaScript backend with Express!