DEV Community

Cover image for Fastify and PostgreSQL REST API
Kenwanjohi
Kenwanjohi

Posted on • Updated on • Originally published at wanjohi.vercel.app

Fastify and PostgreSQL REST API

Enter the 'speed force'

From the documentation, fastify is a fast and low overhead web framework for Node.js.

So, I decided to explore some of the awesome features that fastify offers including but not limited to, speed, extensibility via plugins and decorators, schema validation, and serialization and logging. I dived into their documentation, which is excellent by the way, with the help of some GitHub repositories and decided to build some REST API endpoints powered by fastify and a PostgreSQL database.

You can check out the source code or follow along in this post.

Getting Started

Setting up the project

Navigate to the root folder of your project and run npm init to initialize your project. Create an src folder for your project source code and create an index.js file as the entry point.

Installing dependencies

Installing nodemon

nodemon is a dev dependency that'll monitor your file changes and restart your server automatically.

You can install nodemon locally with npm:

npm install nodemon --save-dev
Enter fullscreen mode Exit fullscreen mode

Add this npm script to the scripts in the package.json file

"start": "nodemon src/index.js"
Enter fullscreen mode Exit fullscreen mode

Installing Fastify

Install with npm:

npm i fastify --save
Enter fullscreen mode Exit fullscreen mode

Hello World: Starting and running your server

In the index.js file add this block of code:

const fastify = require('fastify')({logger: true}) 
fastify.get('/', async (request, reply) => { 
     reply.send({ hello: 'world' }) 
  }) 
async function start()  { 
    try{ 
        await fastify.listen(3000) 
    } catch(err) { 
        fastify.log.error(err) 
        process.exit(1) 
    } 
} 
start()
Enter fullscreen mode Exit fullscreen mode

On the first line, we create a fastify instance and enable logging, fastify uses pino as its logger. We then define a GET route method, specify a homepage endpoint '/' and pass in the route handler function which responds with the object {hello: 'world'} when we make a get request to the homepage.

We instantiate our fastify server instance (wrapped in our start function)  and listen for requests on port 3000. To start the server, run npm start on your terminal in the root folder. You Server should now be running and the following will be logged in the terminal:

{"level":30,"time":1618477680757,"pid":5800,"hostname":"x","msg":"Server listening at http://127.0.0.1:3000"}
Enter fullscreen mode Exit fullscreen mode

When you visit the homepage you should see the response:

curl http://localhost:3000/ 
{"hello":"world"}
Enter fullscreen mode Exit fullscreen mode

Great we have our server!

Plugins

We can extend fastify's functionality with plugins.
From the documentation:

A plugin can be a set of routes, a server decorator, or whatever.

We can refactor our route into a plugin and put it in a separate file i.e routes.js, then require it in our root file and use the register API to add the route or other plugins.

Create a routes.js file and add this code:

async function routes(fastify, options) { 
    fastify.get('/', async (request, reply) => { 
        reply.send({ hello: 'world' }) 
    }) 
} 
module.exports= routes
Enter fullscreen mode Exit fullscreen mode

We then require our module in index.js and register it.

const fastify = require('fastify')({logger: true})
const route  = require('./routes')
fastify.register(route)
async function start()  {
   ...
}
start()
Enter fullscreen mode Exit fullscreen mode

A request on the homepage should still work. Great, we have our first plugin.

Creating our database

To create a database we first need to connect to psql, an interactive terminal for working with Postgres.

To connect to psql run the command in the terminal:

psql -h localhost -U postgres
Enter fullscreen mode Exit fullscreen mode

Enter your password in the prompt to connect to psql.

The CREATE DATABASE databaseName statement creates a database:

CREATE DATABASE todos;
Enter fullscreen mode Exit fullscreen mode

To connect to the created database run the command:

\c todos
Enter fullscreen mode Exit fullscreen mode

To create our table run the statement

CREATE TABLE todos ( 
    id UUID PRIMARY KEY, 
    name VARCHAR(255) NOT NULL, 
    "createdAt" TIMESTAMP NOT NULL, 
    important BOOLEAN NOT NULL, 
    "dueDate" TIMESTAMP, 
    done BOOLEAN NOT NULL 
);
Enter fullscreen mode Exit fullscreen mode

Connecting our  database

To interface with postgreSQL database we need node-postgres or the pg driver.

To install node-postgres:

npm install pg
Enter fullscreen mode Exit fullscreen mode

Database connection plugin

Let's create a plugin to connect to our database. Create a db.js file and add the following code:

const fastifyPlugin = require('fastify-plugin') 
const { Client } = require('pg') 
require('dotenv').config() 
const client = new Client({ 
    user: 'postgres', 
    password:process.env.PASSWORD, 
    host: 'localhost', 
    port: 5432, 
    database: process.env.DATABASE 
}) 
async function dbconnector(fastify, options) { 
    try { 
        await client.connect() 
        console.log("db connected succesfully") 
        fastify.decorate('db', {client}) 
    } catch(err) { 
        console.error(err) 
    } 
} 
module.exports= fastifyPlugin(dbconnector)
Enter fullscreen mode Exit fullscreen mode

Let's skip the fastifyPlugin part first.

We require Client module from node-postgres and create a client instance, passing in the object with the various fields.

Make sure to create a .env file and add:

PASSWORD='yourpassword'
Enter fullscreen mode Exit fullscreen mode

Install and require the dotenv module to load the environment variables

npm i dotenv
Enter fullscreen mode Exit fullscreen mode

We then create our dbconnector plugin and inside the try block, we connect to our postgres database.

Inside the block you can also see:

fastify.decorate('db', {client})
Enter fullscreen mode Exit fullscreen mode

What is the decorate function?

In fastify, to add functionality to the fastify instance, you use decorators. We use the decorate API, pass the property name 'db' as the first argument and the value of our client instance ({client}) as the second argument. The value could also be a function or a string.
We export the plugin wrapped in a fastifyPlugin module.

Require the module in the index.js file and register it.

const dbconnector = require('./db')
fastify.register(dbconnector)
fastify.register(route)
async function start()  {
  ...
}
start()
Enter fullscreen mode Exit fullscreen mode

We can now access our client instance in other parts of the application for instance in our routes to query data using  fastify.db.client.

Let's take a step back to the fastifyPlugin module. Why wrap our plugin with fastifyPlugin? When we register a plugin, we create a fastify context (encapsulation), which means access to the data outside our registered plugin is restricted. In this case, we can't access our database client instance using fastify.db.client anywhere in our application.

To share context, we wrap our plugin in a fastifyPlugin module. We can now access our database client instance anywhere in our application.

Serialization

Lets refactor our homepage route to return information from our database:

async function routes(fastify, options) {  
    //Access our client instance value from our decorator
    const client = fastify.db.client
    fastify.get('/', {schema: allTodos}, async function (request, reply) { 
            try { 
                const {rows} = await client.query('SELECT * FROM todos') 
                console.log(rows) 
                reply.send(rows) 
            } catch(err) { 
                throw new Error(err) 
            } 
        })
}  
module.exports= routes
Enter fullscreen mode Exit fullscreen mode

We First access our database client instance and assign it to a client variable.
Inside our routes we query all columns from our database using the shorthand * and send the returned todos using reply.send(rows) - you could also use return rows.
Make sure you add some todos in your database first in the psql terminal i.e:

INSERT INTO todos (id, name, "createdAt", important, "dueDate",  done) 
VALUES ('54e694ce-6003-46e6-9cfd-b1cf0fe9d332', 'learn fastify', '2021-04-20T12:39:25Z', true, '2021-04-22T15:22:20Z', false); 
INSERT INTO todos (id, name, "createdAt", important, "dueDate",  done)  
VALUES ('d595655e-9691-4d1a-9a6b-9fbba046ae36', 'learn REST APIs', '2021-04-18T07:24:07Z',true, null, false);
Enter fullscreen mode Exit fullscreen mode

If an error occurs, trying to query our database, we throw the error.

When you look closer at our get route method, you can see have an object as our second argument with a schema key and allTodos as the value.

Fastify uses  fast-json-stringify to serialize your response body when a schema is provided in the route options.

To add the schema create a schemas.js file and add the allTodos schema:

const allTodos = {
    response: {
        200: {
            type: 'array',
            items: {
                type: 'object',
                required: ['id', 'name', 'createdAt', 'important', 'dueDate', 'done'],
                properties: {
                    id: {type: 'string',  format: 'uuid'},                                                              
                    name: {type: 'string'},                                           
                    createdAt:{type: 'string',format: "date-time"},                  
                    important: {type: 'boolean'},
                    dueDate: {type: 'string',format: "date-time"},
                    done: {type: 'boolean'},
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Fastify recommends using JSON Schema to serialize your outputs, you can read how to write JSON schema here.

We're specifying the response, the response status code, and the entity which is an array type. The items specify each entry in the array as an object with the required keys and the properties with the various fields and types.

Remember to require the module in the routes.js file.

Validation

In the routes.js file, let's add a POST method route inside our route plugin to add todos to our database.

fastify.post('/', {schema: addTodo}, async function(request, reply) {
            const {name, important, dueDate} = request.body
            const id = uuidv4()
            const done = false
            createdAt = new Date().toISOString()
            const query = {
                    text: `INSERT INTO todos (id, name, "createdAt", important, "dueDate", done)
                                    VALUES($1, $2, $3, $4, $5, $6 ) RETURNING *`,
                    values: [id, name, createdAt, important, dueDate, done],
                    }
            try {
                    const {rows} = await client.query(query)
                    console.log(rows[0])
                    reply.code(201)
                    return {created: true}
            } catch (err) {
                    throw new Error(err)
            }

    })
Enter fullscreen mode Exit fullscreen mode

We allow the client to send a JSON object in the body with name of the todo, important, and dueDate properties.

We then generate a unique id, assign false to done and a timestamp assigned to createdAt.

To generate the unique id install uuid:

npm install uuid
Enter fullscreen mode Exit fullscreen mode

Require the module in the routes.js:

const { v4: uuidv4 } = require('uuid');
Enter fullscreen mode Exit fullscreen mode

We then construct a query object with a text property with the SQL statement to insert the todos in the database and the values property containing the values to be inserted into the respective columns.

After a successful insert we send a 201 Created status code back to the client.
In the schemas.js file, let's add the validation schema for our todos:

const addTodo = {
    body: {
        type: 'object',
        required: ['name'],
        properties: {
            name: {type: 'string',},
            dueDate: {type: 'string', format: 'date-time', nullable: true, default: null},
            important: {type: 'boolean', default: false},
        }
    },
    response: {
        201: {
            type: 'object',
            properties: {
                created: {type: 'boolean'}
            }
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Fastify uses Ajv to validate requests.
We expect the client to always send the name of the todo by adding it in the required property array.

The dueDate property can be omitted by the client whereby it will be null by default. This is made possible by setting the nullable property to true which allows a data instance to be JSON null. When provided it has to be of the format 'date-time'.

The client can optionally indicate whether a todo is important or it falls back to the default.

If the above conditions are not satisfied, fastify will automatically send an error object with the error message.

For instance, if you omit a name, you should see an error like

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'name'"
}
Enter fullscreen mode Exit fullscreen mode

Great! Our validation is working

Adding other REST endpoints

Update todo
Let's allow users to set their todo as done or importance of the todo or change dueDate. To do that let's add a PATCH method route to our routes plugin.

fastify.patch('/:id',{schema: updateTodo}, async function (request, reply) {
        const id = request.params.id
        const {important, dueDate, done} = request.body
        const query = {
                text:  `UPDATE todos SET 
                                important = COALESCE($1, important), 
                                "dueDate" = COALESCE($2, "dueDate"), 
                                done = COALESCE($3, done) 
                                WHERE id = $4 RETURNING *`,
                values : [important, dueDate, done, id]
        }
        try {
                const {rows} = await client.query(query)
                console.log(rows[0])
                reply.code(204)
        } catch (err) {
                throw new Error(err)
        }
})
Enter fullscreen mode Exit fullscreen mode

We're extracting the id of the todo we want to update from the parameter and the values from the request body.

We then create our query statement, updating the columns provided optionally using the COALESCE function. That is, if the clients omit some properties in the JSON body, we update only the provided properties and leave the rest as they are in the todo row.

We then respond with a 204 No Content.

Lets add a validation schema for our route:

const updateTodo = {
    body: {
        type: 'object',
        properties: {
            dueDate: {type: 'string', format: 'date-time'},
            important: {type: 'boolean'},
            done: {type: 'boolean'}
        }
    },
    params: {
        type: 'object',
        properties: {
          id: { type: 'string', format: 'uuid' }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

params validates the params object.

Delete todo

To delete a todo, we just need the id sent in the URL parameter.
Add a DELETE method route:

fastify.delete('/:id', {schema: deleteTodo}, async function(request, reply) {
            console.log(request.params)
            try {
                    const {rows} = await client.query('DELETE FROM todos
                    WHERE id = $1 RETURNING *', [request.params.id])
                    console.log(rows[0])
                    reply.code(204)
            } catch(err) {
                    throw new Error(err)
            }
    })
Enter fullscreen mode Exit fullscreen mode

Lets add a validation schema for our DELETE route:

const deleteTodo = {
    params: {
        type: 'object',
        properties: {
            id: {type: 'string', format: 'uuid'}
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

Give fastify a try and "take your HTTP server to ludicrous speed" ~ Matteo Collina.

You can check out the project source code here

References:

Fastify examples; GitHub repos:

Top comments (2)

Collapse
 
cforsyth68 profile image
Charles Forsyth

Edit suggestion: The SQL commands will not work unless ending in semi-colon. e.g. CREATE DATABASE todo; instead of just CREATE DATABASE todo

Collapse
 
kenwanjohi profile image
Kenwanjohi

Yup, thanks for that