Русский
Русский
English
Статистика
Реклама

Fastify

Fastify.js не только самый быстрый веб-фреймворк для node.js

04.05.2021 18:23:50 | Автор: admin
Последние 10 лет среди веб-фреймворков для node.js самой большой популярностью пользуется Express.js. Всем, кто с ним работал, известно, что сложные приложения на Express.js бывает сложно структурировать. Но, как говорится, привычка вторая натура. От Express.js бывает сложно отказаться. Как, например, сложно бросить курить. Кажется, что нам непременно нужна эта бесконечная цепь middleware, и если у нас забрать возможность создавать их по любому поводу и без повода проект остановится.

Отрадно, что сейчас, наконец, появился достойный претендент на место главного веб-фреймворка всех и вся я имею в виду не Fastify.js, а, конечно же, Nest.js. Хотя по количественным показателям популярности, до Express.js ему очень и очень далеко.

Таблица. Показатели популярности пакетов по данным npmjs.org, github.com
Пакет Количество загрузок Количество звезд
1 connect 4 373 963 9 100
2 express 16 492 569 52 900
3 koa 844 877 31 100
4 nestjs 624 603 36 700
5 hapi 389 530 13 200
6 fastify 216 240 18 600
7 restify 93 665 10 100
8 polka 71 394 4 700


Express.js по-прежнему работает в более чем в 2/3 веб-приложений для node.js. Более того, 2/3 наиболее популярных веб-фреймворков для node.js используют подходы Express.js. (Точнее было бы сказать, подходы библиотеки Connect.js, на которой до версии 4 базировался Express.js).

В предлагаемом сообщении обсуждаются особенности основных веб-фреймворков для node.js, и что делает Fastify.js фреймворком другого уровня, что позволяет выбрать его как фреймворк для разработки Вашего следующего проекта.


Критика фреймворков, основаных на синхронных middleware



Что же плохого может быть в таком коде?

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


1. Функция, которая обрабатывает роут, не возвращает значение. Вместо этого необходимо вызвать один из методов объекта response (res). Если это метод не будет вызван явно, даже после возврата из функции клиент и сервер останутся в состоянии ожидания ответа сервера пока для каждого из них не истечет таймаут. Это только прямые убытки, но есть еще и упущенная выгода. То что эта функция не возвращает значения, делает невозможным просто реализовать востребованную функциональность, например валидацию или логирование возвращаемых клиенту ответов.

2. Обработка ошибок всегда синхронная. Редкий роут обходится без вызовов асинхронных операций. Так как Express.js создавался в допромисовую эру, стандартный обработчик ошибок не сработает и нужно ошибки обрабатывать так:

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


или так:

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


3. Сложность асинхронной инициализации сервисов. Например, приложение работает с базой данных и обращается к базе данных как к сервису, сохранив ссылку в переменной. Инициализация роутов в Express.js всегда синхронная. Это означает, что когда на роуты начнут приходить первые запросы клиентов, асинхронная инициализация сервиса, вероятно еще не успеет отработать, так что придется тащить в роуты асинхронный код с получением ссылки на этот сервис. Все это, конечно, реализуемо. Но слишком далеко уходит от наивной простоты изначального кода:

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


4. Ну и наконец, последнее но немаловажное. В большинстве Express.js приложений работет примерно такой код:

app.use(someFuction);app.use(anotherFunction());app.use((req, res, nexn) => ..., next());app.get('/', (req, res) => {  res.send('Hello World!')})


Когда Вы разрабатываете свою часть приложения, то можете быть уверенным что до вашего кода уже успели отработать 10-20 middleware, которые вешают на объект req всевозможные свойства, и, даже, могут модифицировать исходный запрос, ровно как и в том что столько же если не больше middleware может бтоь добавлено после того, как вы разработаете свою часть приложения. Хотя, к слову сказать, в документации Express.js для навешивания дополнительных свойств неоднозначно рекомендуется объект res.locals:

// из документации Express.jsapp.use(function (req, res, next) {  res.locals.user = req.user  res.locals.authenticated = !req.user.anonymous  next()})


Исторические попытки преодоления недостатков Express.js



Не удивительно, что основной автор Express.js и Connect.js TJ Holowaychuk оставил проект, чтобы начать разработку нового фреймворка Koa.js. Koa.js добавляет асинхронность в Express.js. Например, такой код избавляет от необходимости перехватывать асинхронные ошибки в коде каждого роута и выносит обработчик в один 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    };  }})


Первые версии Koa.js имели замысел внедрить генераторы для обработки асинхронных вызовов:

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


Внедрение async/await свело на нет полезность этой части Koa.js, и сейчас подобных примеров нет даже в документации фреймворка.

Почти ровесник Express.js фреймворк Hapi.js. Контроллеры в Hapi.js уже возвращают значение, что является шагом вперед, по сравнению с Express.js. Не получив популярность сравнимую с Express.js, мега-успешной стала составная часть проекта Hapi.js библиотека Joi, которая имеет количество загрузок с npmjs.org 3 388 762, и сейчас используется как на бэкенде, так и на фронтенде. Поняв, что валидация входящих объектов это не какой-то особый случай, а необходимый атрибут каждого приложения валидация в Hapi.js была включена как составляющая часть фреймворка, и как параметр в определении роута:

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


В настоящее время, библиотека Joi выделена в самостоятельный проект.

Если мы определели схему валидации объекта, тем самымы мы определили и сам объект. Остается совсем немного, чтобы создать самодокументируемый роут, в котором изменение в схеме валидации данных приводит к изменению документации, и, таким образом, документация всегда соответствует программному коду.

На сегодняшний день, одно из лучших решений в документации API swagger/openAPI. Было бы очень удачно, если бы схема, описания с учетом требований swagger/openAPI, могла быть использована и для валидации и для формирования документации.

Fastify.js



Подитожу те требования, который мне кажутся существенными при выборе веб-фреймворка:

1. Наличие полноценных контроллеров (возвращаемое значение функции возвращется клиенту в теле ответа).
2. Удобная обработка синхронных и асинхронных ошибок.
3. Валидация входных параметров.
4. Самодокуметирование на основании определений роутов и схем валидации входных/выходных параметров.
5. Инстанциирование асинхронных сервисов.
6. Расширяемость.

Всем этим пунктам соответствует Nest.js, с которым я сейчас работаю на нескольких проектах. Особенностью Next.js является широкое применение декораторов, что может быть в ряде случаев ограничением, если в технических требованиях указано использовать стандартный JavaScript (а как известно со стандартизацией декораторов в JavaScript сиутация застопорилась несколько лет назад, и, похоже, не скоро найдет свое разрешение).

Поэтому альтернативой может стать фреймворк Fastify.js, особенности применения которого я сейчас разберу.

Fastify.js поддерживает и привычный для разработчиков на Express.js стиль формирования ответа сервера, и более перспективный в форме возвращаемого значения функции, при этом оставляя возможность гибко манипулировать другими параметрами ответа (статусом, заголовками):

// Require the framework and instantiate itconst fastify = require('fastify')({  logger: true})// Declare a routefastify.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}})


Обработка ошибок может быть встроенной (из коробки) и кастомной.

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


Оба варианта и синхронный и асинхронный отрабатывают одинаково встроенным обработчиком ошибок. Конечно, встроенных возможностей всегда мало. Кастомизируем обработчик ошибок:

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


Эта часть кода упрощена (ошибка выбрасывает литерал). Аналогично можно выбрасывать и кастомную ошибку. (Определение кастомных сериализуемых ошибок это отдельная тема, поэтому пример не приводится).

Для валидации Fastify.js использует библиотеку Ajv.js, которая реализует интерфенйс swagger/openAPI. Этот факт делает возможным интеграцию Fastify.js со swagger/openAPI и самодокументирвоание API.

По умолчанию валидация не самая строгая (поля опциональоные и допускаются отсутствующие в схеме поля). Для того чтобы сделать валдиацию строгой необходимо определить параметры в конфигурации Ajv, и в схеме валидации:

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


Поскольку схема входящих объектов уже определена, генерация документации swagger/openAPI сводится к инсталляции плагина:

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


Валидация ответа также возможна. Для этого необходимо инсталлировать плагин:

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


Валидация достаточно гибкая. Например ответ каждого статуса будет проверяться по своей схеме валидации.

Код связанный с написание статьи можно найти здесь.

Дополнительные источники информации

1. blog.stevensanderson.com/2013/12/21/experiments-with-koa-and-javascript-generators
2. habr.com/ru/company/dataart/blog/312638

apapacy@gmail.com
4 мая 2021 года
Подробнее..

NestJS. Загрузка файлов в S3 хранилище (minio)

13.08.2020 02:13:49 | Автор: admin
NestJS фреймворк для создания эффективных, масштабируемых серверных приложений на платформе Node.js. Вы можете встретить утверждение, что NestJS является платформо-независимым фреймворком. Имеется в виду, что он может работать на базе одного из двух фрейморков по Вашему выбору: NestJS+Express или NestJS+Fastify. Это действительно так, или почти так. Эта платформо-независимость заканчивается, на обработке запросов Content-Type: multipart/form-data. То есть практически на второй день разработки. И это не является большой проблемой, если Вы используете платформу NestJS+Express в документации есть пример работы для Content-Type: multipart/form-data. Для NestJS+Fastify такого примера нет, и примеров в сети не так уж и много. И некоторые из этих примеров идут по весьма усложненному пути.

Выбирая между платформой NestJS+Fastify и NestJS+Express я сделал выбор в сторону NestJS+Fastify. Зная склонность разработчиков в любой непонятной ситуации вешать на объект req в Express дополнительные свойства и так общаться между разными частями приложения, я твердо решил что Express в следующем проекте не будет.

Только нужно было решить технический вопрос с Content-Type: multipart/form-data. Также полученные через запросы Content-Type: multipart/form-data файлы я планировал сохранять в хранилище S3. В этом плане реализация запросов Content-Type: multipart/form-data на платформе NestJS+Express меня смущала тем, что не работала с потоками.

Запуск локального хранилища S3



S3 это хранилище данных (можно сказать, хотя не совсем строго, хранилище файлов), доступное по протоколу http. Изначально S3 предоставлялся AWS. В настоящее время API S3 поддерживается и другими облачными сервисами. Но не только. Появились реализации серверов S3, которые Вы можете поднять локально, чтобы использовать их во время разработки, и, возможно, поднять свои серверы S3 для работы на проде.

Для начала нужно определиться с мотивацией использования S3 хранилища данных. В некоторых случаях это позволяет снизить затраты. Например, для хранения резервных копий можно взять самые медленные и дешевые хранилища S3. Быстрые хранилища с высоким трафиком (трафик тарифицируется отдельно) на загрузку данных из хранилища, возможно, будут стоить сравнимо с SSD дисками того же объема.

Более весомым мотивом является 1) расширяемость Вам не нужно думать о том, что место на диске может закончиться, и 2) надежность сервера работают в кластере и Вам не нужно думать о резервном копировании, так как необходимое количество копий есть всегда.

Для поднятия реализации серверов S3 minio локально нужен только установленный на компьютере docker и docker-compose. Соответсвующий файл docker-compose.yml:

version: '3'services:  minio1:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data1-1:/data1      - ./s3/data1-2:/data2    ports:      - '9001:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio2:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data2-1:/data1      - ./s3/data2-2:/data2    ports:      - '9002:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio3:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data3-1:/data1      - ./s3/data3-2:/data2    ports:      - '9003:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio4:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data4-1:/data1      - ./s3/data4-2:/data2    ports:      - '9004:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3


Запускаем и без проблем получаем кластер из 4 серверов S3.

NestJS + Fastify + S3



Работу с сервером NestJS опишу с самых первых шагов, хотя часть этого материала отлично описана в документации. Устанавливается CLI NestJS:

npm install -g @nestjs/cli


Создается новый проект NestJS:

nest new s3-nestjs-tut


Инсталлируются необходимые пакеты (включая те что нужны для работы с S3):

npm install --save @nestjs/platform-fastify fastify-multipart aws-sdk sharpnpm install --save-dev @types/fastify-multipart  @types/aws-sdk @types/sharp


По умолчанию в проекте устанавливается платформа NestJS+Express. Как установить Fastify описано в документации docs.nestjs.com/techniques/performance. Дополнительно нам нужно установить плагин для обработки Content-Type: multipart/form-data fastify-multipart

import { NestFactory } from '@nestjs/core';import {  FastifyAdapter,  NestFastifyApplication,} from '@nestjs/platform-fastify';import fastifyMultipart from 'fastify-multipart';import { AppModule } from './app.module';async function bootstrap() {  const fastifyAdapter = new FastifyAdapter();  fastifyAdapter.register(fastifyMultipart, {    limits: {      fieldNameSize: 1024, // Max field name size in bytes      fieldSize: 128 * 1024 * 1024 * 1024, // Max field value size in bytes      fields: 10, // Max number of non-file fields      fileSize: 128 * 1024 * 1024 * 1024, // For multipart forms, the max file size      files: 2, // Max number of file fields      headerPairs: 2000, // Max number of header key=>value pairs    },  });  const app = await NestFactory.create<NestFastifyApplication>(    AppModule,    fastifyAdapter,  );  await app.listen(3000, '127.0.0.1');}bootstrap();


Теперь опишем сервис, загружающий файлы в хранилище S3, сократив код по обработке некоторых видов ошибок (полный текст есть в репозитарии статьи):

import { Injectable, HttpException, BadRequestException } from '@nestjs/common';import { S3 } from 'aws-sdk';import fastify = require('fastify');import { AppResponseDto } from './dto/app.response.dto';import * as sharp from 'sharp';@Injectable()export class AppService {  async uploadFile(req: fastify.FastifyRequest): Promise<any> {    const promises = [];    return new Promise((resolve, reject) => {      const mp = req.multipart(handler, onEnd);      function onEnd(err) {        if (err) {          reject(new HttpException(err, 500));        } else {          Promise.all(promises).then(            data => {              resolve({ result: 'OK' });            },            err => {              reject(new HttpException(err, 500));            },          );        }      }      function handler(field, file, filename, encoding, mimetype: string) {        if (mimetype && mimetype.match(/^image\/(.*)/)) {          const imageType = mimetype.match(/^image\/(.*)/)[1];          const s3Stream = new S3({            accessKeyId: 'minio',            secretAccessKey: 'minio123',            endpoint: 'http://127.0.0.1:9001',            s3ForcePathStyle: true, // needed with minio?            signatureVersion: 'v4',          });          const promise = s3Stream            .upload(              {                Bucket: 'test',                Key: `200x200_${filename}`,                Body: file.pipe(                  sharp()                    .resize(200, 200)                    [imageType](),                ),              }            )            .promise();          promises.push(promise);        }        const s3Stream = new S3({          accessKeyId: 'minio',          secretAccessKey: 'minio123',          endpoint: 'http://127.0.0.1:9001',          s3ForcePathStyle: true, // needed with minio?          signatureVersion: 'v4',        });        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);      }    });  }}


Из особенностей следует отметить, что мы пишем входной поток в два выходных потока, если загружается картинка. Один из потоков сжимает картинку до размеров 200х200. Во всех случаях используется стиль работы с потоками (stream). Но для того, чтобы отловить возможные ошибки и вернуть их в контроллер, мы вызываем метод promise(), который определен в библиотеке aws-sdk. Полученные промисы накапливаем в массиве promises:

        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);


И, далее, ожидаем их разрешение в методе Promise.all(promises).

Код контроллера, в котором таки пришлось пробросить FastifyRequest в сервис:

import { Controller, Post, Req } from '@nestjs/common';import { AppService } from './app.service';import { FastifyRequest } from 'fastify';@Controller()export class AppController {  constructor(private readonly appService: AppService) {}  @Post('/upload')  async uploadFile(@Req() req: FastifyRequest): Promise<any> {    const result = await this.appService.uploadFile(req);    return result;  }}


Запускается проект:

npm run start:dev


Репозитарий статьи github.com/apapacy/s3-nestjs-tut

apapacy@gmail.com
13 августа 2020 года
Подробнее..
Категории: Node.js , S3 , Nodejs , Docker , Docker-compose , Nestjs , Express , Expressjs , Fastify , Aws-s3 , Minio

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru