How to build REST APIs with Fastify

REST has grown steadily over the years and has become the ideal mode of system interaction over the internet. It’s grown to have a great advantage over the older kids, SOAP and XML-RPC. Thousands of companies and startups are using REST.

REST is preferred because it enables devs and users to predict the action to take, its action on the datastore, and the data they will receive. REST API is an interface in which Mobile, Desktop, and Web systems can interact with a server to perform CRUD actions.

Node.js has many web frameworks we can use to build REST APIs, but today we will be learning about a new Node.js web framework, Fastify.

Tip: Use Bit (Github) to share, document, and manage reusable JS components from different projects. It’s a great way to increase code reuse, speed up development, and build apps that scale.

Fastify

Fast and low overhead web framework for Node.js
  • Get Fastify with NPM: “npm install fastify.” Then, create server.js and add the following content: // Require the framework… www.fastify.io

Fastify is a high-performance HTTP web framework for Nodejs. It is largely inspired by Express.js and Hapi.js, especially in the routing and Middleware systems. Fastify is more like a Chimera of Express.js and Hapi.js in one package.

We will build a basic Users CRUD API to demonstrate how we can use Fastify to:

  • Get a resource
  • Add a resource
  • Edit a resource
  • Delete a resource

What we will learn

In this post, we will learn:

  • How to set up a basic CRUD API using Fastify.
  • How to use a database (MongoDB) with Fastify.
  • The basic routing APIs of Fastify.

Setup

We will need the following utilities:

  • Nodejs: The Nodejs binaries must be installed in your system. If not installed, go to the Nodejs.org download page and download the installer matched for your machine.
  • NPM: This comes installed by default with Nodejs.
  • MongoDB: This will be our database of choice. Our DB can also be used with Fastify, be it MySQL, PostgresSQL, etc.

We will need the following NPM dependencies:

  • Mongoose: A powerful query utility for MongoDB.
  • Fastify: Our main web framework.

A good understanding of JavaScript and REST APIs is also required.

We will set up our project and install the above dependencies:

$ mkdir fastify-prj
$ cd fastify-prj
$ npm init -y
$ touch index.js

What did we do?

Simply, what we added are bash command types in the bash terminal. Following the commands from the top, we created a folder named “fastify-prj” where our server code will be built. Then, we moved into the folder, initialized a Node environment inside the folder, and created an “index.js” file. This is where we will type our code.

Let’s install the dependencies.

First, the Fastify web framework:

$ npm i fastify --save

This command installs the Fastify library.

Next, the Mongoose library:

$ npm i mongoose --save

Note: The --save flag installs them as the main dependency of the project.

With that, our file structure will look like this:

fastify-prj/
    node_modules/
    - index.js
    - package.json

A Fastify server

Now that we have everything installed, let’s set up our Fastify server and create our first route.

Open the index.js and type in the following code:

// index.js
// Import the fastify framework
const fastify = require('fastify')
const app = fastify()
// Set a GET route "/"
app.get('/', function (request, reply) {
    reply.send("Our first route")
})
// Start the server
app.listen(3000, function (err, address) {
    if (err) {
        console.error(err)
        process.exit(1)
    }
    console.log(`Server listening on ${address}`)
})

The above code will set up a simple Fastify server.

First, we imported the Fastify web framework with the require("fastify) statement. Then, we called the Fastify function fastify() and set the returned value to the app variable. Fastify APIs will be available in this variable.

Next, we set up a GET route at our server’s / path by calling the get() method, just like we do in Express.js. The first argument is the path of the API endpoint. In this case it is /.

The second argument is a handler function. This function will be executed when an HTTP GET method with path / is sent to the server. In our case, this handler function replies with an Our first route text to the user.

Handler functions take two arguments: a reply and a request object. The reply object is used to send messages back to the browser or anything called an endpoint. The request object contains methods and properties used to get data from the HTTP request from the browser or anything called the endpoint.

In this case, we used the send method in reply to send the text Our first route back to our caller.

Further down in the code, we call the listen() method, passing a port number 3000 and a callback function. The callback function takes two arguments. The first is an error, and the second is the server’s address. The listen() method will spawn a process and listen for TTP requests on port 3000.

To start our server, run the below command in your terminal:

$ node index.js

We will see a “Server listening on http://localhost:3000" display in our terminal:

$ node index.js
Server listening on http://localhost:3000

Open your browser and navigate to http://localhost:3000. You will see Our first route displayed in your browser.

Using cURL, we will still see the Our first route displayed on the terminal:

$ curl localhost:3000
Our first route
```sh

Creating the users API endpoints

We have demonstrated how to create a simple endpoint using Fastify. Now, we will move on to create our users API endpoints. We will see how we can use the database (MongoDB) with Fastify.

Our users API will have the endpoints:

  • /api/users GET: Returns all users in the datastore.
  • /api/users/:userId GET: Returns a specific user.
  • /api/users POST: Adds a new user.
  • /api/users/:userId PUT: Edits a user.
  • /api/users/:userId DELETE: Removes a user.

Now, let’s quickly create a Mongoose User schema:

touch User.js

We created a User.js file, and this will contain a Mongoose schema for a user:

// User.js
const mongoose = require('mongoose')
let UserSchema = new mongoose.Schema({
    name: String,
    age: Number,
    email: String
})
module.exports = mongoose.model('User', UserSchema)

The UserSchema contains information relating to a single user.

Now, we import the Mongoose library and connect it to the MongoDB server. We will do this in the index.js file:

// index.js
// Import the fastify framework
const fastify = require('fastify')
// Import "mongoose"
const mongoose = require("mongoose")
const app = fastify()
const mongoUrl = process.env.MONGODB_URI || "mongodb://localhost:27017/users"
/** connect to MongoDB datastore */
try {
    mongoose.connect(mongoUrl)
} catch (error) {
    console.error(error)
}
// Set a GET route "/"
app.get('/', function (request, reply) {
    reply.send("Our first route")
})
// Start the server
app.listen(3000, function (err, address) {
    if (err) {
        console.error(err)
        process.exit(1)
    }
    console.log(`Server listening on ${address}`)
})
```js

Next, we import our User schema.
```js
// index.js
// Import the fastify framework
const fastify = require('fastify')
// Import "mongoose"
const mongoose = require("mongoose")
// Import our "User" model
const User = require("./User")
const app = fastify()
...

We code our endpoints:

/api/users GET

...
app.get("/api/users", (request, reply) => {
    User.find({}, (err, users) => {
        if(!err) {
            reply.send(users)
        } else {
            reply.send({ error: err })
        }
    })
})
...

We have our first endpoint, /api/users GET, to get all users in the datastore. We called User. find({}) to return all users to the database and then send it to the user.

api/users/:userId GET

...
app.get("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    User.findById(userId, (err, user) => {
        if(!err) {
            reply.send(user)
        } else {
            reply.send({ error: err })
        }
    })
})
...

Here, we use a parametric path to get a specific user from the API. The :userId in the /api/users/:userId path holds the specific user’s ID that we want to retrieve.

Like Express.js, Fastify will map the userId to the request.params object body to retrieve a user ID by referencing the request.params with userId, like this:

request.params.userId

So, we retrieve the user ID from request.params and use it alongside the Mongoose User.findById method to retrieve only the user record from the database. After the user is retrieved from the database, we send it using request.send().

Now we can move onto the next endpoint.

/api/users POST

...
app.post("/api/users", (request, reply) => {
    var user = request.body
    User.create(user, (err, user) => {
        if(!err) {
            reply.send(user)
        } else {
            reply.send({ error: err })
        }
    })
})
...

This endpoint will be an HTTP POST verb, so that is why we called the post() method on the Fastify instance app. This will set up the /api/users endpoint to listen for a POST request and execute the handler function.

In this endpoint, it will create a new user. The user’s details will be in the request.body object. So, we retrieved it in the user variable, then called User.create with it to create a user with the request details in the database. The handler function will return the created user if successful.

/api/users/:userId PUT

...
app.put("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    var newUserEdit = request.body
    User.findById(userId, (err, user) => {
        if(!err) {
            user.age = newUserEdit.age
            user.name = newUserEdit.name
            user.email = newUserEdit.email
            user.save((er, savedUser) => {
                if(!er) {
                    reply.send(savedUser)
                } else {
                    reply.send(er)
                }
            })
        } else {
            reply.send({ error: err })
        }
    })
})
...

In this endpoint, it will edit an existing user. The PUT HTTP verb is used to denote an editing endpoint. So, we have a parametric path there, with :userId holding the specific ID of the user to be edited/updated. The request body will then hold the data to be updated.

We retrieve the user by calling User.finById with the userId as param. The callback will then accept the retrieved user, and we then update the user properties with the data in the request body. Then, finally, we save the edited user by calling the save() method on the user. This puts the user back in the database with the updated properties.

/api/user/:userId DELETE

...
app.put("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    User.findById(userId, (err, user) => {
        if(!err) {
            user.remove((er) => {
                if(!er) {
                    reply.send("USER DELETED")
                } else {
                    reply.send({ error: er })
                }
            })
        } else {
            reply.send({ error: err })
        }
    })
})
...

This endpoint will delete a user from the database. It is also a parametric path with a :userId that holds the user’s ID to be removed.

We get the ID from the request.params and get the user to be removed with the User.findById method. The callback will hold the user in its user argument, and then we delete the user by calling the remove() method in the returned user. This deletes/removes the user from the database.

We are down with the endpoints. We also have to remove our initial demo route /, just so the code does not confuse us.

Code

// index.js
// Import the fastify framework
const fastify = require('fastify')
// Import "mongoose"
const mongoose = require("mongoose")
// Import our "User" model
const User = require("./User")
const app = fastify()
const mongoUrl = process.env.MONGODB_URI || "mongodb://localhost:27017/users"
/** connect to MongoDB datastore */
try {
    mongoose.connect(mongoUrl)
} catch (error) {
    console.error(error)
}
app.get("/api/users", (request, reply) => {
    User.find({}, (err, users) => {
        if (!err) {
            reply.send(users)
        } else {
            reply.send({ error: err })
        }
    })
})
app.get("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    User.findById(userId, (err, user) => {
        if (!err) {
            reply.send(user)
        } else {
            reply.send({ error: err })
        }
    })
})
app.post("/api/users", (request, reply) => {
    var user = request.body
    User.create(user, (err, user) => {
        if (!err) {
            reply.send(user)
        } else {
            reply.send({ error: err })
        }
    })
})
app.put("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    var newUserEdit = request.body
    User.findById(userId, (err, user) => {
        if (!err) {
            user.age = newUserEdit.age
            user.name = newUserEdit.name
            user.email = newUserEdit.email
            user.save((er, savedUser) => {
                if (!er) {
                    reply.send(savedUser)
                } else {
                    reply.send(er)
                }
            })
        } else {
            reply.send({ error: err })
        }
    })
})
app.put("/api/users/:userId", (request, reply) => {
    var userId = request.params.userId
    User.findById(userId, (err, user) => {
        if (!err) {
            user.remove((er) => {
                if (!er) {
                    reply.send("USER DELETED")
                } else {
                    reply.send({ error: er })
                }
            })
        } else {
            reply.send({ error: err })
        }
    })
})
// Start the server
app.listen(3000, function (err, address) {
    if (err) {
        console.error(err)
        process.exit(1)
    }
    console.log(`Server listening on ${address}`)
})

Before starting our server, we need to power up the Mongo server. To do that, type the this command in your terminal:

$ mongod

Then, in another terminal instance, run this server:

$ node index
Server listening on http://localhost:3000

Testing the API endpoints

We have a fully functional User API. We can now test our API endpoints.

For testing, we will use the cURL. Let’s start with the /api/users POSTAdds a new user endpoint:

$ curl localhost:3000/api/uses -X POST --data "{'name': 'nnamdi', 'age': 20, 'email': 'kurtwanger40@gmail.com'}"
{name: 'nnamdi', age: 20, email: 'kurtwanger40@gmail.com', _id: '5fa23434fda5643434'}

This creates a new user {'name': 'nnamdi', 'age': 20, 'email': 'kurtwanger40@gmail.com'}.

Let’s add a second user:

$ curl localhost:3000/api/uses -X POST --data "{'name': 'chidume', 'age': 27, 'email': 'kurtwanger5@gmail.com'}"
{name: 'chidume', age: 27, email: 'kurtwanger5@gmail.com', _id: 'a56434345fa23434fd'}

Let’s test the /api/users GETReturns all users in the datastore endpoint:

$ curl localhost:3000/api/users 
[
    {name: 'nnamdi', age: 20, email: 'kurtwanger40@gmail.com', _id: '5fa23434fda5643434'},
    {name: 'chidume', age: 27, email: 'kurtwanger5@gmail.com', _id: 'a56434345fa23434fd'}
]

Let’s get specific users:

$ curl localhost:3000/api/users/a56434345fa23434fd
{name: 'chidume', age: 27, email: 'kurtwanger5@gmail.com', _id: 'a56434345fa23434fd'}

This returns the user with the name “chidume”:

$ curl localhost:3000/api/users/5fa23434fda5643434
{name: 'nnamdi', age: 20, email: 'kurtwanger40@gmail.com', _id: '5fa23434fda5643434'}

This returns the user with the name “nnamdi”:

Lastly, let’s edit user “5fa23434fda5643434”:

$ curl localhost:3000/api/users/5fa23434fda5643434 -X PUT --data{"'name': 'nnam', 'age': 29, 'email': 'kurtwnager@gmail.com'"}
{name: 'nnam', age: 29, email: 'kurtwanger@gmail.com', _id: '5fa23434fda5643434'}
```sh

See, it returns the user with the updated values.

We can test the delete by deleting the user 5fa23434fda5643434.
```sh
$ curl localhost:3000/api/users/5fa23434fda5643434 -X DELETE
USER DELETED

Conclusion

We learned a great deal in this post. First, we learned about the new Node.js web framework Fastify, how we can create fast API endpoints with it, and how to use the MongoDB database. We learned about its Express.js and Hapi.js-like routing system.

Fastify is not only limited to REST; we can use it with GraphlQL, gRPC, etc. It is also not only limited to MongoDB, and can be used with other databases (SQL or NoSQL). The sky is the limit.

If you have any questions regarding this or anything to add, correct, or remove, please comment, email, or DM me.

Thanks!

Free Resources

Attributions:
  1. undefined by undefined
Copyright ©2024 Educative, Inc. All rights reserved