DEV Community

Cover image for Build a CRUD API with Fastify
Elijah Trillionz
Elijah Trillionz

Posted on • Updated on

Build a CRUD API with Fastify

Hello everyone, in this article we are going to build a NodeJS CRUD API with Fastify. Fastify is a NodeJS framework for building fast NodeJS servers. With this wonderful tool, you can create a server with NodeJS, create routes (endpoints), handle requests to each endpoint, and lots more.

Fastify is an alternative of Express (L express) which you must have heard of if you are familiar with NodeJS before. In fact, Fastify draws its inspiration from Express only that Fastify servers are way faster compared to Express servers.

I have tested it, and I can testify of its speed. I am currently building a Mobile Application, and in this app, I use Fastify for my API.

So in this article, we will be building a basic NodeJS server with Fastify. This server will have endpoints to Create data, Read data, Update data, and Delete data (CRUD). We will also be doing some authentication using jwt (next article), just to introduce you to the Fastify plugin ecosystem and how cool it is.

Prerequisites
What are the things you need to know before getting started with Fastify:

  • JavaScript: You should know a good amount of JavaScript especially es5 and es6. CodeCademy has great courses that would guide you
  • NodeJS: You should also be familiar with NodeJS. You can also find NodeJS courses on Codecademy.
  • Express: This is totally optional, but if you already know express then you will learn Fastify at a faster paste.

So enough with the introduction, let's jump right into the code.

See the complete code on Github

Getting Familiar with Fastify

Setting up the App

Just as we would create a server with Express and use a simple endpoint to test if it's running, I am going to show you how we would do this with Fastify. We will initialize the server, register a port and listen for events via the port.

Let's initialize a package.json file. You can do that with npm init -y in a terminal, this will create a file called package.json with some information about your app in JSON.

Now let's install Fastify using NPM. You can use yarn also. Use the npm install Fastify command to install Fastify. Other packages we will be installing are

  • nodemon: for automatically restarting the server whenever we make any changes. We will install this package as a dev dependency. Using NPM is npm install -D nodemon.
  • config: for storing secrets. Useful when you want to publish to GitHub. Installing it would be npm install config

Other packages will be introduced and installed when needed. Let's move on to setting up our package.json.

Go to your package.json and change the value of main to server.js, because the file we will create our server in is going to be called server.js. Furthermore, delete the test property and value. Paste the following code inside the script property.

  "start": "node server.js",
  "server": "nodemon server.js"
Enter fullscreen mode Exit fullscreen mode

This simply means when we run the command npm start on the terminal, it will run our server.js file which will be created soon. But when we run the command npm run server on the terminal, it will run our server.js file using nodemon.

Now create a server.js file and get ready to create your first NodeJS server using Fastify.

Creating our Server

We go into our server.js file and import Fastify. I.e

const fastify = require('fastify')({ logger: true });
Enter fullscreen mode Exit fullscreen mode

The logger: true; key value is an option for activating logging on our terminal from Fastify. So the information of requests, server starting, response, errors will all be logged in the terminal.

The next thing we would do is to assign a port to a PORT variable, I will use 5000 for mine. Why we create a variable for it is for the sake of deploying to production. So you should have something like const PORT = process.env.PORT || 5000. As such we are either using the port of the host company (like Heroku or digital ocean) or our customized 5000.

Now let's create a simple route for get requests to /.

fastify.get('/', (req, reply) => {
  reply.send('Hello World!');
});
Enter fullscreen mode Exit fullscreen mode

Isn't that familiar? Looks just like Express right? Yeah, so working with Fastify is going to be so easy for those already familiar with Express, its syntax is alike.

req and reply stands for request and reply (response). They are parameters obviously, so you can call it whatever you like. But we would go with this simple and readable form.

Ok now let's get our server running by listening for events. We use fastify.listen(port) to listen for requests to our server. But this function returns a promise, so we would create a function that handles this promise using async and await.

const startServer = async () => {
  try {
    await fastify.listen(PORT);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
Enter fullscreen mode Exit fullscreen mode

You wanna make sure you log the error and exit the server if an error occurs. Now we can just call startServer() and run npm run server on the terminal to get the server started.

Fastify api

You should see your URL address in the log info in your terminal like in the image above or simply use http://localhost:5000. Use any API testing tool of your choice to test and you should get a Hello world message as a response.

Creating more Routes

Now you wouldn't want all your routes to be in server.js file, so we will create a folder called routes. This we will use to handle and organize all the different routes of our API.

This API is going to be for a blog, so our data will basically be about posts, and admins making these posts. So in the routes folder, create a _ posts.js_, and admins.js file.

To make these files work as endpoints on our server, we need to register them as a plugin. Now don't panic, it's easier than you think. Add the code below to server.js just before the startServer function.

fastify.register(require('./routes/posts')); // we will be working with posts.js only for now
Enter fullscreen mode Exit fullscreen mode

That will register the post routes. You could first import and assign it to a variable and then pass the variable as a parameter in the register function, choice is yours.

If you save, it's going to generate an error, now this is because we haven't created any route in posts.js yet.

In posts.js, create a function called postRoutes and pass these three parameters fastify, options, and done. This function is going to make an instance of our fastify server, which means with the first parameter we can do everything we could do in server.js with the fastify variable.

Now you can cut the get request from server.js into the postRoutes function in posts.js.

Your postRoutes should look like this:

const postRoutes = (fastify, options, done) => {
  fastify.get('/', (req, reply) => {
    reply.send('Hello world');
  });
};
Enter fullscreen mode Exit fullscreen mode

The options (sometimes written as opts) parameter is for options on the routes, we will not be using this.

The done parameter is a function we would call at the end of the postRoutes function, to indicate we are done. Just like making a middleware in Express and calling next to move on.

So you should have done() at the last line of the postRoutes function.

Now, let's export the function and save our file. Use the following command at the last line of the posts.js file to export: module.exports = postRoutes.

Save your file and test your route.

Organizing Routes

We could create more routes just like the one above and call it a day, but then we will hinder ourselves from some of the great features of Fastify. With Fastify we can better organize our API by separating concerns.

With Fastify we can create schemas for requests coming to a route and responses going out. For requests, we can tell Fastify what to expect from the body of the request, or the headers, or params, etc.

We can also tell Fastify what we intend to send as a response e.g the data that will be sent on a 200 response, or 400 response or 500 response, etc.

For example, let's make a schema for our get request above. In our get request we sent Hello world (a string) as a response, now we will be sending an array of posts like this

fastify.get('/', (req, reply) => {
  reply.send([
    { id: 1, title: 'Post One', body: 'This is post one' },
    { id: 2, title: 'Post Two', body: 'This is post two' },
    { id: 3, title: 'Post Three', body: 'This is post three' },
  ]);
});
Enter fullscreen mode Exit fullscreen mode

Let's make a schema for it. A schema in Fastify is an object, this object will be passed as a value for a schema property.

const opts = {
  schema: {},
};

const postRoutes = (fastify, options, done) => {
  fastify.get('/', opts);

  done();
};
Enter fullscreen mode Exit fullscreen mode

This is the way we will be defining our routes, the get method (could be post or any method) will take two parameters, the first being the route and the last is an object of options.

The three properties of the object of options we will be using in this API are

  • schema: defines how our data should be set up, what data should come in, and what data should go out, including their types (string, boolean, number, etc).

  • preHandler: a function that defines what should be done before requests are handled by the handler below.

  • handler: a function that handles the request.

It may not be clear to you now, but when we make examples you will get it straight up. The preHandler is going to be used for authentication, which means it will be used on protected routes only.

Enough with the explanation, if you want more explanation check out the docs. Let's dive into codes.

Our get request is about to look way better.

const opts = {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'number' },
            title: { type: 'string' },
            body: { type: 'string' },
          },
        },
      },
    },
  },
  handler: (req, reply) => {
    reply.send([
      { id: 1, title: 'Post One', body: 'This is post one' },
      { id: 2, title: 'Post Two', body: 'This is post two' },
      { id: 3, title: 'Post Three', body: 'This is post three' },
    ]);
  },
};
Enter fullscreen mode Exit fullscreen mode

While it has now become better, I presume it is now more confusing. Well, it's simple, let's analyze the schema object.

schema

In the schema object, we are telling Fastify that on a 200 response what we will send is an array. And each item in this array is an object and the properties of these objects are id, title, and body which are of type number, string, and string respectively.

Simple right. You should take note of the names of the properties used i.e response, 200, type. The items and properties can be any name but I recommend using these names.

If you try removing the id property and value from the schema object you would notice the id property is no longer sent as part of the response. While if you try changing the id property from type number to type string, you would see it as a string in the response. Cool right!

handler

The handler function is clear enough, we simply copied what we had in our get request.

The opts object is specific to a route. Unless you want to handle different requests on different routes with one response. If that's not the case, then you should make sure the name of the object is unique.

For example in our get request, since we are getting posts we could change the name to getPostsOpts.

Our posts.js should now look like this

const getPostsOpts = {
  schema: {
    response: {
      200: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            id: { type: 'number' },
            title: { type: 'string' },
            body: { type: 'string' },
          },
        },
      },
    },
  },
  handler: (req, reply) => {
    reply.send([
      { id: 1, title: 'Post One', body: 'This is post one' },
      { id: 2, title: 'Post Two', body: 'This is post two' },
      { id: 3, title: 'Post Three', body: 'This is post three' },
    ]);
  },
};

const postRoutes = (fastify, options, done) => {
  fastify.get('/', getPostsOpts);

  done();
};
Enter fullscreen mode Exit fullscreen mode

Now imagine, having 10 routes with different schemas and handlers and maybe some preHandlers. You can tell the code is going to be very bucky and scary to read. This is where controllers come in.

Controllers is not some kind of plugin or package as it sounds. It's just a folder we will create to separate our routes from our schemas and handlers.

Inside of our controllers folder, I am going to create two other folders called schemas and handlers. It makes it look cleaner and easy to read.

In our schemas folder we will create a file called posts.js. This file will contain all the schemas for our post routes (getting all posts, creating a post, deleting a post, etc.).

In schemas/posts.js, create an object called getPostsSchema and cut the value of the schema property (from routes/posts.js) and paste it as the object. Your code should look like this

const getPostsSchema = {
  response: {
    200: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          id: { type: 'number' },
          title: { type: 'string' },
          body: { type: 'string' },
        },
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Now let's export it;

const getPostsSchema = {
  // our schemas
};

module.exports = { getPostsSchema };
Enter fullscreen mode Exit fullscreen mode

We will import it in our routes/posts.js file so we can use it as the value of the schema property.

const { getPostsSchema } = require('../controllers/schemas/posts.js');

const getPostsOpts = {
  schema: getPostsSchema,
  handler: (req, reply) => {
    reply.send([
      { id: 1, title: 'Post One', body: 'This is post one' },
      { id: 2, title: 'Post Two', body: 'This is post two' },
      { id: 3, title: 'Post Three', body: 'This is post three' },
    ]);
  },
};
Enter fullscreen mode Exit fullscreen mode

In our handlers folder, let's create a file called posts.js. This file will contain all our handler functions for our post routes (getting all posts, creating a post, deleting a post, etc.).

In handlers/posts.js, create a function called getPostsHandler with req and reply as our params. Copy the function body from the routes/posts.js file and paste it here, after which export the function. It should look like this

const getPostsHandler = (req, reply) => {
  reply.send([
    { id: 1, title: 'Post One', body: 'This is post one' },
    { id: 2, title: 'Post Two', body: 'This is post two' },
    { id: 3, title: 'Post Three', body: 'This is post three' },
  ]);
};

module.exports = { getPostsHandler };
Enter fullscreen mode Exit fullscreen mode

Import the getPostsHandler into the routes/posts.js file, and set it as the value of the handler method. Your routes/posts.js would look like this

const { getPostsSchema } = require('../controllers/schemas/posts.js');
const { getPostsHandler } = require('../controllers/handlers/posts.js');

const getPostsOpts = {
  schema: getPostsSchema,
  handler: getPostsHandler,
};

const postRoutes = (fastify, opts, done) => {
  fastify.get('/', getPostsOpts);

  done();
};
Enter fullscreen mode Exit fullscreen mode

This looks cleaner right? Now save the file and test it, it should work fine as before.

I would have loved to talk about organizing authentication here, but it would make this article too long, so I will make another article on authentication.

Alright Elijah, can we just build the CRUD API already? Yeah sure!

Building your first CRUD API with Fastify

We will create a blog API where we can create a post, read all posts, read a post, delete a post, and update a post. We will also be able to create admins, log admins in, and make protected routes. But we will do this in another article.

Get all Posts

Since we already have a working get request, I will simply make some changes to the routes and the array of posts.

In routes/posts.js.

fastify.get('/api/posts', getPostsOpts);
Enter fullscreen mode Exit fullscreen mode

That should make the route look more like an API endpoint.

Let's create a folder in the root directory called cloud and create a posts.js file. This file will act as our database because we will be storing all our posts there. Paste the code below in it:

const posts = [
  { id: 1, title: 'Post One', body: 'This is post one' },
  { id: 2, title: 'Post Two', body: 'This is post two' },
  { id: 3, title: 'Post Three', body: 'This is post three' }, // you can add as many as you want
];

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

In handlers/posts.js, import posts and replace it with the array in the send function i.e

In handlers/posts.js.

const posts = require('../../cloud/posts.js');

const getPostsHandler = (req, reply) => {
  reply.send(posts);
};

module.exports = { getPostsHandler };
Enter fullscreen mode Exit fullscreen mode

Save the file and run the program, recall the routes have changed. To get all posts use http://localhost:your_port/api/posts

Note: There are four files called posts.js.

  • cloud/posts.js: where the array of posts is stored (our database).
  • routes/posts.js: where we handle all routes of our blog posts.
  • handlers/posts.js: where we handle responses to our post routes.
  • schemas/posts.js: where we specify the schemas of our post routes.

I will make reference to each one of them with their folder so you can easily know who is who.

Get a Post

The next route we would make is to get a post, we will do this with its id. So we get an id as a parameter from the request and we will filter the posts array to find that post.

Create the Route in routes/posts.js

In routes/posts.js, just below our first route, paste the code below

fastify.get('/api/posts/:id', getPostOpts); // the :id route is a placeholder for an id (indicates a parameter)
Enter fullscreen mode Exit fullscreen mode

Let's create the getPostOpts object

const getPostOpts = {
  schema: getPostSchema, // will be created in schemas/posts.js
  handler: getPostHandler, // will be created in handlers/posts.js
};
Enter fullscreen mode Exit fullscreen mode

Create the Schema in schemas/posts.js

Create an object called getPostSchema and paste the following

const getPostSchema = {
  params: {
    id: { type: 'number' },
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'number' },
        title: { type: 'string' },
        body: { type: 'string' },
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

The params property indicates what data should be collected in the params of the route. I am using it to format the id to a number. By default is a string. Since the ids in our posts array are a number, I simply want them to be of the same type.

Then, since we are fetching just one post, it means our response is going to be an object with id, title, and body as its properties. Export the getPostSchema, simply add it to the object being exported i.e module.exports = { getPostsSchema, getPostSchema };

Now take a good look at your routes/posts.js, you would observe you have repeated yourself. So refactor it to make sure you are not repeating yourself, this is what I did

const typeString = { type: 'string' }; // since i will be using this type a lot

const post = {
  type: 'object',
  properties: {
    id: { type: 'number' },
    title: typeString,
    body: typeString,
  },
};

const getPostsSchema = {
  response: {
    200: {
      type: 'array',
      items: post,
    },
  },
};

const getPostSchema = {
  params: {
    id: { type: 'number' },
  },
  response: {
    200: post,
  },
};

module.exports = { getPostsSchema, getPostSchema };
Enter fullscreen mode Exit fullscreen mode

Create the Handler in handlers/posts.js

In handlers/posts.js, create an object called getPostHandler and paste the following

const getPostHandler = (req, reply) => {
  const { id } = req.params;

  const post = posts.filter((post) => {
    return post.id === id;
  })[0];

  if (!post) {
    return reply.status(404).send({
      errorMsg: 'Post not found',
    });
  }

  return reply.send(post);
};
Enter fullscreen mode Exit fullscreen mode

The first line of the function body is how we fetch the id from the request route. So a route like http://localhost:5000/api/posts/4 will return 4 as its id.

The reply.status function tells Fastify what status code the response should be. If the post is not found a customized error message is sent, with Fastify we could also use

return reply.status(404).send(new Error('Post not found'));
Enter fullscreen mode Exit fullscreen mode

So when a post is not found Fastify will send the JSON below as a response

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Post not found"
}
Enter fullscreen mode Exit fullscreen mode

Now export getPostHandler and save all files. Run the program and test your new route.

Creating new Posts

Create the Route in routes/posts.js

First, let's create the route in the postRoutes function. Just after the last route we created, paste the code below

fastify.post('/api/posts/new', addPostOpts);
Enter fullscreen mode Exit fullscreen mode

/api/posts/new is our endpoint to add a new post to our array of posts. The next thing we'd do is create the addPostOpts object outside of our routes function and pass a value for schema and handler

const addPostOpts = {
  schema: addPostSchema, // will be created in schemas/posts.js
  handler: addPostHandler, // will be created in handlers/posts.js
};
Enter fullscreen mode Exit fullscreen mode

In my next article, I will make this route a private route, which means we will add a preHandler to the object above in the next article.

Create the Schema in schemas/posts.js

We will tell Fastify what data should come in from our request body and what data we will be sending out as a response.

Create an object called addPostSchema, assign the code below to it;

const addPostSchema = {
  body: {
    type: 'object',
    required: ['title', 'body']
    properties: {
      title: typeString, // recall we created typeString earlier
      body: typeString,
    },
  },
  response: {
    200: typeString, // sending a simple message as string
  },
};
Enter fullscreen mode Exit fullscreen mode

We use body as a property to tell Fastify what to expect from the request body of our post route. Just like we did with params above. We can also do the same for headers (I will show you this during authentication).

With the required property we are telling Fastify to return an error if both title and body are not part of the request body.

Fastify will return a 400 Bad Request error as a response if a required field is not provided.

Add addPostSchema to the object being exported out of this file (schemas/posts.js).

Create the Handler in handlers/posts.js

We will create an id for the data that is sent to us and append it to our array of posts. Simple right!

const addPostHandler = (req, reply) => {
  const { title, body } = req.body; // no body parser required for this to work

  const id = posts.length + 1; // posts is imported from cloud/posts.js
  posts.push({ id, title, body });

  reply.send('Post added');
};
Enter fullscreen mode Exit fullscreen mode

Add addPostHandler to the object being exported out of this file (handlers/posts.js).

Before saving your files and running your program, make sure to add addPostSchema and addPostHandler to the object being imported into routes/posts.js.

To verify your post has been created you can run http://localhost:your_port/api/posts (our first endpoint), you would see it at the bottom of the array.

Updating a Post

Create the Route in routes/posts.js

We will use the put method for this route. Add the code below to your postRoutes function

fastify.put('/api/posts/edit/:id', updatePostOpts);
Enter fullscreen mode Exit fullscreen mode

Next thing is to create the updatePostOpts object outside of the postRoutes function. As before, we will pass a value for the schema and handler properties i.e

const updatePostOpts = {
  schema: updatePostSchema, // will be created in schemas/posts.js
  handler: updatePostHandler, // will be created in handlers/posts.js
};
Enter fullscreen mode Exit fullscreen mode

Before moving to the other files quickly add updatePostSchema and updatePostHandler to the imported objects in this file (routes/posts.js).

Create the Schema in schemas/posts.js

Create an object called updatePostSchema and use this code for it

const updatePostSchema = {
  body: {
    type: 'object',
    required: ['title', 'body'],
    properties: {
      title: typeString,
      body: typeString,
    },
  },
  params: {
    id: { type: 'number' }, // converts the id param to number
  },
  response: {
    200: typeString, // a simple message will be sent
  },
};
Enter fullscreen mode Exit fullscreen mode

Don't forget to add the updatePostSchema to the object being exported out.

Create the Handler in handlers/posts.js

const updatePostHandler = (req, reply) => {
  const { title, body } = req.body;
  const { id } = req.params;

  const post = posts.filter((post) => {
    return post.id === id;
  })[0];

  if (!post) {
    return reply.status(404).send(new Error("Post doesn't exist"));
  }

  post.title = title;
  post.body = body;

  return reply.send('Post updated');
};
Enter fullscreen mode Exit fullscreen mode

Don't forget to add the updatePostHandler to the object being exported out.

Now you can save your files and test your new route.

Deleting a Post

Create the Route in routes/posts.js

We will follow the same procedure we have been following in the previous routes, we will only change the route and method.

fastify.delete('/api/posts/:id', deletePostOpts);
Enter fullscreen mode Exit fullscreen mode

The deletePostOpts object would be

const deletePostOpts = {
  schema: deletePostSchema,
  handler: deletePostHandler,
};
Enter fullscreen mode Exit fullscreen mode

Create the Schema in schemas/posts.js

You should note that creating schemas is completely optional, for a route like this, you may not have to create a schema.

const deletePostSchema = {
  params: {
    id: { type: 'number' }, // converts the id param to number
  },
  response: {
    200: typeString,
  },
};
Enter fullscreen mode Exit fullscreen mode

Create the Handler in handlers/posts.js

const deletePostHandler = (req, reply) => {
  const { id } = req.params;

  const postIndex = posts.findIndex((post) => {
    return post.id === id;
  });

  if (postIndex === -1) {
    return reply.status(404).send(new Error("Post doesn't exist"));
  }

  posts.splice(postIndex, 1);

  return reply.send('Post deleted');
};
Enter fullscreen mode Exit fullscreen mode

Export your handler and schema, and import them in routes/posts.js accordingly. Save your files and test your new route.

Final Words

These are my final words for this article, not for Fastify. We are yet to add admins' routes that will involve authentication. We will do that next, so you wanna make sure you get the notification when that is out.

With that being said, I want to congratulate you on building your first CRUD API with Fastify. In this project, we created routes for Creating data, Reading data, Updating data, and Deleting data. We also explained a tiny bit of Fastify. So great job.

If you find this article useful please do like and share. You can also support me with a cup of coffee. Thanks for reading and happy hacking.

Latest comments (1)

Collapse
 
danger89 profile image
Melroy van den Berg • Edited

my 2cents.

You can also add a handle/controller to your Fastify instance via prefix (Typescript example):

import { FastifyPluginAsync } from 'fastify'
import userHandler from '../../controllers/handlers/user.js'

const apiRoutes: FastifyPluginAsync = async (instance) => {
   instance.register(userHandler, { prefix: '/users' })
}
export default apiRoutes
Enter fullscreen mode Exit fullscreen mode

Also I want to mention you can access the Fasty instance nowadays via your req/request parameter, via: request.server. Which can be very useful if you configured let's say MySQL. This allows you to execute a query in a controller (handler) like so: request.server.mysql.query(....).

Again MySQL was here just an example, but you get the point. Matteo Collina (the leader maintainer of Fastify) actually recommends to keep the route & handler code together. But that is his opinion.