This year, Fastify 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 format on its core. Many of its features and libraries are built around the popular standard. Ajv, 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:
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:
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. 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:
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. Thanks to the official 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:
Now, when you start your Fastify application and navigate to /documentation
in a browser this page will pop up:
Note: The more metadata you add to your routes, the more complete your page will look. Check out the route options documentation.
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. 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
):
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:
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 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. 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:
You can simplify things using toMatchSchema
:
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:
- Validating objects sent to the route.
- Serializing and validating objects returned to the clients.
- Generating documentation.
- Creating mock objects.
- 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:
Now, since posterUrl
must also be serialized and returned to the client, we also add it to the output schema (Movie
):
And that's pretty much it!
Here is what will happen once you restart your server:
- Fastify will start checking for
posterUrl
in thePOST /movie
route. - The Swagger file will be updated. The
posterUrl
property will start showing on the documentation page. - Mock objects in your tests will start being generated with a string value for
posterUrl
. - Tests using the
toMatchSchema
matcher will start checking for theposterUrl
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 or Yup, writing schemas using raw JavaScript objects might feel like a step back.
To overcome that feeling, you can use 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
:
Looks neat, huh?
And that's a wrap! I hope you have enjoyed it. Stay tuned for more Fastify articles. ✌️
Top comments (0)