Fastify.js bukan hanya kerangka web tercepat untuk node.js

Express.js telah menjadi kerangka web paling populer untuk node.js selama 10 tahun terakhir. Setiap orang yang telah bekerja dengannya tahu bahwa aplikasi Express.js yang kompleks bisa jadi sulit untuk dibuat strukturnya. Tapi, seperti yang mereka katakan, kebiasaan adalah kodrat kedua. Express.js bisa jadi sulit untuk ditinggalkan. Misalnya, sulit berhenti merokok. Tampaknya kita benar-benar membutuhkan rantai middleware yang tak ada habisnya ini, dan jika kita menghilangkan kemampuan untuk membuatnya untuk alasan apa pun dan tanpa alasan, proyek akan berhenti.



Sungguh memuaskan bahwa sekarang, akhirnya, ada pesaing yang layak untuk tempat kerangka web utama untuk semua orang dan segalanya - maksud saya bukan Fastify.js, tetapi, tentu saja, Nest.js. Meskipun dalam hal indikator kuantitatif popularitas, itu sangat-sangat jauh dari Express.js.



Meja. Metrik popularitas paket dari npmjs.org, github.com

Tidak. Paket Jumlah unduhan Jumlah "bintang"
satu Menghubung 4 373 963 9100
2 mengekspresikan 16492 569 52.900
3 koa 844 877 31.100
empat nestjs 624603 36.700
lima hapi 389 530 13.200
6 fastify 216 240 18.600
7 restify 93.665 10 100
delapan polka 71 394 4.700




Express.js masih berfungsi di lebih dari 2/3 aplikasi web node.js. Selain itu, 2/3 dari kerangka kerja web paling populer untuk node.js menggunakan pendekatan Express.js. (Akan lebih akurat untuk mengatakan, pendekatan pustaka Connect.js, yang menjadi dasar Express.js sebelum versi 4).



Posting ini membahas fitur-fitur kerangka kerja web utama untuk node.js, dan apa yang membuat Fastify.js tingkat kerangka kerja yang berbeda, yang memungkinkan Anda untuk memilihnya sebagai kerangka kerja untuk mengembangkan proyek Anda berikutnya.



Kritik kerangka kerja berdasarkan middleware sinkron



Apa yang salah dengan kode semacam ini?



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

      
      





1. Fungsi yang memproses rute tidak mengembalikan nilai. Sebaliknya, Anda harus memanggil salah satu metode pada objek response (res). Jika metode ini tidak dipanggil secara eksplisit, bahkan setelah fungsi tersebut kembali, klien dan server akan tetap dalam keadaan menunggu respons server hingga setiap waktu tunggu habis. Ini hanya “kerugian langsung”, tetapi ada juga “keuntungan yang hilang”. Fakta bahwa fungsi ini tidak mengembalikan nilai membuatnya tidak mungkin untuk hanya mengimplementasikan fungsionalitas yang diminta, misalnya, validasi atau pencatatan respons yang dikembalikan ke klien.



2. Di Express.js, penanganan error bawaan selalu sinkron. Namun, jarang ada rute yang dilakukan tanpa panggilan ke operasi asinkron. Karena Express.js dibangun di era pra-industri, penangan kesalahan sinkron standar untuk kesalahan asinkron tidak akan berfungsi, dan kesalahan asinkron harus ditangani seperti ini:



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

      
      





atau seperti ini:



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

      
      





3. Kompleksitas inisialisasi asynchronous layanan. Misalnya, aplikasi bekerja dengan database dan mengakses database sebagai layanan dengan menyimpan referensi dalam variabel. Inisialisasi rute Express.js selalu sinkron. Ini berarti bahwa ketika permintaan klien pertama mulai tiba di rute, inisialisasi asinkron layanan, kemungkinan besar, tidak akan punya waktu untuk bekerja, jadi Anda harus "menyeret" kode asinkron ke rute untuk mendapatkan tautan ke layanan ini. Semua ini, tentu saja, dapat direalisasikan. Tapi itu terlalu jauh dari kesederhanaan naif dari kode asli:



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

      
      





4. Dan akhirnya, yang terakhir tapi tidak kalah pentingnya. Sebagian besar aplikasi Express.js menjalankan sesuatu seperti ini:



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

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

      
      





Ketika Anda mengembangkan bagian aplikasi Anda, Anda dapat yakin bahwa 10-20 middleware telah bekerja sebelum kode Anda, yang menggantung semua jenis properti pada objek req, dan bahkan dapat mengubah permintaan asli, persis seperti pada kenyataannya bahwa jumlah yang sama jika tidak ada lebih banyak middleware yang dapat ditambahkan setelah Anda mengembangkan bagian aplikasi. Meskipun, dalam dokumentasi Express.js, objek res.locals direkomendasikan secara ambigu untuk melampirkan properti tambahan:



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

      
      





Upaya historis untuk mengatasi kekurangan Express.js



Tidak mengherankan, penulis utama Express.js dan Connect.js - TJ Holowaychuk - meninggalkan proyek untuk mulai mengembangkan kerangka kerja Koa.js yang baru. Koa.js menambahkan asynchrony ke Express.js. Misalnya, kode ini menghilangkan kebutuhan untuk menangkap kesalahan asinkron dalam kode setiap rute dan menempatkan penangan dalam satu 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
    };
  }
})

      
      





Versi Koa.js yang paling awal bermaksud memperkenalkan generator untuk menangani panggilan asinkron:



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

      
      





Pengenalan async / await meniadakan kegunaan bagian Koa.js ini, dan sekarang tidak ada contoh seperti itu bahkan dalam dokumentasi framework.



Hampir seusia dengan Express.js - kerangka kerja Hapi.js. Pengontrol di Hapi.js sudah mengembalikan nilai, yang merupakan langkah maju dari Express.js. Tidak mendapatkan popularitas yang sebanding dengan Express.js, sebuah komponen dari proyek Hapi.js - perpustakaan Joi, yang memiliki 3.388.762 unduhan dari npmjs.org, dan sekarang digunakan baik di backend maupun di frontend, telah menjadi sangat sukses. Menyadari bahwa validasi objek yang masuk bukanlah kasus khusus, tetapi atribut yang diperlukan dari setiap aplikasi - validasi di Hapi.js dimasukkan sebagai bagian dari kerangka kerja, dan sebagai parameter dalam definisi rute:



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

      
      





Saat ini, perpustakaan Joi adalah proyek mandiri.



Jika kita telah mendefinisikan skema validasi objek, maka kita telah mendefinisikan objek itu sendiri. Ada sedikit yang tersisa untuk membuat rute pendokumentasian sendiri di mana perubahan dalam skema validasi data mengubah dokumentasi, sehingga dokumentasi selalu cocok dengan kodenya.



Sejauh ini, salah satu solusi terbaik dalam dokumentasi API adalah swagger / openAPI. Akan sangat mudah jika skema, deskripsi yang mempertimbangkan persyaratan swagger / openAPI, dapat digunakan baik untuk validasi maupun untuk menghasilkan dokumentasi.



Fastify.js



Izinkan saya meringkas persyaratan yang tampaknya penting bagi saya saat memilih kerangka kerja web:



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


Semua poin ini sesuai dengan Nest.js, yang saat ini saya kerjakan di beberapa proyek. Fitur Nest.js adalah penggunaan dekorator secara luas, yang dalam beberapa kasus dapat menjadi batasan jika persyaratan teknis menentukan penggunaan JavaScript standar (dan seperti yang Anda ketahui, dengan standarisasi dekorator di JavaScript, situasi ini terhenti beberapa bertahun-tahun yang lalu, dan tampaknya tidak akan segera menemukan penyelesaiannya) ...



Oleh karena itu, alternatifnya bisa berupa kerangka Fastify.js, fitur yang sekarang akan saya analisis.



Fastify.js mendukung gaya menghasilkan respons server yang akrab bagi pengembang Express.js, dan lebih menjanjikan dalam bentuk nilai pengembalian fungsi, sambil meninggalkan kemampuan untuk secara fleksibel memanipulasi parameter respons lainnya (status, header):



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

      
      





Penanganan error dapat dilakukan secara built-in (out of the box) dan 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' };
  });
}

      
      





Kedua opsi - sinkron dan asinkron - ditangani dengan cara yang sama oleh penangan kesalahan bawaan. Tentu saja, selalu ada sedikit kemampuan bawaan. Mari kita sesuaikan penangan kesalahan:



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

      
      





Bagian kode ini disederhanakan (error melempar literal). Demikian pula, Anda dapat membuat kesalahan khusus. (Mendefinisikan kesalahan serial yang dapat disesuaikan adalah topik terpisah, jadi tidak ada contoh yang diberikan).



Untuk validasi, Fastify.js menggunakan perpustakaan Ajv.js, yang mengimplementasikan antarmuka swagger / openAPI. Fakta ini memungkinkan untuk mengintegrasikan Fastify.js dengan swagger / openAPI dan mendokumentasikan API sendiri.



Secara default, validasi tidak ketat (kolom bersifat opsional dan kolom yang tidak ada dalam skema diperbolehkan). Untuk membuat validasi ketat, perlu untuk menentukan parameter dalam konfigurasi Ajv, dan dalam skema validasi:



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

      
      





Karena skema objek yang masuk telah ditentukan, membuat dokumentasi swagger / openAPI tergantung pada penginstalan 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,
});

      
      





Validasi respons juga dimungkinkan. Untuk melakukan ini, Anda perlu menginstal plugin:



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

      
      





Validasi cukup fleksibel. Misalnya, respons dari setiap status akan diperiksa sesuai dengan skema validasinya sendiri.



Kode yang terkait dengan penulisan artikel dapat ditemukan di sini .



Sumber informasi tambahan



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

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



apapacy@gmail.com

Mei 4 2021 tahun



All Articles