How to Create Your First REST API with Fastify

Share this article

A lion from Memphis

Fastify is a framework designed for back-end web development. It offers a more lightweight alternative to heavier Node.js API frameworks, such as Hapi and Express. From July 2020, Fastify has released its third version of the framework.

This third version comes with improved validation abilities to verify incoming and outgoing requests, as request parameters. Moreover, the third version of the framework consolidates its throughput claims of being the fastest Node.js framework compared with Koa, Resitfy, Hapi, and Express. More information can be found on the benchmarks page.

Fastify has gained a lot of popularity due to its lightweight design. However, a lot of attention goes to its plugin ecosystem. Fastify has adopted the idea that everything is a plugin, whereas with JavaScript, everything is an object. This allows you to quickly encapsulate functionality for your project as a plugin and distribute it so other projects can use your code.

Let’s get started with this tutorial. You’ll learn the following aspects of Fastify:

  • How to set up your first Fastify API
  • How to define Fastify API routes
  • How to add schema validation to requests
  • How to load and use Fastify plugins
  • How to define Fastify hooks

Requirements and Installation

To follow this tutorial, you’ll need:

  1. the latest Node.js version
  2. a tool for sending requests, such as cURL or Postman

Next, make sure to create an empty Node.js project. If you don’t have one yet, you can use the following command to set up your project:

npm init -y

Lastly, we want to add this Fastify dependency to our project:

npm i fastify --save

All good? Let’s create our basic API setup in the next step.

Step 1: Basic API Setup

First, let’s create our basic API setup. To get started, we need to create a new file called index.js within our project root:

touch index.js

Next, let’s add the basic server setup. Copy the code below:

// Require the framework and instantiate it
const app = require('fastify')({
    logger: true
})

// Declare a route
app.get('/', function (req, reply) {
    reply.send({ hello: 'world' })
})

// Run the server!
app.listen(3000, (err, address) => {
    if (err) {
        app.log.error(err)
        process.exit(1)
    }
    app.log.info(`server listening on ${address}`)
})

There are a couple of things happening here. We first load the Fastify application object and enable logging. Next, we declare a root route that replies with a JSON response. The last part of the code snippet shows that we’re listening on port 3000 for the application to receive requests.

Let’s validate if your basic server setup works. First, we need to start the server by running the index.js file:

node index.js

Thereafter, navigate to http://localhost:3000 in your browser. You should see the following response:

{
    "hello": "world"
}

Success? Let’s go to Step 2 to define different CRUD routes.

Step 2: Define CRUD Routes

An API is useless with only GET routes. Let’s define more routes for handling blogs. Therefore, let’s create the following routes:

  • GET all blogs at /api/blogs
  • GET one blog at /api/blogs/:id
  • POST add blog at /api/blogs
  • PUT update blog at /api/blogs/:id
  • DELETE delete blog at /api/blogs/:id

The first thing to do is creating a blog controller.

Step 2.1: Create Blogs Controller

To keep our code clean, let’s define a controller folder in the project root. Here, we create a file called blogs.js.

This file contains some demo data to avoid complicating this tutorial with a database integration. Therefore, we use an array containing blog objects which each contain an ID and title field.

Moreover, we define the different handlers for all the above routes in this file. A handler always accepts a req (request) and reply parameter. The request parameter is useful to access request parameters or request body data.

Add the following code to your /controller/blogs.js file:

// Demo data
let blogs = [
    {
        id: 1,
        title: 'This is an experiment'
    },
    {
        id: 2,
        title: 'Fastify is pretty cool'
    },
    {
        id: 3,
        title: 'Just another blog, yea!'
    }
]

// Handlers
const getAllBlogs = async (req, reply) => {
    return blogs
}

const getBlog = async (req, reply) => {
    const id = Number(req.params.id) // blog ID
    const blog = blogs.find(blog => blog.id === id)
    return blog
}

const addBlog = async (req, reply) => {
    const id = blogs.length + 1 // generate new ID
    const newBlog = {
        id,
        title: req.body.title
    }

    blogs.push(newBlog)
    return newBlog
}

const updateBlog = async (req, reply) => {
    const id = Number(req.params.id)
    blogs = blogs.map(blog => {
        if (blog.id === id) {
            return {
                id,
                title: req.body.title
            }
        }
    })

    return {
        id,
        title: req.body.title
    }
}

const deleteBlog = async (req, reply) => {
    const id = Number(req.params.id)

    blogs = blogs.filter(blog => blog.id !== id)
    return { msg: `Blog with ID ${id} is deleted` }
}

module.exports = {
    getAllBlogs,
    getBlog,
    addBlog,
    updateBlog,
    deleteBlog
}

Note how we can access the request parameter for routes such as /api/blogs/:id via req.params.id. For POST and PUT routes, we can access the body of the request via req.body.

In step 2.2, we’ll connect the route handlers to the route objects.

Step 2.2: Define Blog Routes and Couple Blogs Controller

Again, to keep our code clean, let’s define a routes folder in the project root. Here, we create a file called blogs.js. This file holds the routes object for our blog routes:

mkdir routes
cd routes
touch blogs.js

Luckily, Fastify allows us to define an array containing route objects. Here, we can couple the handlers we’ve defined previously to the different routes. Don’t forget to require the blogs controller. Let’s take a look:

const blogController = require('../controller/blogs');

const routes = [{
        method: 'GET',
        url: '/api/blogs',
        handler: blogController.getAllBlogs
    },
    {
        method: 'GET',
        url: '/api/blogs/:id',
        handler: blogController.getBlog
    },
    {
        method: 'POST',
        url: '/api/blogs',
        handler: blogController.addBlog
    },
    {
        method: 'PUT',
        url: '/api/blogs/:id',
        handler: blogController.updateBlog
    },
    {
        method: 'DELETE',
        url: '/api/blogs/:id',
        handler: blogController.deleteBlog
    }
]
module.exports = routes

Now we’ve defined all routes. However, Fastify doesn’t know about these routes. The next step shows how you can register routes with your Fastify application object.

Step 2.3: Register Fastify Routes

In this step, we’ll register Fastify routes to the app object. First, we load all the blog routes. Next, we loop over all the routes to register them one by one:

// Require the framework and instantiate it
const app = require('fastify')({
    logger: true
})

// Declare a route
app.get('/', function (req, reply) {
    reply.send({ hello: 'world' })
})

// Register routes to handle blog posts
const blogRoutes = require('./routes/blogs')
blogRoutes.forEach((route, index) => {
    app.route(route)
})

// Run the server!
app.listen(3000, (err, address) => {
    if (err) {
        app.log.error(err)
        process.exit(1)
    }
    app.log.info(`server listening on ${address}`)
})

Done? It’s time to validate if the blog routes work. Spin up the server using node index.js and visit http://localhost:3000/blogs/1 to get the first blog from the demo data. You should see the following result:

{
    "id": 1,
    "title": "This is an experiment"
}

All good? Let’s learn in Step 3 how to add schema validation to requests and responses.

Step 3: Adding Schema Validation

This step teaches you how to add schema validation to your project. We can make use of the schema key in our routes definition to pass a validation schema to a particular route.

Let’s start with defining a schema for the route /api/blogs/:id to validate the request parameter and response. Requirements?

  1. :id parameter must be of type string
  2. response must contain an object with two properties id (integer) and title (string)

Add the following validation object to your routes/blogs.js file:

const getBlogValidation = {
        params: {
            id: { type: 'string' }
        },
        response: {
            200: {
                type: 'object',
                properties: {
                    id: { type: 'integer' },
                    title: { type: 'string' }
                }
            }
        }
}

To connect the validation object to our route, we have to define the schema key. Look for the /api/blogs/:id route in the routes array and change the object accordingly:

...
{
    method: 'GET',
    url: '/api/blogs/:id',
    schema: getBlogValidation, // add validation
    handler: blogController.getBlog
},
...

Let’s do the same for adding a blog POST /api/blogs. Here, we want to verify if the req.body object contains a title parameter. Let’s take a look:

const addBlogValidation = {
    body: {
        type: 'object',
        required: [
            'title'
        ],
        properties: {
            title: { type: 'string' }
        }
    },
    response: {
        200: {
            type: 'object',
            properties: {
                id: { type: 'integer' },
                title: { type: 'string' }
            }
        }
    }
}

Next, we have to connect the validation object again to the correct route:

...
{
    method: 'POST',
    url: '/api/blogs',
    schema: addBlogValidation, // add validation
    handler: blogController.addBlog
},
...

To verify our validation, let’s retrieve the blog with ID 3. Open your browser at http://localhost:3000/api/blogs/3. You should see the following response:

{
    "id": 3,
    "title": "Just another blog, yea!"
}

Now, let’s make a mistake and change the params validation for the id field from sting to object like so:

const getBlogValidation = {
        params: {
            id: { type: 'object' } // Try changing to object to see error
        },
        response: {
            200: {
                type: 'object',
                properties: {
                    id: { type: 'integer' },
                    title: { type: 'string' }
                }
            }
        }
}

When requesting the same resource from your API, you’ll receive the following error message.

{
    "statusCode": 400,
    "error": "Bad Request",
    "message": "params.id should be object"
}

Do you see the error? Good! Let’s revert the change to string to avoid future errors and move to the next step.

Step 4: Load Fastify Plugins

Here, let’s make use of Fastify’s rich plugin ecosystem. You can find plugins that help you with various tasks, such as database integrations or authorization setups. Why would you spend time writing authorization from scratch while you can make use of Fastify plugins? Often, you want to look for packages outside of Fastify’s ecosystem that help you with certain problems or tasks. However, by providing a rich plugin ecosystem, Fastify becomes a one-stop solution that definitely improves the developer experience!

A quick note about plugins: You can create your own plugins to encapsulate functionality. Moreover, you can load those plugins to your Fastify application object. By default, Fastify will first load plugins from the Fastify ecosystem. Afterward, custom plugins are loaded.

Ok, let’s get practical! I would like to use the fastify-env plugin, which helps you with loading environment variables and setting defaults for each variable. Therefore, let’s add this dependency to our project:

npm install --save fastify-env

Next, we can load the dependency after loading the Fastify application object in the index.js file. Your index.js file looks like this:

// Require the framework and instantiate it
const app = require('fastify')({
    logger: true
})

// Use Fastify Env plugin: https://github.com/fastify/fastify-env
const fastifyEnv = require('fastify-env') // load plugin

const options = {
    confKey: 'config', // optional, default: 'config'
    schema: {
        type: 'object',
        required: ['PORT'],
        properties: {
            PORT: {
                type: 'string',
                default: 1000
            }
        }
    }
}

app
    .register(fastifyEnv, options)
    .ready((err) => {
        if (err) console.error(err)

        console.log(app.config)
        // output: { PORT: 1000 }
    })

// Declare a route
app.get('/', function (req, reply) {
    reply.send({ hello: 'world' })
})

// Register routes to handle blog posts
const blogRoutes = require('./routes/blogs')
blogRoutes.forEach((route, index) => {
    app.route(route)
})

// Run the server!
app.listen(app.config.PORT, (err, address) => {
    if (err) {
        app.log.error(err)
        process.exit(1)
    }
    app.log.info(`server listening on ${address}`)
})

Note that we have to define an options object that tells the fastify-env plugin what env variables to look for and which defaults to set. Here, I want to load a PORT variable with a default value of 1000.

By default, the fastify-env plugin will make all environment variables available via the Fastify app object like so: app.config.PORT. Why? The fastify-env plugin attaches the loaded configurations to the confKey, which by default is set to config. However, if you wish, you can change this to another key.

Start the project with node index.js and monitor the output. You should see the PORT variable being printed in your terminal.

Other interesting plugins to use?

  1. fastify-auth: run multiple auth functions in Fastify
  2. fastify-bearer-auth: bearer auth plugin for Fastify
  3. fastify-caching: general server-side cache and etag support
  4. fastify-cors: enables the use of CORS in a Fastify application

Step 5: Define Hooks

Lastly, let’s define some hooks. From the Fastify hooks documentation, we can read the following. “Hooks are registered with the fastify.addHook method and allow you to listen to specific events in the application or request/response lifecycle. You have to register a hook before the event is triggered, otherwise the event is lost.”

Make sure to define hooks before you define any routes:

// hooks
app.addHook('onRoute', (routeOptions) => {
    console.log(`Registered route: ${routeOptions.url}`)
})

// Declare a route
app.get('/', function (req, reply) {
    reply.send({ hello: 'world' })
})

As you can see, the addHook function first accepts the hook you want to listen for. In our example, we want to listen for new routes being registered with the application. Next, the callback function accepts a routeOptions argument which contains a lot of information, such as the route URL or route method.

Specific details for the onRoute hook can be found in the documentation.

Let’s start the API with node index.js to see which routes have been registered. Your terminal output should look like this:

Registered route: /
Registered route: /api/blogs
Registered route: /api/blogs/:id
Registered route: /api/blogs
Registered route: /api/blogs/:id
Registered route: /api/blogs/:id

Got the same output? Success! At the same time, this was the end of the Fastify tutorial. Let’s wrap up this project with a short conclusion.

Wrapping Up

Fastify is a great, light-weight project that allows you to make use of its rich plugin ecosystem. Instead of creating functionality from scratch, you can make use of existing plugins. In other words, Fastify acts as a one-stop shop for developers, definitely improving the developer experience.

Personally, I like the Fastify hooks functionality as you can listen for various lifecycle events within your application.

To learn more about Fastify, check out the following documentation pages:

You can also check out the repo for this introduction on GitHub.

Frequently Asked Questions (FAQs) about Creating a REST API with Fastify

What are the prerequisites for creating a REST API with Fastify?

Before you start creating a REST API with Fastify, you need to have a basic understanding of JavaScript and Node.js. You should also have Node.js and npm installed on your computer. Node.js is a JavaScript runtime that allows you to run JavaScript on your server, while npm is a package manager for Node.js that allows you to install and manage packages that your project depends on.

How do I install Fastify?

You can install Fastify using npm, the Node.js package manager. Open your terminal and run the following command: npm install fastify. This will install Fastify in your current project directory. Make sure you are in the correct directory before running the command.

How do I create a server with Fastify?

To create a server with Fastify, you first need to require the Fastify module in your application. Then, you can create a new Fastify instance. Here is a basic example:

const fastify = require('fastify')({ logger: true })

fastify.listen(3000, (err, address) => {
if (err) throw err
fastify.log.info(`server listening on ${address}`)
})

How do I define routes in Fastify?

In Fastify, you define routes using the route method. This method takes an object as an argument, which defines the route’s properties. Here is an example:

fastify.route({
method: 'GET',
url: '/',
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})

How do I handle errors in Fastify?

Fastify provides a built-in error handling mechanism. You can use the reply object’s code and send methods to send an error response. Here is an example:

fastify.get('/error', function (request, reply) {
reply.code(500).send(new Error('Internal server error'))
})

How do I use plugins in Fastify?

Fastify has a powerful plugin architecture that allows you to extend its functionality. You can use the register method to register a plugin. Here is an example:

fastify.register(require('fastify-cors'), {
origin: true
})

How do I validate requests in Fastify?

Fastify provides a built-in validation and serialization library called fastify-schema. You can use this library to validate incoming requests and serialize outgoing responses. Here is an example:

fastify.route({
method: 'POST',
url: '/validate',
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string' },
email: { type: 'string' }
}
}
},
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})

How do I test my Fastify application?

Fastify provides a inject method that allows you to simulate HTTP requests and responses for testing purposes. Here is an example:

fastify.inject({
method: 'GET',
url: '/',
}, (err, response) => {
console.log(response.payload)
})

How do I deploy my Fastify application?

You can deploy your Fastify application like any other Node.js application. You can use platforms like Heroku, AWS, or Google Cloud Platform. Make sure to set the NODE_ENV environment variable to production when deploying your application.

How do I handle asynchronous code in Fastify?

Fastify supports both callbacks and promises for handling asynchronous code. You can use the async/await syntax in your route handlers to write asynchronous code in a more readable way. Here is an example:

fastify.get('/async', async function (request, reply) {
const result = await someAsyncOperation()
return result
})

Michiel MuldersMichiel Mulders
View Author

Fullstack Blockchain Developer at TheLedger.be with a passion for the crypto atmosphere.

Fastifynode.jsrest api
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week