Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Accelerating Server-Side Development with Fastify

You're reading from   Accelerating Server-Side Development with Fastify A comprehensive guide to API development for building a scalable backend for your web apps

Arrow left icon
Product type Paperback
Published in Jun 2023
Publisher Packt
ISBN-13 9781800563582
Length 406 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Maksim Sinik Maksim Sinik
Author Profile Icon Maksim Sinik
Maksim Sinik
Matteo Collina Matteo Collina
Author Profile Icon Matteo Collina
Matteo Collina
Manuel Spigolon Manuel Spigolon
Author Profile Icon Manuel Spigolon
Manuel Spigolon
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Part 1:Fastify Basics
2. Chapter 1: What Is Fastify? FREE CHAPTER 3. Chapter 2: The Plugin System and the Boot Process 4. Chapter 3: Working with Routes 5. Chapter 4: Exploring Hooks 6. Chapter 5: Exploring Validation and Serialization 7. Part 2:Build a Real-World Project
8. Chapter 6: Project Structure and Configuration Management 9. Chapter 7: Building a RESTful API 10. Chapter 8: Authentication, Authorization, and File Handling 11. Chapter 9: Application Testing 12. Chapter 10: Deployment and Process Monitoring for a Healthy Application 13. Chapter 11: Meaningful Application Logging 14. Part 3:Advanced Topics
15. Chapter 12: From a Monolith to Microservices 16. Chapter 13: Performance Assessment and Improvement 17. Chapter 14: Developing a GraphQL API 18. Chapter 15: Type-Safe Fastify 19. Index 20. Other Books You May Enjoy

Loading route schemas

Before implementing the schemas, let’s add a dedicated folder to organize our code base better. We can do it inside the ./routes/todos/ path. Moreover, we want to load them automatically from the schemas folder. To be able to do that, we need the following:

  • A dedicated plugin inside the schemas folder
  • A definition of the schemas we wish to use
  • An autohooks plugin that will load everything automatically when the todos module is registered on the Fastify instance

We will discuss these in detail in the following subsections.

Schemas loader

Starting with the first item of the list we just discussed, we want to create a ./routes/todos/schemas/loader.js file. We can check the content of the file in the following code snippet:

'use strict'
const fp = require('fastify-plugin')
module.exports = fp(async function schemaLoaderPlugin (fastify, opts) { // [1]
  fastify.addSchema(require('./list-query.json')) // [2]
  fastify.addSchema(require('./create-body.json'))
  fastify.addSchema(require('./create-response.json'))
  fastify.addSchema(require('./status-params.json'))
})

Let’s break down this simple plugin:

  • We defined a Fastify plugin named schemaLoaderPlugin that loads JSON schemas ([1])
  • We called Fastify’s addSchema method several times, passing the path of each JSON file as an argument ([2])

As we already know, every schema definition defines the structure and the validation rules of response bodies, parameters, and queries for different routes.

Now, we can start implementing the first body validation schema.

Validating the createTodo request body

The application will use this schema during task creation. We want to achieve two things with this schema:

  1. Prevent users from adding unknown properties to the entity
  2. Make the title property mandatory for every task

Let’s take a look at the code of create-body.json:

{
  "type": "object",
  "$id": "schema:todo:create:body", // [1]
  "required": ["title"], // [2]
  "additionalProperties": false, // [3]
  "properties": {
    "title": {
      "type": "string" // [4]
    }
  }
}

The schema is of the object type and, even if short in length, adds many constraints to the allowed inputs:

  • $id is used to identify the schema uniquely across the whole application; it can be used to reference it in other parts of the code ([1]).
  • The required keyword specifies that the title property is required for this schema. Any object that does not contain it will not be considered valid against this schema ([2]).
  • The additionalProperties keyword is false ([3]), meaning that any properties not defined in the "properties" object will be considered invalid against this schema and discarded.
  • The only property allowed is title of the string type ([4]). The validator will try to convert title to a string during the body validation phase.

Inside Using the schemas section, we will see how to attach this definition to the correct route. Now, we will move on and secure the request path parameters.

Validating the changeStatus request parameters

This time, we want to validate the request path parameters instead of a request body. This will allow us to be sure that the call contains the correct parameters with the correct type. The following status-params.json shows the implementation:

{
  "type": "object",
  "$id": "schema:todo:status:params", // [1]
  "required": ["id", "status"], // [2]
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string" // [3]
    },
    "status": {
      "type": "string",
      "enum": ["done", "undone"] // [4]
    }
  }
}

Let’s take a look at how this schema works:

  • The $id field defines another unique identifier for this schema ([1]).
  • In this case, we have two required parameters – id and status ([2]).
  • The id property must be a string ([3]), while status is a string whose value can be "done" or "undone" ([4]). No other properties are allowed.

Next, we will explore how to validate the query parameters of a request using listTodos as an example.

Validating the listTodos request query

At this point, it should be clear that all schemas follow the same rules. A query schema is not an exception. However, in the list-query.json snippet, we will use schema reference for the first time:

{
  "type": "object",
  "$id": "schema:todo:list:query", // [1]
  "additionalProperties": false,
  "properties": {
    "title": {
      "type": "string" // [2]
    },
    "limit": {
      "$ref": "schema:limit#/properties/limit" // [3]
    },
    "skip": {
      "$ref": "schema:skip#/properties/skip"
    }
  }
}

We can now break down the snippet:

  • As usual, the $id property gives the schema a unique identifier that can be referenced elsewhere in the code ([1]).
  • The title property is of the string type, and it is optional ([2]). It can be filtered by the partial title of the to-do item. If not passed, the filter will be created empty.
  • The limit property specifies the maximum number of items to return and is defined by referencing the schema schema:limit schema ([3]). The skip property is also defined by referencing schema schema:skip and is used for pagination purposes. These schemas are so general that they are shared throughout the project.

Now, it is time to take a look at the last schema type – the response schema.

Defining the createTodo response body

Defining a response body of a route adds two main benefits:

  • It prevents us from leaking undesired information to clients
  • It increases the throughput of the application, thanks to the faster serialization

create-response.json illustrates the implementation:

{
  "type": "object",
  "$id": "schema:todo:create:response", // [1]
  "required": ["id"], // [2]
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string" // [3]
    }
  }
}

Let’s examine the structure of this schema:

  • Once again, $id is a unique identifier for this schema ([1])
  • The response object has one required ([2]) property, named id, of the string type ([3])

This response schema ends the current section about schema definitions. Now, it is time to learn how to use and register those schemas.

Adding the Autohooks plugin

Once again, we can leverage the extensibility and the plugin system Fastify gives to developers. We can start by recalling from Chapter 6 that we already registered a @fastify/autoload instance on our application. The following excerpt from the app.js file shows the relevant parts:

fastify.register(AutoLoad, {
  dir: path.join(__dirname, 'routes'),
  indexPattern: /.*routes(\.js|\.cjs)$/i,
  ignorePattern: /.*\.js/,
  autoHooksPattern: /.*hooks(\.js|\.cjs)$/i, // [1]
  autoHooks: true, // [2]
  cascadeHooks: true, // [3]
  options: Object.assign({}, opts)
})

For the purpose of this section, there are three properties that we care about:

  • autoHooksPattern ([1]) is used to specify a regular expression pattern that matches the filenames of the hook files in the routes directory. These files will be automatically loaded and registered as hooks for the corresponding routes.
  • autoHooks ([2]) enables the automatic loading of those hook files.
  • cascadeHooks ([3]) ensures that the hooks are executed in the correct order.

After this brief reminder, we can move on to implementing our autohook plugin.

Implementing the Autohook plugin

We learned from autoHooksPattern in the previous section that we can put our plugin inside a file named autohooks.js in the ./routes/todos directory, and it will be automatically registered by @fastify/autoload. The following snippet contains the content of the plugin:

'use strict'
const fp = require('fastify-plugin')
const schemas = require('./schemas/loader') // [1]
module.exports = fp(async function todoAutoHooks (fastify, opts) {
  fastify.register(schemas) // [2]
})

We start importing the schema loader plugin we defined in a previous section ([1]). Then, inside the plugin body, we register it ([2]). This one line is enough to make the loaded schemas available in the application. In fact, the plugin attaches them to the Fastify instances to make them easily accessible.

Finally, we can use these schemas inside our route definitions, which we will do in the next section.

Using the schemas

Now, we have everything in place to secure our routes and make the application’s throughput ludicrously fast.

This section will only show you how to do it for one route handler. You will find the complete code in the book’s repository at https://github.com/PacktPublishing/Accelerating-Server-Side-Development-with-Fastify/tree/main/Chapter%207, and you are encouraged to experiment with other routes too.

The following code snippet attaches the schemas to the route definition:

  fastify.route({
    method: 'POST',
    url: '/',
    schema: {
      body: fastify.getSchema('schema:todo:create:body'), // [1]
      response: {
        201: fastify.getSchema('schema:todo:create:response') // [2]
      }
    },
    handler: async function createTodo (request, reply) {
      // ...omitted for brevity
    }
  })

We are adding a schema property to the route definition. It contains an object with two fields:

  • The body property of the schema option specifies the JSON schema that the request body must validate against ([1]). Here, we use fastify.getSchema
('schema:todo:create:body'), which retrieves the JSON schema for the request body from the schemas collection, using the ID we specified in the declaration.
  • The response property of the schema option specifies the JSON schema for the response to the client ([2]). It is set to an object with a single key, 201, which specifies the JSON schema for a successful creation response, since it is the code we used inside the handler. Again, we use fastify.getSchema('schema:todo:create:response') to retrieve the JSON schema for the response from the schemas collection.

If we now try to pass an unknown property, the schema validator will strip away from the body. Let’s experiment with it using the terminal and curl:

$ curl -X POST http://localhost:3000/todos -H "Content-Type: application/json" -d '{"title": "awesome task", "foo": "bar"}'
{"id":"6418671d625e3ba28a056013"}%
$ curl http://localhost:3000/todos/6418671d625e3ba28a056013
{"id":"6418671d625e3ba28a056013","title":"awesome task","done":false,"createdAt":"2023-03-20T14:01:01.658Z","modifiedAt":"2023-03-20T14:01:01.658Z"}%

We pass the foo property inside our body, and the API returns a successful response, with the unique id of the task saved in the database. The second call checks that the validator works as expected. The foo field isn’t present in the resource, and therefore, it means that our API is now secure.

This almost completes our deep dive into RESTful API development with Fastify. However, there is one more important thing that can make our code base more maintainable, which we need to mention before moving on.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime