Fastify.js no solo es el marco web más rápido para node.js

Express.js ha sido el marco web más popular para node.js durante los últimos 10 años. Todos los que han trabajado con él saben que las aplicaciones Express.js complejas pueden ser difíciles de estructurar. Pero, como dicen, el hábito es una segunda naturaleza. Express.js puede ser difícil de abandonar. Por ejemplo, es difícil dejar de fumar. Parece que necesitamos absolutamente esta cadena interminable de middleware, y si le quitamos la capacidad de crearlos por cualquier motivo y sin motivo, el proyecto se detendrá.



Es gratificante que ahora, finalmente, haya un competidor digno para el lugar del marco web principal para todos y para todo: no me refiero a Fastify.js, sino, por supuesto, a Nest.js. Aunque en términos de indicadores cuantitativos de popularidad, está muy, muy lejos de Express.js.



Mesa. Métricas de popularidad de paquetes de npmjs.org, github.com

No. Paquete Número de descargas Número de "estrellas"
uno conectar 4 373 963 9100
2 Rápido 16 492 569 52,900
3 Koa 844 877 31,100
cuatro nestjs 624 603 36,700
cinco hapi 389530 13.200
6 fastificar 216 240 18.600
7 restituir 93.665 10 100
ocho polca 71 394 4.700




Express.js todavía funciona en más de 2/3 de las aplicaciones web de node.js. Además, 2/3 de los frameworks web más populares para node.js utilizan enfoques Express.js. (Sería más exacto decir los enfoques de la biblioteca Connect.js, en la que se basaba Express.js antes de la versión 4).



Esta publicación analiza las características de los marcos web principales para node.js y lo que hace que Fastify.js sea un nivel diferente de marco, lo que le permite elegirlo como marco para desarrollar su próximo proyecto.



Crítica de frameworks basados ​​en middleware sincrónico



¿Qué podría estar mal con este tipo de código?



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

      
      





1. La función que procesa la ruta no devuelve ningún valor. En su lugar, debe llamar a uno de los métodos en el objeto de respuesta (res). Si este método no se llama explícitamente, incluso después de que la función regrese, el cliente y el servidor permanecerán esperando la respuesta del servidor hasta que expire cada tiempo de espera. Estas son solo "pérdidas directas", pero también hay "lucro cesante". El hecho de que esta función no devuelva un valor hace que sea imposible simplemente implementar la funcionalidad solicitada, por ejemplo, la validación o el registro de las respuestas devueltas al cliente.



2. En Express.js, el manejo de errores incorporado siempre es sincrónico. Sin embargo, es raro que una ruta funcione sin llamadas a operaciones asincrónicas. Dado que Express.js se creó en la era preindustrial, el controlador de errores síncronos estándar para errores asincrónicos no funcionará, y los errores asincrónicos deben manejarse así:



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

      
      





o así:



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

      
      





3. Complejidad de la inicialización asincrónica de servicios. Por ejemplo, una aplicación trabaja con una base de datos y accede a la base de datos como un servicio almacenando una referencia en una variable. La inicialización de la ruta Express.js siempre es sincrónica. Esto significa que cuando las primeras solicitudes del cliente comiencen a llegar a las rutas, la inicialización asincrónica del servicio, muy probablemente, no tendrá tiempo de funcionar todavía, por lo que tendrá que "arrastrar" el código asincrónico a las rutas para obtener un enlace a este servicio. Todo esto es, por supuesto, realizable. Pero va demasiado lejos de la ingenua simplicidad del código original:



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

      
      





4. Y finalmente, por último, pero no menos importante. La mayoría de las aplicaciones Express.js ejecutan algo como esto:



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

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

      
      





Cuando desarrolle su parte de la aplicación, puede estar seguro de que el middleware 10-20 ya funcionó antes que su código, que cuelga todo tipo de propiedades en el objeto req, e incluso puede modificar la solicitud original, exactamente como en el hecho que se puede agregar la misma cantidad, si no más, middleware después de desarrollar su parte de la aplicación. Aunque, por cierto, en la documentación de Express.js, el objeto res.locals se recomienda de manera ambigua para adjuntar propiedades adicionales:



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

      
      





Intentos históricos de superar las deficiencias de Express.js



Como era de esperar, el autor principal de Express.js y Connect.js, TJ Holowaychuk, abandonó el proyecto para comenzar a desarrollar el nuevo marco Koa.js. Koa.js agrega asincronía a Express.js. Por ejemplo, este código elimina la necesidad de detectar errores asincrónicos en el código de cada ruta y coloca el controlador en un 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
    };
  }
})

      
      





Las primeras versiones de Koa.js tenían la intención de introducir generadores para manejar llamadas asincrónicas:



// 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;
});

      
      





La introducción de async / await anuló la utilidad de esta parte de Koa.js, y ahora no hay tales ejemplos ni siquiera en la documentación del marco.



Casi la misma edad que Express.js: el marco Hapi.js. Los controladores en Hapi.js ya devuelven un valor, que es un paso adelante de Express.js. Al no ganar una popularidad comparable a Express.js, un componente del proyecto Hapi.js, la biblioteca Joi, que tiene 3.388.762 descargas de npmjs.org, y ahora se usa tanto en el backend como en el frontend, se ha vuelto un gran éxito. Al darse cuenta de que la validación de los objetos entrantes no es un caso especial, sino un atributo necesario de cada aplicación, la validación en Hapi.js se incluyó como parte del marco y como parámetro en la definición de la ruta:



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)
            })
        }
    }
});

      
      





Actualmente, la biblioteca Joi es un proyecto independiente.



Si hemos definido un esquema de validación de objetos, entonces hemos definido el objeto en sí. Queda muy poco para crear una ruta autodocumentada en la que un cambio en el esquema de validación de datos cambie la documentación, de modo que la documentación siempre coincida con el código.



De lejos, una de las mejores soluciones en la documentación de la API es swagger / openAPI. Sería muy conveniente que el esquema, descripciones teniendo en cuenta los requisitos de swagger / openAPI, se pudiera utilizar tanto para la validación como para la generación de documentación.



Fastify.js



Permítanme resumir los requisitos que me parecen imprescindibles a la hora de elegir un framework web:



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


Todos estos puntos corresponden a Nest.js, con el que actualmente estoy trabajando en varios proyectos. Una característica de Nest.js es el amplio uso de decoradores, que en algunos casos puede ser una limitación si los requisitos técnicos especifican el uso de JavaScript estándar (y como saben, con la estandarización de decoradores en JavaScript, esta situación se estancó algunos hace años, y parece que no encontrará pronto su resolución) ...



Por tanto, una alternativa puede ser el framework Fastify.js, cuyas características analizaré ahora.



Fastify.js admite tanto el estilo de generar una respuesta de servidor que es familiar para los desarrolladores de Express.js como más prometedor en la forma de un valor de retorno de función, al tiempo que deja la capacidad de manipular de manera flexible otros parámetros de respuesta (estado, encabezados):



// 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}
})

      
      





El manejo de errores puede ser incorporado (listo para usar) y personalizado.



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' };
  });
}

      
      





Ambas opciones, sincrónica y asincrónica, se manejan de la misma manera mediante el controlador de errores incorporado. Por supuesto, siempre hay pocas capacidades integradas. Personalicemos el controlador de errores:



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' };
  });

      
      





Esta parte del código está simplificada (el error arroja literal). Del mismo modo, puede generar un error personalizado. (La definición de errores serializables personalizados es un tema aparte, por lo que no se proporciona ningún ejemplo).



Para la validación, Fastify.js usa la biblioteca Ajv.js, que implementa la interfaz swagger / openAPI. Este hecho hace posible integrar Fastify.js con swagger / openAPI y autodocumentar la API.



Por defecto, la validación no es la más estricta (los campos son opcionales y los campos que no están en el esquema están permitidos). Para que la validación sea estricta, es necesario definir los parámetros en la configuración de Ajv, y en el esquema de validación:



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' };
  });
}

      
      





Dado que el esquema de los objetos entrantes ya se ha definido, la generación de la documentación swagger / openAPI se reduce a instalar el complemento:



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,
});

      
      





La validación de la respuesta también es posible. Para hacer esto, necesita instalar el complemento:



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

      
      





La validación es lo suficientemente flexible. Por ejemplo, la respuesta de cada estado se comprobará de acuerdo con su propio esquema de validación.



El código relacionado con la redacción del artículo se puede encontrar aquí .



Fuentes de información adicionales



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

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



apapacy@gmail.com

Mayo 4 2021 año



All Articles