REST API with Node.js, Express & PostgreSQL

Christopher Af Bjur

September 1st, 2020

In this tutorial we'll integrate PostgreSQL into our REST API that we built in part 1 of these series. If you haven't checked out that part yet, I advice you to do so before continuing with this tutorial.


Prerequisites

In order to be able to follow along you will need:


Project Structure

  • First pull down this github repo.

  • Then install it by running npm init in a terminal within the project root.

  • Then create these new files ./config/db.js, ./database.js and ./setup.js in the root folder.

  • This means that your project root should now look like so:

config
db.js
node_modules
database.js
index.js
messagesTable.js
package.json
package-lock.json
README.md


Setting up PostgreSQL

Follow this tutorial in order to setup the PostgreSQL environment on your computer. You will be ready in no time!

When you're finished with that short tutorial, keep in mind that you'll need to remember the username, password and database name later in this tutorial.


Creating the table & columns

We'll use pgAdmin in order to create a message table in which we will store our message data.

  • So click and expand your database

  • Find Schemas and expand it

  • find public and expand it

  • Right click on Tables and in the popup menu select Query Tool...

pgadmin 4 tables query tool
  • Now the Query Editor should open up

  • We'll run a query which creates a table called messages with two columns in it called id and text.

  • Copy/Paste the following query there

Query

DROP TABLE IF EXISTS messages;

CREATE TABLE messages (
    id SERIAL PRIMARY KEY,
    text VARCHAR(255)
  ); 
  • Click the little play icon or press f5 to run the query.

pgadmin 4 create messages table query
  • Next right click on Tables in the menu to the left and click Refresh...

  • You should now see the table and the two columns id and text that we just created.

pgadmin 4 table messages columns id text

Well done! Now we have successfully added the messages table in which we will store and read messages through our API.


Preparing the API

Now that you've created and setup the database it's time that we make use of it!

  • Well start of by modifying adding some code to the file ./config/db.js.

  • Replace yourusername with your database user.

  • Replace youruserpassword with your database user password.

  • Replace yourdatabasename with your database name.

./config/db.js

module.exports = {
  host: "localhost",
  user: "yourusername",
  password: "youruserpassword",
  database: "yourdatabasename",
};

Then we'll modify the file database.js in our project root. It should then look like this:

database.js

const CONFIG = require("./config/db");
const Pool = require("pg").Pool;

const pool = new Pool({
  ...CONFIG,
  port: 5432,
});

module.exports = pool;
Detailed Explaination
  • We require our database configuration as well as the pg dependency we installed earlier.

  • We then create a connection pool by initializing a new Pool that we store in a variable called pool.

  • We then destruct our properties from the CONFIG object into the new Pool calls argument object. We also apply the PostgreSQL port. This is everything needed to connect to PostgreSQL.

  • We then export the connection pool so that we can use it in our route functions to request data from PostgreSQL.

Now we are done with database.js. Good job so far! Next - this is what I want you to do:

  • Save the changes and head over to index.js

  • Import the connection from database.js and store it in a variable called db.

  • We can also remove the defined MESSAGES variable now, since we're going to replace the simulated database from the previous tutorial with a real database.

  • This means that we can also get rid of the file messagesTable.js so go ahead and delete that file.

  • With these new changes made, the first part of index.js (before the actual routes/endpoints are defined) is finished.

index.js

const express = require("express");
const db = require("./database");
const app = express();
const port = 3000;

app.use(express.json());

Migrating POST request handlers

Now the time has come to migrate our request handlers that we created in the last section. Yay!

There's going to be quite a bit of changes for each handler so my advice is that you copy/paste the upcoming code snippets and replace them with the old ones in your project and then after each snippet, I'll explain in detail what we're doing. Note that since I've already explained how the app request methods and their callbacks work, I'll focus on the code within the callback functions. Incase you need a recap, have a look here. Sound good? Ok. Let's go!

So let's begin with our post request handler.

index.js

app.post("/", function (req, res) {
  const query = "INSERT INTO messages (text) VALUES ($1)";
  const values = [req.body.text];

  db.query(query, values, function (err, result) {
    if (err) throw err;
    const message = { id: result.insertId, text: values[0] };
    res.status(200).send(message);
  });
});
Detailed Explaination
  • We define a query variable in which we store an SQL query string which will insert data into the text column of our messages table in our database, once we run it. Note that we've specified a placeholder $1. All the placeholders that we use in a query string will be replaced with the values stored in the array that we've defined underneath, called values. So in this case we have 1 placeholder in the query string called $1, meaning it will be replaced with the first index of the values array, which in this case is the request body text that we passed to this endpoint (the message that we want to save in the database).

    If we'd want to insert multiple values, we could use more placeholders in the correct positions of the SQL query string and have more indexes in the values array and they would be replaced depending on placeholder index and values index. But in this case, we only have 1 placeholder to replace with a value.

    You may ask yourself; "Ok, but why the heck can't I just use a template literal and define an expression instead of a placeholder that will be replaced later on?"

    Well, the short answer is... We want to avoid being targets for SQL injection attacks.

    I will not go into detail on injection attacks but basically it means that a malicious user could modify data in our database without our permission.

  • Next, we use the query method of our connection that we stored in a variable called db that we previously defined. This method takes 3 arguments.

    The query which holds the PostgreSQL statement to create a new row in our messages table and populate the column value text with a value.

    The values array which indexes will replace the the placeholders of the query.

    A callback function that will be called as soon as PostgreSQL has completed the operation.

  • Now, in the callback function we have two parameters err and result. If we get an error we throw that error.

  • If we didn't get an error, we can assume that we have a result from PostgreSQL. We get the insertId, from the result object, which is the index of the last row inserted into the messages table of our database. We use this insertId as well as the first values index, which will be the request.body.text, to construct a message object that we send back as a response with a response status code of 200.


Migrating GET request handlers

Time to migrate our GET request handlers. As you probably remember from the last tutorial of these series, we have two types of GET endpoints. One for getting all the messages and the second for getting a message with a specific id.

So let's begin with the one for getting all the messages.

index.js

app.get("/", function (req, res) {
  const query = "SELECT * FROM messages";

  db.query(query, function (err, result) {
    if (err) throw err;
    const messages = result.rows;
    res.status(200).send(messages);
  });
});
Detailed Explaination
  • In the callback function, we define a SQL query string which will tell MySQL to give us all rows from the messages table in our database.

  • In the db.query call we pass the query as well as a callback function. Note that we do not pass any values here like in the POST request - simply because we do not have any data to insert. This time we only want to get everything in the messages table.

  • As usual we throw and error if there is one. If not, we get the result.rows and for clarity we save it in a variable called messages.

  • Just like before we send all those messages back as a response with a status code of 200.

Now it's time to migrate our second GET request endpoint.

index.js

app.get("/:id", function (req, res) {
  const id = parseInt(req.params.id);
  const query = "SELECT * FROM messages WHERE id = $1";
  const values = [id];

  db.query(query, values, function (err, result) {
    if (err) throw err;
    const found = result.rows;

    if (found.length > 0) {
      res.status(200).send(found);
    } else {
      res.status(404).send({ msg: `No message with id ${id} found!` });
    }
  });
});
Detailed Explaination
  • We get the id from req.params.id which will be of string type and parse it into an integer that we store in a id variable.

  • We create a query and a values array.

  • Similar to the POST request, we pass query and values as arguments to the db.query call as well as a callback function.

  • We throw an error if there is one, as usual.

  • If there's no error, we create a variable found and store the result.rows in it for clarity.

  • Since the result.rows is always an array, and we stored it in found, we create a condition to check if there were any found items (if the length of the found array is greater than 0). If it is, we send the found array as a response with a statuscode of 200.

  • If on the other hand found is an empty array, meaning we couldn't find anything in our database based on the id that we passed, we send an object as a response with the statuscode 404 (not found), containing a error message.


Migrating PUT request handlers

Time to migrate the PUT request endpoint.

index.js

app.put("/:id", function (req, res) {
  const id = parseInt(req.params.id);
  const query = "UPDATE messages SET text = $1 WHERE id = $2";
  const values = [req.body.text, id];

  db.query(query, values, function (err, result) {
    if (err) throw err;
    const updated = [{ id: values[1], message: values[0] }];

    if (result.rowCount === 0)
      return res
        .status(404)
        .send({ msg: `No message with id ${values[1]} found!` });

    res.status(200).send(updated);
  });
});
Detailed Explaination
  • A lot of the syntax here might look similar to the previous request handlers that we've migrated. How ever, note here that the query uses two placeholders and therefore two indexes in the values array, which is the req.body.text (t updated message text) as well as the id for the message that we want to update the message text for.

  • In the callback we handle potential errors as usual.

  • If there's no error, we store an object that consist of values data, in an array that we pass to a variable called updated. This is the data which we will send as a response in case we were able to update the specified message in MySQL.

  • Next we create a condition where we check if result.rowCount is equal to zero. This means that we were unable to find and update any message in our database with the specified id and therefore result.rowCount will be zero since no rows where affected. If it is zero we'll return here and send an error message as a response.

  • If on the other hand there were more than zero affected rows, which means that we actually succeded to find and update a message with the specified id, we send a response with the status code 200 and pass the updated array within that response.


Migrating DELETE request handlers

Almost done now! Time for migration of the final endpoint where we handle DELETE requests.

index.js

app.delete("/:id", function (req, res) {
  const id = parseInt(req.params.id);
  const query = "DELETE FROM messages WHERE id = $1";
  const values = [id];

  db.query(query, values, function (err, result) {
    if (err) throw err;
    const deleted = [{ id: values[0] }];

    if (result.rowCount === 0)
      return res
        .status(404)
        .send({ msg: `No message with id ${values[0]} found!` });

    res.status(200).send(deleted);
  });
});
Detailed Explaination
  • As you might notice, the callback function of this route is somewhat similar to the PUT request. We get the id, create the query and add the id in the values array which will replace the placeholder of the query.

  • In the db.query function call's callback we handle the error.

  • If no error, we create a deleted array which holds an object with an id property that contain the id that we have requested to DELETE.

  • If there weren't any affected rows (result.rowCount), we return and send a response with a status code of 404 and an error message.

  • If, there were affected rows, we send the deleted array as a response with a status code of 200.


Testing the endpoints using cURL

If you have curl installed on your computer you can follow along in this section. Note that all Mac computers, with operating system version equal to or greater than OSX, comes with cURL pre-installed.

Obviously your Express server needs to be running before you try to make any requests to it. So if it's not already running, start it.

Terminal

npm run start

Executing a POST request

Next we'll make a POST request from a terminal. We'll send the request to http://127.0.0.1:3000 which is equal to localhost, at port 3000 which is the port that we use for our application. We pass a JSON request body with the message text. We also pass a request header that defines that the data that we're sending is JSON data.

Terminal

curl -X POST http://127.0.0.1:3000/ -d '{"text":"New message"}' -H "Content-Type: application/json"

When sending this request from your terminal, you should get a response back that looks like this:

Terminal Response

{"id":1,"text":"New message"}

Cool! So now we've successfully created a new message with a POST request!

Executing GET requests

Now let's try to make a GET request, fetching all the messages that our server has stored so far. We should be able to fetch the 1 message that we added with the POST request. And of course if you were to add more messages, these messages should also be fetched with this request.

So let's test our GET endpoint which will return all the messages stored in the database so far.

Terminal

curl -X GET http://127.0.0.1:3000/

Boom! Here's all of our messages (currently just one if you haven't sent multiple POST requests).

Terminal Response

[{"id":1,"text":"New message"}]

Next, let's try the GET endpoint which will return a specific message based on the passed request parameter id.

So, this time, let's pass a request parameter 1 when making the GET request.

Terminal

curl -X GET http://127.0.0.1:3000/1

Voila! We get the message with the requested id.

Terminal Response

[{"id":1,"text":"New message"}]

But what if we request an id that doesn't exist in our database? Let's see.

Terminal

curl -X GET http://127.0.0.1:3000/1337

Ah! We get back the error object that we previously defined - telling us that the message with id 1337 was not found. Perfect!

Terminal Response

{"msg":"No message with id 1337 found!"}

Executing a PUT request

Now let's try updating a message. Let's update message with id 1.

Terminal

curl -X PUT http://127.0.0.1:3000/1 -d '{"text":"Updated message"}' -H "Content-Type: application/json"

We get back our updated message as a response. Awesome!

Terminal Response

[{"id":1,"text":"Updated message"}]

Executing a DELETE request

Finally, let's try to delete a message. Let's delete message with id 1.

Terminal

curl -X DELETE http://127.0.0.1:3000/1

Works like a charm!

Terminal Response

[{"id":1}]

Testing the endpoints using Postman

So if you were unable to test your endpoints with cURL, or just didn't feel like doing so, the time has come to test our endpoints using Postman.

Let's start off by making sure that our Express server is running. As you already know, we run it by executing the following command from our project root.

Terminal

npm run start

Executing a POST request

So, open up Postman and from the top menu, select File > New Tab.

Then select POST from the dropdown menu and input http://127.0.0.1:3000/ as the request url. Note that this is the same as inputting http://localhost:3000/ so choose whatever you prefer.

Postman new POST request

Next click on Body just below the request url bar.

Select raw in the radio button selection and in the dropdown to the right of the radio buttons, select JSON. Then press the Send button.

Postman POST request raw JSON format

Now in the bottom section of Postman, you should see a response consisting of the message that we just created, as well as a Status of "200 OK".

Postman successful POST request

Awesome! We've just created our first message by sending our first POST request.

Executing GET requests

Now let's try to make a GET request, fetching all the messages that our database has stored so far. Since we've only made one POST request so far, we should be able to get that message. How ever if you've made more than one POST request to create more messages, you should be able to fetch them all from this GET request.

So let's test our GET endpoint which will return all the messages stored in the database so far. We can do this by simply just changing the request type to GET from the top left drop-down menu. Note that we can leave the request body data as is - it will not be sent with the GET request anyways. Press Send when you've applied that change to the request type.

Postman GET request

Now we should get some data back which we can find in the response body section of Postman.

Postman GET response body

Cool! So we were able to get all the messages that we had stored in our database (only one in my case, since I've only made 1 POST request so far). Now let's test our other GET endpoint which will return a specific message based on the request parameter passed with the request.

Simply add an id to the request URL. Let's get message with id 1. Then press Send again.

Postman GET request with request parameter

This time the response body should look like so (Yes I know it looks exactly the same as the previous response if you haven't made any extra POST requests):

Postman response body

Doesn't it just feel good when you get whatever you request? Love it.

Ok, so what happens if we try to request a message with an id that doesn't exist? Let's try it out. Let's use 1337 as an id.

Postman GET request with request parameter

Press Send and see what happens.

Postman response body error message

Wonderful! We're getting our error message that we defined earlier, with the statuscode of 404 which means that the item that we requested was not found on the server.

Executing a PUT request

Now let's try updating a message. Let's update message with id 1.

So first, change the request type to PUT and make sure that the request parameter is 1 in the request url.

Postman PUT request with request parameter

Then before sending this, let's modify the request body.

Postman PUT request body

Now press Send.

And the response body should now return our updated message.

Postman PUT response body

Executing a DELETE request

Finally, let's try to delete a message. Let's delete message with id 1. So change the request type to DELETE and make sure that you pass the id 1 as a request parameter.

Postman DELETE request with request parameter

We get the deleted message back as a response within the response body.

Postman DELETE response body

Wooha! No message with id 1 in our database, which means that our DELETE request worked!


Final words

You're now one step closer to being table to construct a fully functional REST api using Node.js, Express & PostgreSQL. In the next tutorial of these series, we will take the API that we just built to the next level and deploy it to a cloud service.

You can find the final source code for this tutorial here.

CODICULUM

©2020-present Christopher af Bjur. All Rights Reserved.