Fastify.js is not only the fastest web framework for node.js

Express.js has been the most popular web framework for node.js for the past 10 years. Everyone who has worked with it knows that complex Express.js applications can be difficult to structure. But, as they say, habit is second nature. Express.js can be difficult to abandon. For example, it is difficult to quit smoking. It seems that we absolutely need this endless chain of middleware, and if we take away the ability to create them for any reason and for no reason, the project will stop.



It is gratifying that now, finally, there is a worthy contender for the place of the main web framework for everyone and everything - I do not mean Fastify.js, but, of course, Nest.js. Although in terms of quantitative indicators of popularity, it is very, very far from Express.js.



Table. Package popularity metrics from npmjs.org, github.com

No. Package Number of downloads Number of "stars"
one connect 4 373 963 9 100
2 express 16 492 569 52,900
3 koa 844 877 31,100
four nestjs 624 603 36,700
five hapi 389 530 13,200
6 fastify 216 240 18,600
7 restify 93,665 10 100
eight polka 71 394 4,700




Express.js still works in over 2/3 of node.js web applications. Moreover, 2/3 of the most popular web frameworks for node.js use Express.js approaches. (It would be more accurate to say, the approaches of the Connect.js library, on which Express.js was based before version 4).



This post discusses the features of the main web frameworks for node.js, and what makes Fastify.js a different level of framework, which allows you to choose it as a framework for developing your next project.



Criticism of frameworks based on synchronous middleware



What could be wrong with this kind of code?



app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





1. The function that processes the route does not return a value. Instead, you must call one of the methods on the response (res) object. If this method is not called explicitly, even after returning from the function, the client and server will remain in a state of waiting for the server's response until each timeout expires. These are only “direct losses”, but there is also “lost profit”. The fact that this function does not return a value makes it impossible to simply implement the requested functionality, for example, validation or logging of responses returned to the client.



2. In Express.js, built-in error handling is always synchronous. However, it is rare for a route to do without calling asynchronous operations. Since Express.js was built in the pre-industrial era, the standard synchronous error handler for asynchronous errors will not work, and asynchronous errors should be handled like this:



app.get('/', async (req, res, next) => {
   try {
      ...
   } catch (ex) {
      next(ex);
   }
})

      
      





or like this:



app.get('/', (req, res, next) => {
   doAsync().catch(next)
})

      
      





3. Complexity of asynchronous initialization of services. For example, an application works with a database and accesses the database as a service by storing a reference in a variable. Express.js route initialization is always synchronous. This means that when the first client requests begin to arrive on the routes, the asynchronous initialization of the service, most likely, will not have time to work out yet, so you will have to "drag" the asynchronous code into the routes to obtain a link to this service. All this is, of course, realizable. But it goes too far from the naive simplicity of the original code:



app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





4. And finally, last but not least. Most Express.js applications run something like this:



app.use(someFuction);
app.use(anotherFunction());
app.use((req, res, nexn) => ..., next());

app.get('/', (req, res) => {
  res.send('Hello World!')
})

      
      





When you develop your part of the application, you can be sure that 10-20 middleware has already worked out before your code, which hang all sorts of properties on the req object, and even can modify the original request, just like in the fact that the same number if not more middleware can be added after you develop your part of the application. Although, by the way, in the Express.js documentation, the res.locals object is ambiguously recommended for attaching additional properties:



//   Express.js
app.use(function (req, res, next) {
  res.locals.user = req.user
  res.locals.authenticated = !req.user.anonymous
  next()
})

      
      





Historical attempts to overcome the shortcomings of Express.js



Not surprisingly, the main author of Express.js and Connect.js - TJ Holowaychuk - left the project to start developing the new Koa.js framework. Koa.js adds asynchrony to Express.js. For example, this code eliminates the need to catch asynchronous errors in the code of each route and puts the handler in one middleware:



app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    // will only respond with JSON
    ctx.status = err.statusCode || err.status || 500;
    ctx.body = {
      message: err.message
    };
  }
})

      
      





The earliest versions of Koa.js had the intention of introducing generators to handle asynchronous calls:



// from http://blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators/
var request = Q.denodeify(require('request'));
 
// Example of calling library code that returns a promise
function doHttpRequest(url) {
    return request(url).then(function(resultParams) {
        // Extract just the response object
        return resultParams[];
    });
}

app.use(function *() {
    // Example with a return value
    var response = yield doHttpRequest('http://example.com/');
    this.body = "Response length is " + response.body.length;
});

      
      





The introduction of async / await negated the usefulness of this part of Koa.js, and now there are no such examples even in the framework documentation.



Almost the same age as Express.js - the Hapi.js framework. Controllers in Hapi.js already return a value, which is a step up from Express.js. Not gaining popularity comparable to Express.js, a component of the Hapi.js project - the Joi library, which has 3 388 762 downloads from npmjs.org, and is now used both on the backend and on the frontend, has become mega-successful. Realizing that validation of incoming objects is not some special case, but a necessary attribute of every application - validation in Hapi.js was included as a part of the framework, and as a parameter in the definition of the route:



server.route({
    method: 'GET',
    path: '/hello/{name}',
    handler: function (request, h) {
        return `Hello ${request.params.name}!`;
    },
    options: {
        validate: {
            params: Joi.object({
                name: Joi.string().min(3).max(10)
            })
        }
    }
});

      
      





Currently, the Joi library is a standalone project.



If we have defined an object validation scheme, then we have defined the object itself. There is very little left to create a self-documenting route in which a change in the data validation schema changes the documentation, so that the documentation always matches the code.



By far, one of the best solutions in the API documentation is swagger / openAPI. It would be very beneficial if the schema, descriptions taking into account the requirements of swagger / openAPI, could be used both for validation and for generating documentation.



Fastify.js



Let me summarize the requirements that seem essential to me when choosing a web framework:



  1. ( ).
  2. .
  3. .
  4. / .
  5. .
  6. .


All these points correspond to Nest.js, with which I am currently working on several projects. A feature of Nest.js is the wide use of decorators, which can in some cases be a limitation if the technical requirements specify the use of standard JavaScript (and as you know, with the standardization of decorators in JavaScript, this situation stalled a few years ago, and it seems that it will not soon find its resolution) ...



Therefore, an alternative can be the Fastify.js framework, the features of which I will now analyze.



Fastify.js supports both the style of generating a server response that is familiar to Express.js developers, and more promising in the form of a function return value, while leaving the ability to flexibly manipulate other response parameters (status, headers):



// Require the framework and instantiate it
const fastify = require('fastify')({
  logger: true
})

// Declare a route
fastify.get('/', (request, reply) => {
  reply.send({ hello: 'world' })
})

// Run the server!
fastify.listen(3000, (err, address) => {
  if (err) throw err
  // Server is now listening on ${address}
})

      
      





const fastify = require('fastify')({
  logger: true
})

fastify.get('/',  (request, reply) => {
  reply.type('application/json').code(200)
  return { hello: 'world' }
})

fastify.listen(3000, (err, address) => {
  if (err) throw err
  // Server is now listening on ${address}
})

      
      





Error handling can be built-in (out of the box) and custom.



const createError = require('fastify-error');
const CustomError = createError('403_ERROR', 'Message: ', 403);

function raiseAsyncError() {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new CustomError('Async Error')), 5000);
  });
}

async function routes(fastify) {
  fastify.get('/sync-error', async () => {
    if (true) {
      throw new CustomError('Sync Error');
    }
    return { hello: 'world' };
  });

  fastify.get('/async-error', async () => {
    await raiseAsyncError();
    return { hello: 'world' };
  });
}

      
      





Both options - synchronous and asynchronous - are handled in the same way by the built-in error handler. Of course, there are always few built-in capabilities. Let's customize the error handler:



fastify.setErrorHandler((error, request, reply) => {
  console.log(error);
  reply.status(error.status || 500).send(error);
});

  fastify.get('/custom-error', () => {
    if (true) {
      throw { status: 419, data: { a: 1, b: 2} };
    }
    return { hello: 'world' };
  });

      
      





This part of the code is simplified (error throws literal). Similarly, you can throw a custom error. (Defining custom serializable errors is a separate topic, so no example is provided).



For validation, Fastify.js uses the Ajv.js library, which implements the swagger / openAPI interface. This fact makes it possible to integrate Fastify.js with swagger / openAPI and self-document the API.



By default, the validation is not the strictest (fields are optional and fields that are not in the schema are allowed). In order to make the validation strict, it is necessary to define the parameters in the Ajv configuration, and in the validation scheme:



const fastify = require('fastify')({
  logger: true,
  ajv: {
    customOptions: {
      removeAdditional: false,
      useDefaults: true,
      coerceTypes: true,
      allErrors: true,
      strictTypes: true,
      nullable: true,
      strictRequired: true,
    },
    plugins: [],
  },
});
  const opts = {
    httpStatus: 201,
    schema: {
      description: 'post some data',
      tags: ['test'],
      summary: 'qwerty',
      additionalProperties: false,
      body: {
        additionalProperties: false,
        type: 'object',
        required: ['someKey'],
        properties: {
          someKey: { type: 'string' },
          someOtherKey: { type: 'number', minimum: 10 },
        },
      },
      response: {
        200: {
          type: 'object',
          additionalProperties: false,
          required: ['hello'],
          properties: {
            value: { type: 'string' },
            otherValue: { type: 'boolean' },
            hello: { type: 'string' },
          },
        },
        201: {
          type: 'object',
          additionalProperties: false,
          required: ['hello-test'],
          properties: {
            value: { type: 'string' },
            otherValue: { type: 'boolean' },
            'hello-test': { type: 'string' },
          },
        },
      },
    },
  };

  fastify.post('/test', opts, async (req, res) => {
    res.status(201);
    return { hello: 'world' };
  });
}

      
      





Since the schema of the incoming objects has already been defined, generating the swagger / openAPI documentation comes down to installing the plugin:



fastify.register(require('fastify-swagger'), {
  routePrefix: '/api-doc',
  swagger: {
    info: {
      title: 'Test swagger',
      description: 'testing the fastify swagger api',
      version: '0.1.0',
    },
    securityDefinitions: {
      apiKey: {
        type: 'apiKey',
        name: 'apiKey',
        in: 'header',
      },
    },
    host: 'localhost:3000',
    schemes: ['http'],
    consumes: ['application/json'],
    produces: ['application/json'],
  },
  hideUntagged: true,
  exposeRoute: true,
});

      
      





Response validation is also possible. To do this, you need to install the plugin:



fastify.register(require('fastify-response-validation'));

      
      





Validation is flexible enough. For example, the response of each status will be checked according to its own validation scheme.



The code related to writing the article can be found here .



Additional sources of information



1. blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators

2. habr.com/ru/company/dataart/blog/312638



apapacy@gmail.com

May 4 2021 year



All Articles