CRUD API with Fastify, Postgres, Docker

Francesco Ciulla - Jan 10 '22 - - Dev Community

Hi I am Francesco. You can find me on Twitter here https://twitter.com/FrancescoCiull4

Creating Content in Public
All this Content has been created during 2 livestreams from scratch

Here is the link if you wanna take a look on how I created this content (Even this article as it's part of the content itself!)

Part 1

IMAGE ALT TEXT HERE

Part 2

IMAGE ALT TEXT HERE

In this article, we will set some CRUD API using:

  • Node.js (JavaScript Runtime Engine)
  • Fastify (Fast and low overhead web framework, for Node.js )
  • Postgres (PostgreSQL) is a free open-source relational database, very popular and stable)
  • Docker (Platform to deploy applications using containers)

GitHub Repository: https://github.com/FrancescoXX/study-with-me-fastify-docker

NODE

image.png

Node is a back-end JavaScript runtime environment, which means briefly that can execute JavaScript code on a computer, for example, yours or the one where Node is installed. The good thing is that, by having Docker, you DON't actually need to install it, because we will use the Node image, and so we can also avoid versioning between my version of Node installed on my machine and yours

FASTIFY

image.png

Fastify is a web framework focused on performance. It is inspired by Hapi and Express and it's for sure one of the fastest web frameworks in town.

POSTGRES

image.png

Postgres (PostgreSQL) is a free open-source relational database, very popular and stable

DOCKER

image.png

Docker is a platform to build run and share application using the idea of containers. If you want a brief introduction, here is a short video

IMAGE ALT TEXT HERE

Step by Step

  1. Create a folder named fastify-postgres-docker and enter into it
mkdir fastify-postgres-docker && cd fastify-postgres-docker
Enter fullscreen mode Exit fullscreen mode
  1. Initialize node application using npm
npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install the dependencies
npm install fastify fastify-postgres pg
Enter fullscreen mode Exit fullscreen mode
  1. Create the app folder and enter into it
mkdir app && cd app
Enter fullscreen mode Exit fullscreen mode

From inside the src folder, create a server.js file and a route.js file

The folder structure should look like this

image.png

Let's write the server.js file

const fastify = require('fastify')({ logger: true });
fastify.register(require('fastify-postgres'), {
  connectionString: `postgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_SERVICE}:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}`,
});
fastify.register(require('./routes'));

// Run the server
const start = () => {
  fastify.listen(3000, '0.0.0.0', (err, address) => {
    if (err) {
      fastify.log.error(err);
      process.exit(1);
    }
  });
};
start();
Enter fullscreen mode Exit fullscreen mode

Fastify uses the idea of plugins, you can check more about this here

https://www.fastify.io/docs/master/Plugins/

Let's write the first part of the routes.js file

async function routes(fastify, options) {
  // Testing route
  fastify.get('/', async (request, reply) => {
    return { hello: 'world' };
  });
}

module.exports = routes;
Enter fullscreen mode Exit fullscreen mode

DOCKER

image.png

Now the Docker Part!

In the main folder, create 3 files:

  • Dockerfile
  • docker-compose.yml
  • .dockerignore (it starts with a dot)

the .dockerignore file:

node_modules
.gitignore
.env
Enter fullscreen mode Exit fullscreen mode

the Dockerfile:

FROM node:14

EXPOSE 3000

# Use latest version of npm
RUN npm install npm@latest -g

COPY package.json package-lock.json* ./

RUN npm install --no-optional && npm cache clean --force

# copy in our source code last, as it changes the most
WORKDIR /usr

COPY . .

CMD [ "node", "app/server.js"]
Enter fullscreen mode Exit fullscreen mode

The docker-compose.yml file:

version: '3.8'
services:
  fastify_backend:
    container_name: fastify_backend
    image: francescoxx/fastify_backend:0.0.1
    build:
      context: .
    ports:
      - '3000:3000'
    env_file: .env
    depends_on: 
      - postgres

  postgres:
    container_name: postgres
    hostname: postgres
    image: 'postgres:13'
    ports:
      - '5432:5432'
    restart: always
    env_file: .env
    volumes:
      - fastify_volume:/var/lib/postgres/data

volumes:
  fastify_volume: {}
Enter fullscreen mode Exit fullscreen mode

replace the image "francescoxx/fastify_backend:0.0.1" with an image name of your choice!

Before running our services, we need to create a .env file, to store our environment variables, and populate it with all the environment variables we need.

POSTGRES_USER=francesco
POSTGRES_PASSWORD=dbpassword
POSTGRES_DB=fastifydb
POSTGRES_SERVICE=postgres
POSTGRES_PORT=5432
Enter fullscreen mode Exit fullscreen mode

The End file should look something like this:

image.png

you can change them accordingly on your needings!

Let's start the postgres service:

docker-compose up -d postgres
Enter fullscreen mode Exit fullscreen mode

we should have a Postgres DB up and running!

let's check what is inside the DB:
From another Terminal, type

docker exec -it postgres psql -U francesco fastifydb
Enter fullscreen mode Exit fullscreen mode

and once we are inside the container
(you can verify this by checking the postgres=# terminal)

connect to the fastifydb database

\c fastifydb
Enter fullscreen mode Exit fullscreen mode

this means that a database named "fastifydb" has been created by postgres using the environment variable we have passed at the beginning

and then:

\dt
Enter fullscreen mode Exit fullscreen mode

and you should get the message:

"Did not find any relations."

image.png

This is because we have created the database, using the environment variable, but we haven't created any table or relationship yet

Type 'exit' to exit from this terminal

exit
Enter fullscreen mode Exit fullscreen mode

And you are again at your terminal

Time to build our image!

from the folder where the docker-compose.yml file is located, run

docker-compose build
Enter fullscreen mode Exit fullscreen mode

image.png

Now it's time to run our node application

docker-compose up -d fastify_backend
Enter fullscreen mode Exit fullscreen mode

WE can verify if both the containers are running, by using the 'docker ps -a' command

image.png

Let's add an endpoint to init the DB. (This could be done in other better ways!)

In the route.js file, let's add a simple endpoint that will create the users table:

// INIT TABLE. Launch just once to create the table
  fastify.get('/initDB', (req, reply) => {
    fastify.pg.connect(onConnect);
    function onConnect(err, client, release) {
      if (err) return reply.send(err);
      client.query(
        'CREATE TABLE IF NOT EXISTS "users" ("id" SERIAL PRIMARY KEY,"name" varchar(30),"description" varchar(30),"tweets" integer);',
        function onResult(err, result) {
          release();
          reply.send(err || result);
        }
      );
    }
  });
Enter fullscreen mode Exit fullscreen mode

ADDING API ENDPOINTS

Let's add other 5 endpoints:

Endpoint to GET all the Users:

  //GET AL USERS
  fastify.route({
    method: 'GET',
    url: '/users',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query('SELECT * from users', function onResult(err, result) {
          release();
          reply.send(err || result.rows);
        });
      }
    },
  });

Enter fullscreen mode Exit fullscreen mode

Endpoint to get one User

  //GET ONE USER if exists
  fastify.route({
    method: 'GET',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query(`SELECT * from users where id=${request.params.id}`, function onResult(err, result) {
          release();
          reply.send(err || result.rows[0]);
        });
      }
    },
  });
Enter fullscreen mode Exit fullscreen mode

Endpoint to create one user

  //UPDATE ONE USER fields
  fastify.route({
    method: 'PUT',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      async function onConnect(err, client, release) {
        if (err) return reply.send(err);
        const oldUserReq = await client.query(`SELECT * from users where id=${request.params.id}`);
        const oldUser = oldUserReq.rows[0];
        client.query(
          `UPDATE users SET(name,description,tweets) = ('${request.body.name}', '${request.body.description || oldUser.description}', ${
            request.body.tweets || oldUser.tweets
          })
      WHERE id=${request.params.id}`,
          function onResult(err, result) {
            release();
            reply.send(err || `Updated: ${request.params.id}`);
          }
        );
      }
    },
  });
Enter fullscreen mode Exit fullscreen mode

Endpoint to Delete one user:

  //DELETE ONE USER if exists
  fastify.route({
    method: 'DELETE',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query(`DELETE FROM users WHERE id=${request.params.id}`, function onResult(err, result) {
          release();
          reply.send(err || `Deleted: ${request.params.id}`);
        });
      }
    },
  });
Enter fullscreen mode Exit fullscreen mode

The final routes.js file should look like this:

async function routes(fastify, options) {
  // Testing route
  fastify.get('/', async (request, reply) => {
    return { hello: 'world' };
  });

  // INIT TABLE. Launch just once to create the table
  fastify.get('/initDB', (req, reply) => {
    fastify.pg.connect(onConnect);
    function onConnect(err, client, release) {
      if (err) return reply.send(err);
      client.query(
        'CREATE TABLE IF NOT EXISTS "users" ("id" SERIAL PRIMARY KEY,"name" varchar(30),"description" varchar(30),"tweets" integer);',
        function onResult(err, result) {
          release();
          reply.send(err || result);
        }
      );
    }
  });

  //GET AL USERS
  fastify.route({
    method: 'GET',
    url: '/users',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query('SELECT * from users', function onResult(err, result) {
          release();
          reply.send(err || result.rows);
        });
      }
    },
  });

  //GET ONE USER if exists
  fastify.route({
    method: 'GET',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query(`SELECT * from users where id=${request.params.id}`, function onResult(err, result) {
          release();
          reply.send(err || result.rows[0]);
        });
      }
    },
  });

  //Create users
  fastify.route({
    method: 'POST',
    url: '/users',
    handler: function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        const newUser = request.body;
        client.query(
          `INSERT into users (name,description,tweets) VALUES('${newUser.name}','${newUser.description}',${newUser.tweets})`,
          function onResult(err, result) {
            release();
            reply.send(err || result);
          }
        );
      }
    },
  });

  //UPDATE ONE USER fields
  fastify.route({
    method: 'PUT',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      async function onConnect(err, client, release) {
        if (err) return reply.send(err);
        const oldUserReq = await client.query(`SELECT * from users where id=${request.params.id}`);
        const oldUser = oldUserReq.rows[0];
        client.query(
          `UPDATE users SET(name,description,tweets) = ('${request.body.name}', '${request.body.description || oldUser.description}', ${
            request.body.tweets || oldUser.tweets
          })
      WHERE id=${request.params.id}`,
          function onResult(err, result) {
            release();
            reply.send(err || `Updated: ${request.params.id}`);
          }
        );
      }
    },
  });

  //DELETE ONE USER if exists
  fastify.route({
    method: 'DELETE',
    url: '/users/:id',
    handler: async function (request, reply) {
      fastify.pg.connect(onConnect);
      function onConnect(err, client, release) {
        if (err) return reply.send(err);
        client.query(`DELETE FROM users WHERE id=${request.params.id}`, function onResult(err, result) {
          release();
          reply.send(err || `Deleted: ${request.params.id}`);
        });
      }
    },
  });
}

module.exports = routes;
Enter fullscreen mode Exit fullscreen mode

Now let's test these APIs!


POSTMAN

Important! you need to specify localhost and not 127.0.0.1 in the first part of the url, otherwise it doesn't work!

image.png

We will use Postman, but you can use a whenever tool you want

First of all, we need to create the user table. We will trigger it by hitting with a GET this url:

GET http://localhost:3000/initDB
Enter fullscreen mode Exit fullscreen mode

image.png

If we get this answer, it means that our 'users' table has been created!

Now let's check all the users with another GET :

GET http://localhost:3000/users
Enter fullscreen mode Exit fullscreen mode

image.png

if we get the empty array answer, [], it means that we actually have the users table, in our DB, but the are no users. This is perfectly fine!

Let's create some users. We will do this by making a POST request at the same endpoint, adding the values in a json

Example:

{
    "name":"Adrian",
    "description":"Kangaroo Fighter",
    "tweets":12000
}
Enter fullscreen mode Exit fullscreen mode

image.png

Please notice that we don't need to add an 'id' , as it is automatically incremented at each new user

LEt's add another one

image.png

and another one

image.png

Now let's check again all the users:

image.png

And we see that this time we have 3 users!

We can get one single users by adding the id of the user at the end of the previous url path. For example

GET http://localhost:3000/users/2
Enter fullscreen mode Exit fullscreen mode

To get the user with the id = 2

image.png

To delete an user, you can make a DELETE request at the same endpoint you use to get one user:

DELETE http://localhost:3000/users/2
Enter fullscreen mode Exit fullscreen mode

image.png

Finally, to Update the user, you make a PUT request, passing the new values inside a json, like this

{
    "name":"Adrian2",
    "description":"SuperKANGAROO"
}
Enter fullscreen mode Exit fullscreen mode

and you also need to pass the id of the user you wanna update in the url request, like this

PUT http://localhost:3000/users/3
Enter fullscreen mode Exit fullscreen mode

image.png

To check if the user has been really updated, you can make another GET Request:

image.png

As you can see, the name and the description of the user has changed, but not the tweets.


Conclusion

If you have tried to follow this article, I would like to know if you have encountered any problem. Thanks!

GitHub Repository:
https://github.com/FrancescoXX/study-with-me-fastify-docker

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .