Fastify Fundamentals: How to Validate API Responses

Fastify Fundamentals: How to Validate API Responses

Validation is an important aspect of API development as it helps you check whether an API is working as expected, whether it returns the right data quickly, and if it meets security requirements.

Developers who use Fastify need to remain aware of how to handle validations with the framework.

In this tutorial, we will look at how to validate your API's data and serialize it to be read and stored in a reliable data structure.

Fastify and AJV Schema Validator

Fastify embraces the JSON schema, allowing you to define the structure of your API. The JSON schema is used by Fastify for both incoming and outgoing data, allowing it to quickly generate OpenAPI or Swagger specifications from the defined routes.

Our JSON schema validator of choice is AJV, providing a rich API. Validation is important as it checks incoming query strings and body values against the provided schema, which generates an error if it does not match. The error will automatically reach the error handler, which may choose to report the failure to the user.

Let’s look at a short demo to see how this works. In the route folder, create a new file called movies.js and paste the code below there:

export default function movies (fastify, opts, done) { 
app.post(
    "/movies",
    {
      schema: {
        body: {
          type: "object",
          properties: {
            title: { type: "string" },
            title: { type: "number" },
          },
          required: ["title", "year"],
        },
      },
    },
    async (request, reply) => {
      const { title, year } = request.body;
      return { title, year };
    }, done()
  );
}

Then register the plugin in your app.js file as shown below:

import fastify from 'fastify'
import movies from './routes/movies.js'


export async function build (opts = {}) {
  const app = fastify(opts)

  //Root route
  app.get('/', async (request, reply) => {
    return { hello: 'world' }
  })

  //Register the movie route here
  app.register(movies)

  //Handles request to urls that do not exist on our route
  app.get('/notfound', async (request, reply) => {
    reply.callNotFound()
  })

  //This route gets called if an error occurs within the app and throws an error
  app.get('/error', async (request, reply) => {
    throw new Error('kaboom')
  })

  //This handles any error that gets thrown within the application
  app.setErrorHandler(async (err, request, reply) => {
    if (err.validation) {
      reply.code(403)
      return err.message
    }
    request.log.error({ err })
    reply.code(err.statusCode || 500)

    return "I'm sorry, there was an error processing your request."
  })

  //This handles the logic when a request is made to a route that does not exist
  app.setNotFoundHandler(async (request, reply) => {
    reply.code(404)
    return "I'm sorry, I couldn't find what you were looking for."
  })

  return app

}

You can proceed to start the server in the terminal. Your terminal should look like this.

Now let’s test our route. We will use a VS code extension called Thunderclient. You can also use Postman, Rest Client or any other API testing VS Code extensions.

In the URL field, paste the movie route http://localhost:3000/movies and set the request type to a POST request.

Then, click on the Body in the tabs area and select JSON.

First, let's check the response type when we input the correct datatypes as specified in our API.

We get a status code of 200 and the appropriate response as specified in our API.

Now, let's send another request, but this time, we will pass in a string as the value for the year property.

This time we get a status code of 403 and a response stating that body/year must be a number. This error message is automatically handled by the JSON Schema validator in our app.

Finally, let's send another request without the title property in our request body.

Here we also get a response status 403 and a response message stating that the 'body must have required property “title”'.

import movies from './routes/movies.js'

//some code here

app.register(movies)

When you run the code in the terminal (using test data as shown below), here is what happens:

curl -X POST -H 'Content-Type: application/json' -d '{"title" : "foo", "year": 2024 }' http://localhost:3000/movies

Serializations

Fastify provides a serialization solution, making it possible to specify a custom schema for each status code range that the routes return.

While this response may not be completely validated against the schema, unwanted properties are filtered out, helping to increase the application's performance.

This is because fast-json-stringify allows the rendering of JSON faster than JSON.stringify(). It uses the same syntax as JSON Schema.

TypeBox

Typing untrusted input without validation is a security hazard. If you desire type-safety, you can use a tool like TypeBox.

TypeBox is a JSON Schema type builder and runtime validator for TypeScript. It allows you to define the expected structure of JSON data using familiar TypeScript types and then validate incoming data against those predefined types at runtime.

Here is an example of how to implement TypeBox in your Fastify application:

import fastify, { FastifyServerOptions } from 'fastify'
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox'

//create an instance for the Fastify application
const app = fastify.withTypeProvider<TypeBoxTypeProvider>();

app.get('/search', {
  schema: {
    querystring: Type.Object({
      q: Type.Required(Type.String({ minLength: 3 }))
    })
  }
}, async function (request) {
  return { q: request.query.q } 
})

const movie = Type.Object({
  title: Type.Required(Type.String()),
  year: Type.Required(Type.Number())
})

app.post('/movies', {
  schema: {
    body: movie,
    response: {
      200: movie
    }
  }
}, async function (request) {
  return { title: request.body.title, year: request.body.year' }
})


app.listen({ port: 3000 })

In this video, we took a look at this in further depth, exploring how to write type-safe Fastify applications.

Security Notice

It is important to note that schema definitions are part of the application code. Hence, if schemas are obtained from untrusted sources, the application is at risk of injection attacks.

Additionally, the $async Ajv feature, which is disabled by default, should not be used as part of the first validation strategy. The option is used to access databases and reading them during the validation process may lead to a Denial of Service attack on the application.

Instead, if you need to run asynchronous tasks, use Fastify’s hooks after validation completes such as preHandler.

Wrapping Up

In this tutorial, we’ve explored how to use the AJV Schema validator in your Fastify applications and how to handle subsequent errors.

In addition, you should now be able to specify a custom schema that your API returns by implementing serialization. Lastly, we delved into how to build type-safe validations with tools such as Typebox.

Supercharging Fastify Development with Platformatic

Developed by the co-creator of Fastify, Platformatic is a backend development platform designed to extend the capabilities of the Fastify web framework. Together, Platformatic and Fastify offer:

  • Developer-centric design

  • Real-time metrics

  • A vast plugin ecosystem

  • Built-in validation and serialization

  • Built-in logging

Find out more and get in touch.