How to Use Schemas on Fastify for Fun and Profit
Tags: fastify, javascript, node
Published: 2020-12-11 15:27:54

---
title: 'How to Use Schemas on Fastify for Fun and Profit'
published: true
description: 
tags:
  - fastify
  - javascript
  - nodejs
cover_image: https://hyperfoo.io/images/fastify-01.jpg
canonical_url: https://ruanmartinelli.com/posts/using-schemas-fastify-fun-and-profit
---

This year, [Fastify](https://fastify.io) became my go-to framework for building Node.js APIs.

If the word sounds new to you, Fastify is a **web framework for Node.js**. It is used to build APIs and services the same way Express does.

Fastify comes with great features that really speed up the process of making applications. Among those features, my favorite one is the fact that the framework is _schema-based_ (I'll explain).

In this post, I will share a few tricks on how you can leverage Fastify's schema capabilities to build APIs at a fast clip.

## Schemas

Fastify adopts the [JSON Schema](https://json-schema.org/) format on its core. Many of its features and libraries are built around the popular standard. [Ajv](https://ajv.js.org/), a library to compile and validate JSON Schemas, is a direct dependency of the framework.

By adopting JSON Schema, Fastify opens doors to an entire ecosystem of tools built around it. Below, let's see how to combine all these tools and libraries together with the framework.

### 1. Validation

One of the ways Fastify uses JSON Schema is to validate data coming from clients. It lets you add _input schemas_ to your routes. For example:

```js
// Schema for `POST /movie` body
const PostMovieBody = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    releaseYear: { type: 'integer', minimum: 1878 },
  },
}

app.post('/movie', {
  schema: {
    // Refence the schema here
    body: PostMovieBody,
  },
  handler: createMovie,
})
```

<figcaption>1. Using input schemas to validate data in the router.</figcaption>

In this example, any incoming data to `POST /movie` that doesn't conform to the `PostMovieBody` schema will throw a validation error.

This way, we are making sure that the handler function doesn't process any invalid or unexpected payloads.

Invalid objects will result in a validation error that looks like this:

```bash
POST /movie
{ releaseYear: 2020 } # The `title` parameter was not sent

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'title'"
}
```

<figcaption>2. Example of Fastify's default error response.</figcaption>

> Note: the content on `message` comes from Ajv.

### 2. Serialization

Serialization is the process of converting an object to a format that can be transferred over a network.

With Fastify, you can also define _output schemas_ for JSON payloads. When you do so, any data returned to clients will be serialized _and_ validated according to that definition.

More specifically, defining output schemas helps you in two ways:

- Fastify serializes the data with [fast-json-stringify](https://github.com/fastify/fast-json-stringify). In many cases, it is faster than `JSON.stringify`.
- Ajv validates the response. This will prevent sensitive fields from being exposed.

When declaring output schemas in your routes, each possible status code accepts a definition. For example, you can have schemas defined for `200` and `204` responses.

> Tip: to use the same schema for a family of status codes, you can use the `'2xx'` notation.

Here's how to define an output schema to responses with a `200` status code:

```js
// Generic `Movie` schema
const Movie = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    title: { type: 'string' },
    releaseYear: { type: 'integer', minimum: 1878 },
  },
}

app.post('/movie', {
  schema: {
    response: {
      // Payloads will be serialized according to the `Movie` schema
      200: Movie,
    },
  },
  // ...
})
```

<figcaption>3. Using output schemas to serialize and validate data in the router.</figcaption>

In this example, any object returned by the handler that doesn't match the `Movie` schema will result in an error. By default, the client receives a `400` response - similar to the example #2.

### 3. Documentation

Documentation is an essential piece in any REST API.

There are many ways to document your application. One of them is manually, where you write routes and definitions by hand in a common format like YAML or JSON.

You can already guess this approach has many problems: outdated schemas, inconsistent validations, type discrepancies, etc.

Another approach is automating your documentation. A tool will automatically generate all the routes and definitions based on an existing schema.

One popular specification for writing documentation is [Swagger](https://swagger.io/). Thanks to the official [fastify-swagger](https://github.com/fastify/fastify-swagger) plugin, you can transform your existing JSON Schema definitions into Swagger ones and expose a beautiful documentation page in a flick.

Adding `fastify-swagger` to a Fastify application should be straightforward:

```js
const fastify = require('fastify')()

// Register the plugin before your routes
fastify.register(require('fastify-swagger'), {
  exposeRoute: true,
  routePrefix: '/documentation',
  swagger: {
    info: { title: 'movie-api' },
    // Add more options to get a nicer page ✨
  },
})

// Declare your routes here...
```

<figcaption>4. Registering the `fastify-swagger` plugin.</figcaption>

Now, when you start your Fastify application and navigate to `/documentation` in a browser this page will pop up:

<img src="https://hyperfoo.io/images/swagger-fastify.jpg" class="shadow-lg rounded-sm mb-4" />

<figcaption>5. The documentation page generated by `fastify-swagger`.</figcaption>

> Note: The more metadata you add to your routes, the more complete your page will look. Check out the [route options documentation](https://www.fastify.io/docs/latest/Routes/#options).

### 4. Mocking

When testing services or endpoints, many times you will need to provide a fake or simulated input. These inputs are called _mock objects_. They replicate the structure and behavior of real objects.

You can create mock objects dynamically with the schemas you already have by using [json-schema-faker](https://github.com/json-schema-faker/json-schema-faker). The library converts existing JSON Schemas into dummy objects that you can use in your tests. Let's see an example.

First, create a helper function (just a wrapper for `json-schema-faker`):

```js
const jsf = require('json-schema-faker')

/**
 * Creates an object from a JSON Schema. Example:
 * schemaToObject(Movie)
 * => { id: 823, title: 'unicorn', releaseYear: 1942 }
 */
function schemaToObject(schema) {
  return jsf.resolve(schema)
}
```

<figcaption>6. Creating the `schemaToObject` helper function. </figcaption>

The `schemaToObject` function does exactly what the name says: given a JSON Schema definition, it returns a matching mock object.

Now let's put it to use. You can call this function whenever you need to create fake objects for your tests. For example, when sending requests to routes:

```js
it('should create a movie', async () =
	// Create a mock object for the request
	const payload = await schemaToObject(PostMovieBody)

	// Calls the POST /movie
	const response = await request.post('/movie', payload)

	expect(response.status).toBe(200)
})
```

<figcaption>7. Using `schemaToObject` to generate fake payloads.</figcaption>

In this example, we are creating a mock object, `POST`-ing it to the `POST /movie` route, and checking the status code.

The `schemaToObject` function gives you a nice and clean way to test the "happy path" in your tests (when everything meets the expectations).

### 5. Jest

[Jest](https://jestjs.io/) is a testing framework for JavaScript. One of its features is the possibility to create or import custom matchers.

One of these matchers is [jest-json-schema](https://github.com/americanexpress/jest-json-schema#readme). This package adds a new assertion to Jest: `toMatchSchema`. It lets you validate an object against an existing JSON Schema definition - it's like Ajv was integrated to Jest.

> Note: If you are using another testing framework, there is likely an equivalent to `jest-json-schema` for it.

Instead of manually asserting the values of each property in an object like this:

```js
it('should create a movie', async () => {
  // ...
  expect(response.title).toBeString()
  expect(response.releaseYear).toBePositive()
})
```

<figcaption>8. Manually asserting each property in an object.</figcaption>

You can simplify things using `toMatchSchema`:

```js
import { matchers } from 'jest-json-schema'
import { Movie } from './schemas'

expect.extend(matchers)

it('should create a movie', async () => {
  // ...
  expect(response).toMatchSchema(Movie)
})
```

<figcaption>9. Asserting all properties using a schema.</figcaption>

Notice I am using the `Movie` schema defined in example #3.

Of course, this is just simplifying type-checking in your tests. There are still other aspects of your code that need to be tested. Still, based on how easy it is to implement, I believe it's a good addition.

## Putting it all together

Let's do a quick recap.

In examples #1 and #3, we have declared two schemas using the JSON Schema format - `PostMovieBody` and `Movie`. These schemas are used for:

1. Validating objects sent to the route.
2. Serializing _and_ validating objects returned to the clients.
3. Generating documentation.
4. Creating mock objects.
5. Asserting objects on tests.

Now here's the fun part!

Suppose you need to start tracking a new property in your movie objects. For example, you need to save and display _the movie poster URL_. Let's name the new field `posterUrl`.

If you were not using a schema-based framework, you would need to go through all your code and update the existing objects to include the new property. This is far from ideal. The chances of missing an assertion in your tests or forgetting to update the documentation are high.

But thanks to the magic of schemas, this process is a breeze. **Your definitions are your source of truth**. Anything based on the schemas will change once the schema changes.

So, now let's see how we can add the `posterUrl` property.

The first step is to change the input schema (`PostMovieBody`) to include the new property:

```diff
const PostMovieBody = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    releaseYear: { type: 'integer', minimum: 1878 },
+   posterUrl: { type: 'string' }
  }
}
```

<figcaption>10. Adding `posterUrl` to the input schema.</figcaption>

Now, since `posterUrl` must also be serialized and returned to the client, we also add it to the output schema (`Movie`):

```diff
const Movie = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    title: { type: 'string' },
    releaseYear: { type: 'integer', minimum: 1878 }
+   posterUrl: { type: 'string' }
  }
}
```

<figcaption>11. Adding `posterUrl` to the output schema.</figcaption>

And that's pretty much it!

Here is what will happen once you restart your server:

1. Fastify will start checking for `posterUrl` in the `POST /movie` route.
2. The Swagger file will be updated. The `posterUrl ` property will start showing on the documentation page.
3. Mock objects in your tests will start being generated with a string value for `posterUrl`.
4. Tests using the `toMatchSchema` matcher will start checking for the `posterUrl` property.

...and you got all that just by changing two lines in your code. How cool is that?

### Honorable mention: fluent-schema

If you are used to libraries like [Joi](https://github.com/sideway/joi) or [Yup](https://github.com/jquense/yup), writing schemas using raw JavaScript objects might feel like a step back.

To overcome that feeling, you can use [fluent-schema](https://github.com/fastify/fluent-schema). It gives you the same compact and programmable interface present in other tools.

For example, we could rewrite the `Movie` schema in example #3 using `fluent-schema`:

```js
const S = require('fluent-schema')

const Movie = const schema = S.object()
  .prop('title', S.string())
  .prop('releaseYear', S.number().minimum(1878))
```

<figcaption>12. Example of the `Movie` schema defined with fluent-schema.</figcaption>

Looks neat, huh?

And that's a wrap! I hope you have enjoyed it. Stay tuned for more Fastify articles. ✌️