Skip to content

Upgrading Fastify's input validation to Ajv version 8

Upgrade your application input validation and unlock new features.

Every Fastify v3 application that validates its input JSON Schema can now upgrade to Ajv version 8 for better performance and security.

The upgrade process to reduce server startup time has been designed by Fastify’s team to be accessible to every application that relies upon the JSON-Schema validation to verify the client's input. The new Ajv 8 standalone mode feature makes this possible.

All the source code accompanying this article is available at nearform/fastify-schema-controller-example .

The validation engine

Fastify allows you to validate HTTP parts such as query string parameters, headers, path parameters and JSON body payload through the JSON Schema specification.

Fastify’s Validator Compiler component parses the input schemas at startup and compiles them into a function that is executed when an HTTP request comes in. The compilation step is the most CPU-intensive task that Fastify must execute before listening for incoming HTTP requests.

[caption id="attachment_300015991" align="alignnone" width="1176"]

How Fastify turns JSON Schemas into validation functions[/caption]

This process is one of the ingredients of Fastify’s speed formula to boost your applications.

Note that the Serializer Compiler is not discussed in this article and it won’t be affected by our new configuration. That Fastify component is implemented by the fast-json-stringify module and it doesn’t rely on the Validator Compiler component.

What is the current validator compiler?

The default Fastify v3 validator compiler is implemented by the Ajv v6 module. The Ajv v6 module is the last major release that supports Node.js 10, as Fastify does, and supports the Draft 7 JSON Schema specification.

Unfortunately, Ajv v6 reached the End-Of-Life stage at the end of 2020, so it is no longer supported. Don’t worry; this module is still secure: There are no open vulnerabilities at the time of writing, but you may still come across some bugs.

What is the next validator compiler?

Spoiler alert: Fastify is preparing the next major release, and it will integrate the new Ajv v8 module. Ajv v8 has been rewritten and improved with new features:

  • It supports the Draft 2019-09 JSON Schema specification (formerly known as Draft 8).
  • It introduces support for the experimental JSON Type Definition (JTD).
  • It ships with a new standalone mode to boost your application’s performance.

You can find a complete list of the changes here . The big news is that you can use it right now in your Fastify v3 applications and get the benefits of all the new features and a well supported module.

How do you customise the validation compiler?

Fastify implements the open/closed principle for most of its components and, since v3.16.0, you can customise the whole validation process with the schemaController option . This configuration is used internally by Fastify to inject the Validator Compiler’s implementation, and we are going to use it to do the same to integrate Ajv v8.

Thanks to the schemaController option, you can provide Fastify v3 the new @fastify/ajv-compiler v2 module that integrates Ajv v8, and it will be shipped in Fastify v4 by default.

Adding Ajv v8 into Fastify v3:

JavaScript
import fastify from 'fastify'
import ajvCompiler from '@fastify/ajv-compiler'

const factory = ajvCompiler()

// provide the new ajv8 factory to your fastify server
const app = fastify({
 logger: true,
 schemaController: {
   compilersFactory: {
     buildValidator: factory
   }
 }
})

That’s it. The new Ajv v8 integration is maintained by the Fastify team, so you can still have a supported and updated dependencies tree to secure and consolidate your application.

After the Fastify configuration, you can use the new Ajv options and features right away. Using the new schema syntax:

JavaScript
// define your schemas
const jsonSchemaDraft201909 = {
 type: 'object',
 additionalProperties: false,
 properties: {
   happyDay: {
     $ref: '#/$defs/toggle',
     default: true
   }
 },
 $defs: {
   toggle: {
     type: 'boolean',
     default: null
   }
 }
}

// provide the schemas to your routes
app.post('/validate', {
 schema: {
   body: jsonSchemaDraft201909
 },
 handler: function echo (request, reply) {
   reply.send(request.body)
 }
})

As shown in the previous example, the jsonSchemaDraft201909 variable relies on the new $defs parameter, and no further configuration is needed. You can read a complete migrating list in the official JSON Schema release notes to update your existing schemas to the new Draft version. Of course, you can still use your existing Draft 07 schemas: Ajv v8 supports it as well, and you will get updates and bug fixes, if any.

How does it boost application performance?

One of the most exciting things about Ajv v8 is the new standalone feature, which lets Fastify skip the compilation phase during startup. Based on our tests, for an application that uses ~300 schemas, it reduces the startup time by up to 20%.

How does the standalone mode work?

The Ajv 8 standalone mode generates a CommonJS JavaScript function string that you can load after storing it into the file system.

Multiple benefits include:

  • You don’t need to compile the JSON schemas.
  • You don’t need to load the Ajv module in memory (the biggest Fastify module).

This results in a faster startup.

How to use the standalone mode?

The @fastify/ajv-compiler module documentation explains how to use the Ajv standalone mode. Let’s have a look at it in detail:

[caption id="attachment_300015994" align="alignnone" width="1188"]

The Ajv standalone mode steps[/caption]

There are two phases to using the standalone mode:

  1. Generate your application’s Validation Functions.
  2. Use the generated functions instead of the JSON Schemas.

The image above shows the automatic steps completed by Fastify as dashed arrows. The solid arrows indicate a manual operation you must complete in order to use the standalone mode.

Generate the Validation Functions

In the first phase, you need to configure the @fastify/ajv-compiler module in “write mode” to save all the generated functions to the file system. The write mode configuration:

JavaScript
// note the new import statement
import ajvCompiler from '@fastify/ajv-compiler/standalone.js'
import sanitize from 'sanitize-filename'
import fs from 'fs'
import path from 'path'
import fastify from 'fastify'

// provide a new configuration to the ajvCompiler 
const factoryWrite = ajvCompiler({
 readMode: false,

 // this function must store the schemaValidationCode string argument
 storeFunction (routeOpts, schemaValidationCode) {
   const fileName = generateFileName(routeOpts)
   fs.writeFileSync(path.join(process.cwd(), fileName), schemaValidationCode)
 }
})

// this function generates an identifier that relates an endpoint to its schemas
function generateFileName ({method, url, httpPart}) {
  return `/generated-${method}-${httpPart}-${sanitize(url)}.cjs`
}

// as before provide the factory to the Fastify server
const app = fastify({
 logger: true,
 schemaController: {
   compilersFactory: {
     buildValidator: factoryWrite
   }
 }
})

// … your routes and schemas

Now, your application should start as usual, but you should see a lot of generated- * files — one for each schema of your application.

By doing this, you have stored the generated validation functions, but your application is still compiling the JSON Schemas. It is time to close the server and proceed to phase two and read the generated functions.

Read the generated functions

To skip the compiling step during server startup, you must change the @fastify/ajv-compiler module configuration. The read mode configuration:

JavaScript
// It requires Node.js >= 12.2.0
import { createRequire } from 'module'

// The `restoreFunction` option is synchronous,
// so we need the CommonJS require() function.
const require = createRequire(import.meta.url)

const factoryRead = ajvCompiler({
 readMode: true,
 restoreFunction (routeOpts, schemaValidationCode) {
   const fileName = generateFileName(routeOpts)
   return require(path.join(process.cwd(), fileName))
 }
})

Now you need to replace the factoryWrite with factoryRead variable in the code of the previous section and start your application. Your application is now using the stored validation functions and it’s skipping the JSON Schemas compilation.

Generated code is not fresh

An important reminder: Generated code may not be aligned to your route’s JSON Schema, so you must remember or automate that for every JSON Schema change, the stored validator functions should be re-generated.

Upgrade complete

We have explored upgrading your Fastify v3 application to Ajv v8 and improving your codebase’s security and performance. Doing so will help you to migrate to Fastify v4 when it is released, giving you the opportunity to prepare for your validation setup upgrade.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact