How to Build REST APIs with Fastify

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces
Published in
11 min readAug 12, 2020

--

REST has grown steadily over the years and has now become the ideal way for systems interaction over the internet. It has grown to have a great advantage over the older kids: SOAP, and XML-RPC. REST is being used by thousands of companies and startups.

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

Nodejs has so many web frameworks with which we can use to build REST APIs, but today we will be learning about a new NOdejs 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

Fastify is a high-performance HTTP web framework for Nodejs. 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
  • Get resources
  • 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 it is not installed, go to Nodejs.org download page and download the installer matched for your machine.
  • NPM: Comes installed by default with Nodejs.
  • MongoDB: This will be our database of choice, our DB can be used also 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.

Also, a good understanding of JavaScript and REST APIs are 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 just do? Simple, Those are bash command type in the bash terminal. Following the commands from the top, we created a folder named “fastify-prj”, this is where our server code will be built. Then, we moved into the folder and then initialized a Node environ 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

The 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 this our file structure will look like this:

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

A Fastify Server

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

Open the index.js and type in the below 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 spin up a simple Fastify server.

First, we imported the Fastify web framework with the require("fastify) statement. Then we call 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 the “/” path in our server 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 "/", and 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 back messages to the browser or anything that called an endpoint. The “request” object contains methods and properties used to get data from the HTTP request from the browser or anything that called the endpoint.

In this case, we used the “send” method in “reply” to send back the text “Our first route” 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 address of the server. The listen method will spawn a process and would listen for HTTP requests on port 3000.

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

$ node index.js

We will see “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 our browser.

Also, using cURL we will still see the “Our first route” displayed on the terminal:

$ curl localhost:3000
Our first route

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.

Quick, let’s create a Mongoose User schema:

touch User.js

We created a User.js file, 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 to 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}`)
})

Next, we import our User schema.

// 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 the User.find({}) so to return all users in the database and 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 id of the user we want to retrieve, and like Express.js, Fastify will map the userId to the request.params object body so we can 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 mongoose User.findById method to retrieve only the user record from the database. After, the user is retrieved from the database we send it to the user using request.send().

OK, we 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 be creating a new user. The user’s details will be in the request.body object. So we retrieved it in the user variable, then called the 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 be editing 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 retrieved the user by calling User.finById with the userId as param, the callback will then accept the retrieved user, which we then update the properties with the data in the request body. Then, finally, we saved the edited user by calling the save() method on the user. This puts the user back to 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 which has a :userId that holds the id of the user to be removed.

To remove a user, we get the id from the request.params and get the user using the User.findById method, the callback will hold the user in its user argument, 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 have to remove our initial demo route “/”. This is done so the code does not confuse us.

Let’s see our code in full.

// 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 below command in your terminal:

$ mongod

Then, in another terminal instance, run our server:

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

Testing the API endpoints

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

For testing, we will use the cURL. Let’s start with the “/api/users POST - Adds a new user"

$ 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 GET - Returns 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”.

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'}

See, it returns the user with the updated values.

We can test the delete by deleting the user 5fa23434fda5643434.

$ curl localhost:3000/api/users/5fa23434fda5643434 -X DELETEUSER 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 also how to use MongoDB database with it. 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. Also, it is not only limited to MongoDB, it can be used with other databases (SQL or NoSQL). The sky is just your limit.

If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email, or DM me.

Thanks !!!

Share & Manage Reusable JS Components with Bit

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

Bit supports Node, TypeScript, React, Vue, Angular, and more.

Example: components shared on Bit

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕