airasoul.

Building APIs With Fastify, JSON Schema

Cover Image for Building APIs With Fastify, JSON Schema

In the first of this series of 3 posts, I will demonstrate how to setup a node.js based api, with an external JSON schema/swagger specification.  I will be using fastify, which supports swagger/json-schema out of the box.

I recently worked on a project, where by our BA created a fairly large swagger specification, for various reasons, the swagger document was managed outside of the service being developed.

By default, Fastify allows validation rules to be defined as code, on routes defined. In this example we have an external swagger document which we will use to define our api.

To achieve this we use a couple of modules:

This series of posts contains three entries:

1. Building APIs with Fastify, JSON Schema - setup and configure fastify and fastify-openapi-glue, to bind the swagger specification to fastify routes.
2. Documentating APIs with fastify, JSON Schema - setup fastify-swagger to generate swagger documentation.
3. Testing APis with Fastify, JSON Schema - add integration tests using swagger-fixtures, to create test fixtures, which effectively binds our tests to the external swagger document using swagger examples.

The source code for this post can be found here:
https://github.com/AndrewKeig/fastify-api-with-json-schema

API

In order to demonstrate how to set up fastify with an external swagger document, we have defined 3 routes in our API, 2 of these routes are private and require authentication. 1 route is public; the api can be viewed here:

Full swagger document can be found here: src/swagger.json

GET /healthcheck
Unauthenticated endpoint to check status of api
Response:
{
  "requestId": 20,
  "message": "api status ok",
  "responseTimeStamp": 11.088726000860333
}

GET /user/{userId}
headers: token: 1111-2222-3333-444
Authenticated endpoint, returns a single user, based on userId
Response:
{
    "requestId": "21",
    "responseTimeStamp": 5.32319900020957,
    "message": "User fetched successfully",
    "data": {...}
}

POST /user
headers: token: 1111-2222-3333-444
Authenticated endpoint, Creates a new user, based on properties in request body
Response:
{
  "requestId": 22,
  "message": "User added successfully",
  "responseTimeStamp": 19.0625949986279
}

Fastify setup

fastify-openapi-glue is the secret sauce to get this working, simply import, the fastify modules and your swagger file.

The full listing can be found here: src/app/js

const Fastify = require('fastify');
const openapiGlue = require('fastify-openapi-glue');
const specification = \`${\_\_dirname}/swagger.json\`;

Now create a fastify application, passing in your prefered options.

const app = Fastify({
  logger: true,
  pluginTimeout: 10000,
});

In order to configure fastify, to use an external swagger document, we need to define our openapiGlue options, and register our plugin, passing in our options.

  • specification, contains our imported swagger document.
  • noAdditional, we set to true, so that additional properties in a request are rejected.
  • service, contains our routes, more on this, further in the post.
const glueOptions = {
  specification,
  noAdditional: true,
  service:\`${\_\_dirname}/routes.js\`,
};

app.register(openapiGlue, glueOptions);

We will also setup an error handler to handle those validation errors, in a nicer way, feel free to experiment here.

app.setErrorHandler((error, request, reply) => {
  if (error.validation && error.validation.length > 0) {
    constpath = error.validation\[0\].dataPath;
    constfield = path.charAt(1).toUpperCase() + path.slice(2);
    constmessage = \`${field}${error.validation\[0\].message}\`;
    reply.status(422).send(format(request, reply, message));
  }
});

module.exports = app;

Our routes file contains handlers for the 3 routes defined in our specification, getUser and addUser require authentication, a token as defined in our swagger specification, so we run an authenticate function, before the route handler:

Full listing can be found here: src/services/authenticate.js

module.exports.authenticate = async (request, reply) => {
  try {
    const account = await getAccount(request.headers.token);

    if (!account) {
      return send(request, reply, "Unauthorized", 401);
    }

    request.account = account;
    return true;
  } catch (error) {
    request.log.error(error);
    return send(request, reply, "Unauthorized", 401);
  }
};

Full listing can be found here: src/routes.js

const { authenticate } = require("./services/authentication");
const { getHealthcheckRoute } = require("./routes/healthcheck/get");
const { getUserRoute } = require("./routes/user/get");
const { addUserRoute } = require("./routes/user/add");

module.exports = {
  getAPIStatus: async (request, reply) => {
    await getHealthcheckRoute(request, reply);
  },
  getUser: async (request, reply) => {
    await authenticate(request, reply);
    await getUserRoute(request, reply);
  },
  addUser: async (request, reply) => {
    await authenticate(request, reply);
    await addUserRoute(request, reply);
  },
};

The route handlers do nothing interesting, just return a response to demonstrate the api.

Full listing can be found here: src/routes/get.js

const { send } = require("../../services/response");

module.exports.getHealthcheckRoute = async (request, reply) => {
  try {
    return send(request, reply, "api status ok", 200);
  } catch (error) {
    return send(request, reply, "api status down", 500);
  }
};

We now have everything we need to run our fastify api, configured to run an external swagger/json document, please pull the code, and startup the server.