Перейти к содержанию

Type-Safe Fastify

Добро пожаловать в заключительную главу этой книги! В этой главе мы рассмотрим, как встроенная в Fastify поддержка TypeScript может помочь нам писать более надежные и поддерживаемые приложения.

Безопасность типов — важнейший аспект современной разработки программного обеспечения. Она позволяет разработчикам выявлять ошибки и недочеты на ранних этапах процесса разработки, сокращая время и затраты на отладку и тестирование. Используя TypeScript с Fastify, вы можете воспользоваться преимуществами безопасности типов во время компиляции и избежать непредвиденных ошибок во время выполнения, что приведет к созданию более стабильных и надежных приложений.

Использование TypeScript с Fastify также улучшает работу разработчиков, обеспечивая лучшее завершение кода, вывод типов и документирование. Кроме того, Fastify обладает первоклассной поддержкой TypeScript, предоставляя все необходимое для создания надежных и масштабируемых приложений, включая интерфейсы и дженерики.

Более того, использование TypeScript делает развертывание более безопасным, поскольку позволяет отлавливать потенциальные ошибки и баги еще до запуска. Это обеспечивает дополнительный уровень защиты нашего кода, что дает нам больше уверенности при развертывании.

К концу главы мы научимся делать следующее:

  • Создать простой проект Fastify на TypeScript
  • Добавлять поддержку автоматического вывода типов с помощью так называемых провайдеров типов
  • Автоматически генерировать сайт документации для наших API

Технические требования

Чтобы следовать этой главе, вам понадобится следующее:

Код проекта можно найти на GitHub.

В следующем разделе мы подробно рассмотрим настройку проекта Fastify на TypeScript, добавив все зависимости, необходимые для его работы.

Создание проекта

Создание нового проекта Fastify с поддержкой TypeScript несложно, но требует добавления некоторых дополнительных зависимостей. В этом разделе мы рассмотрим процесс ручной настройки нового проекта Fastify с TypeScript и установки необходимых зависимостей.

Начнем с файла package.json, конфигурационного файла для проекта Node.js. Он содержит информацию о зависимостях, точке входа и скриптах. Ниже приведен лишь частичный фрагмент, поскольку мы будем развивать его в ходе изучения разделов этой главы:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
    "version": "1.0.0",
    "main": "dist/server.js", // [1]
    "dependencies": {
        "@fastify/autoload": "^5.7.1",
        "fastify": "^4.15.0"
    },
    "devDependencies": {
        "@types/node": "^18.15.11", // [2]
        "eslint-config-standard-with-typescript": "^34.0.1",
        "fastify-tsconfig": "^1.0.1", // [3]
        "rimraf": "^5.0.0",
        "tsx": "^3.12.6", // [4]
        "typescript": "^5.0.4" // [5]
    },
    "scripts": {
        "build": "rimraf dist && tsc", // [6]
        "dev": "tsx watch src/server.ts" // [7]
    }
}

Файл package.json, указанный в предыдущем блоке кода, включает в себя базовые зависимости, необходимые для проекта, и два скрипта, которые улучшат опыт разработки. Как мы уже говорили, чтобы добавить поддержку TypeScript, нам нужно добавить в проект несколько дополнительных зависимостей для разработки, помимо Fastify.

Вот разбивка зависимостей для разработки:

  • Поле main ([1]) определяет точку входа приложения, когда оно запускается командой node . из корня проекта.
  • @types/node ([2]) — это зависимость разработки, которая предоставляет определения типов TypeScript для API Node.js. Она нужна нам для использования глобальных переменных, поставляемых в среде выполнения Node.js.
  • fastify-tsconfig ([3]) предоставляет предварительно настроенную конфигурацию TypeScript для использования с фреймворком Fastify. Мы можем расширить нашу конфигурацию из нее и иметь удобные настройки по умолчанию уже из коробки.
  • tsx ([4]) добавляет инструмент выполнения TypeScript для наблюдения и повторного запуска сервера при изменении файлов. Он построен на базе Node.js и имеет политику нулевой конфигурации.
  • Наконец, зависимость разработки typescript ([5]) добавляет компилятор TypeScript для проверки определений типов и компиляции проекта в JavaScript. Мы добавим файл tsconfig.json в корень проекта, чтобы он работал правильно.

Перейдя в секцию scripts файла package.json, мы получим следующее:

  • build ([6]) — это скрипт, который удаляет существующую папку dist и вызывает компилятор TypeScript (tsc).
  • Сценарий dev ([7]) запускает инструмент командной строки tsx для повторного запуска приложения при внесении изменений в файлы проекта. Запуск файлов TypeScript напрямую удобен во время разработки, поскольку позволяет ускорить цикл разработки.

Мы готовы создать файл tsconfig.json в корневой папке проекта. Этот файл конфигурации превратит наш Node.js-проект в TypeScript-проект.

Добавление файла tsconfig.json

Файл tsconfig.json является конфигурационным файлом для компилятора TypeScript, и он предоставляет возможность указать опции и настройки, которые управляют тем, как код компилируется в JavaScript. По этой причине, как мы видели в предыдущем разделе, команда Fastify поддерживает пакет fastify-tsconfig npm с рекомендуемой конфигурацией для плагинов и приложений Fastify, написанных на TypeScript.

В фрагменте кода tsconfig.json мы можем увидеть, как его использовать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "extends": "fastify-tsconfig", // [1]
    "compilerOptions": {
        "outDir": "dist", // [2]
        "declaration": false, // [3]
        "sourceMap": true // [4]
    },
    "include": [
        // [5]
        "src/**/*.ts"
    ]
}

Давайте проанализируем конфигурацию:

  • Во-первых, мы используем свойство extends ([1]) для расширения из fastify-tsconfig. Этот пакет предоставляет рекомендуемую конфигурацию для веб-приложений Fastify, построенных на TypeScript.
  • compilerOptions ([2]) настраивает компилятор TypeScript на размещение скомпилированных JavaScript-файлов в директории dist. По этой причине ранее мы настроили точку входа приложения на dist/server.js, используя поле main в файле package.json.
  • Поскольку мы разрабатываем приложение, наш код будет выполняться, а не потребляться как библиотека. Поэтому мы устанавливаем опцию declaration в false ([3]), поскольку нам не нужно, чтобы компилятор генерировал файлы определения типов (*.d.ts).
  • С другой стороны, мы хотим, чтобы компилятор генерировал файлы карт исходного кода (*.map), которые сопоставляют скомпилированный JavaScript-код с исходным кодом TypeScript ([4]). Это полезно для понимания ошибок во время выполнения и отладки, поскольку позволяет нам устанавливать точки останова и переходить к исходному коду TypeScript.
  • Наконец, при компиляции исходного кода мы хотим включить все файлы с расширением .ts в папку src и ее подпапки ([5]).

Используя файл tsconfig.json, разработчики могут гарантировать, что все члены команды используют одни и те же параметры конфигурации, обеспечивая стандартизированный способ настройки компилятора TypeScript на разных машинах.

Параметры компилятора TypeScript

TypeScript предлагает широкий спектр опций компилятора, которые можно указать в файле tsconfig.json для управления поведением компилятора TypeScript. Эти опции включают в себя такие параметры, как версия целевого вывода, стратегия разрешения модулей, генерация карты исходников и генерация кода. Команда Fastify предлагает конфигурацию, которая подходит для большинства проектов. Более подробную информацию обо всех опциях можно найти в официальной TypeScript documentation.

Добавление точки входа приложения

Сначала нам нужно написать точку входа для нашего приложения. Обычный сервер Fastify с плагином автозагрузки загрузит наши маршруты и выполнит эту работу. Код прост, и мы можем посмотреть на него в следующем фрагменте:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { join } from 'node:path';
import Fastify from 'fastify';
import AutoLoad from '@fastify/autoload';
const fastify = Fastify({ logger: true }); // [1]
void fastify.register(AutoLoad, {
    // [2]
    dir: join(__dirname, 'routes'),
});
fastify
    .listen({ host: '0.0.0.0', port: 3000 })
    .catch((err) => {
        fastify.log.error(err);
        process.exit(1); // [3]
    });

Прежде чем перейти к коду, не забудьте запустить npm i в корне проекта.

Теперь давайте проанализируем предыдущий фрагмент:

  • Этот код создает сервер Fastify с включенным логом ([1]). Поскольку мы находимся внутри файла TypeScript, система типов включена. Например, если мы наведем курсор на переменную fastify в редакторе VS Code, то увидим, что она имеет тип FastifyInstance. Более того, благодаря первоклассной поддержке языка TypeScript в Fastify, все полностью типизировано из коробки.
  • Далее регистрируется плагин, использующий AutoLoad для динамической загрузки маршрутов из каталога routes. Метод register возвращает объект Promise, но нас не интересует его возвращаемое значение. Используя void, мы явно указываем, что не хотим перехватывать или использовать возвращаемое значение объекта Promise, и запускаем метод только для получения побочных эффектов ([2]).
  • Затем запускается сервер на порту 3000 и прослушивается на предмет входящих запросов. Если при загрузке сервера возникает ошибка, он записывает ее в лог и завершает процесс с кодом ошибки ([3]).

Теперь, когда мы определили точку входа, мы можем позаботиться о плагине root.

Использование провайдеров типов Fastify

Провайдер типов Fastify — это пакет только для TypeScript, который упрощает определение JSON-схем, предоставляя аннотации типов и дженерики. Его использование позволяет Fastify выводить информацию о типах непосредственно из определений схем. Провайдеры типов позволяют разработчикам легко определять ожидаемые входные и выходные данные конечных точек API, автоматически проверять корректность типов во время компиляции и подтверждать данные во время выполнения.

Fastify поддерживает несколько провайдеров типов, таких как json-schema-to-ts и TypeBox. В проектах на TypeScript использование провайдера типов позволяет сократить объем кода, необходимого для проверки ввода, и снизить вероятность возникновения ошибок, связанных с некорректными типами данных. В конечном итоге это сделает ваш код более надежным, поддерживаемым и масштабируемым.

Для краткости в следующем примере мы рассмотрим только провайдер типов TypeBox. Однако, поскольку выбор провайдера типов зависит от ваших личных предпочтений, мы рекомендуем вам попробовать другие провайдеры типов, чтобы найти наиболее подходящий:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import {
    type FastifyPluginAsyncTypebox,
    Type,
} from '@fastify/type-provider-typebox'; // [1]
const plugin: FastifyPluginAsyncTypebox = async function (
    fastify,
    _opts
) {
    // [2]
    fastify.get(
        '/',
        {
            schema: {
                querystring: Type.Object({
                    name: Type.String({ default: 'world' }), // [3]
                }),
                response: {
                    200: Type.Object({
                        hello: Type.String(), // [4]
                    }),
                },
            },
        },
        (req) => {
            const { name } = req.query; // [5]
            return { hello: name }; // [6]
        }
    );
    fastify.post(
        '/',
        {
            schema: {
                body: Type.Object({
                    name: Type.Optional(Type.String()), // [7]
                }),
                response: {
                    200: Type.Object({
                        hello: Type.String(),
                    }),
                },
            },
        },
        async (request) => {
            const { name } = request.body; // [8]
            const hello =
                typeof name !== 'undefined' && name !== ''
                    ? name
                    : 'world';
            return { hello }; // [9]
        }
    );
};
export default plugin;

Сниппет кода показывает плагин Fastify, который использует @fastify/type-provider-typebox для определения и проверки формы объектов запроса и ответа маршрутов.

Ниже приводится описание того, что делает этот код:

  • Во-первых, мы импортируем FastifyPluginAsyncTypebox и Type из модуля @fastify/type-provider-typebox ([1]). FastifyPluginAsyncTypebox — это псевдоним типа для FastifyPluginAsync, который внедряет поддержку определений схем @sinclair/typebox.
  • Плагин определяется как async функция, принимающая два аргумента: fastify и _opts. Благодаря явной аннотации типа FastifyPluginAsyncTypebox ([2]), этот экземпляр fastify будет автоматически определять типы схем маршрутов.
  • Метод fastify.get() определяет маршрут GET по корневому URL (/). Мы используем импортированный ранее объект Type для создания объекта querystring, определяющего свойство name типа string, содержащее «world» в качестве значения по умолчанию ([3]). Кроме того, мы снова используем его, чтобы задать ответ в виде объекта с единственным свойством hello типа string ([4]). Для обоих типов автоматически будут определены типы TypeScript внутри обработчика маршрута.
  • При наведении курсора на переменную name ([5]) в VS Code будет показан тип string. Такое поведение происходит благодаря провайдеру типов.
  • Обработчик маршрута возвращает объект с единственным свойством hello, установленным на значение свойства name, извлеченного из объекта querystring ([6]). Тип возвращаемой функции также определяется благодаря TypeBox. В качестве упражнения мы можем попробовать изменить возвращаемый объект на { hello: 2 }, и компилятор TypeScript выдаст ошибку, поскольку мы присвоили число вместо строки.
  • Метод fastify.post() вызывается для определения POST-маршрута по корневому URL (/). Схема маршрута включает объект body, который определяет необязательное свойство name типа string ([7]). Благодаря этому объявлению объект request.body в обработчике маршрута снова является полностью типизированным ([8]). На этот раз мы объявили свойство request.body.name необязательным. Прежде чем использовать его в возвращаемом объекте, нам нужно проверить, не является ли оно undefined, и в противном случае установить его в строку world ([9]). Как мы видели для другого обработчика маршрутов, возврат значения, несовместимого с объявлением схемы, приведет к ошибке компиляции.

На этом мы завершаем этот раздел. Благодаря провайдерам типов мы можем быстро достичь безопасности типов во всей кодовой базе, следуя этим указателям и не прибегая к явному объявлению типов:

  • Во время выполнения схема JSON будет дезинфицировать входные данные и сериализовать выходные, делая наши API более надежными, безопасными и быстрыми.
  • Во время компиляции наша кодовая база полностью проверяется на типы. Кроме того, каждая переменная из объявлений схемы имеет выведенные типы, что позволяет нам найти больше ошибок до развертывания приложения.

В следующем разделе мы рассмотрим, как можно автоматически сгенерировать сайт документации, соответствующий спецификации Swagger/OpenAPI.

Генерация документации API

Спецификация OpenAPI — это широко распространенный и открытый стандарт для документирования RESTful API. Она предоставляет формат для описания структуры и операций API в машиночитаемом формате, что позволяет разработчикам быстро понять API и взаимодействовать с ним.

Спецификация определяет набор файлов JSON или YAML, которые описывают конечные точки API, параметры, ответы и другие детали. Эта информация может быть использована для создания документации по API, клиентских библиотек и других инструментов, облегчающих работу с API.

Спецификации Swagger и OpenAPI

Swagger и OpenAPI — это две родственные спецификации, причем OpenAPI является более новой версией Swagger. Изначально Swagger был проектом с открытым исходным кодом, но позже спецификация была приобретена компанией SmartBear и переименована в OpenAPI. Сегодня спецификация поддерживается инициативой OpenAPI, консорциумом лидеров индустрии. Swagger также известен как OpenAPI v2, в то время как под OpenAPI обычно подразумевается только v3.

Fastify поощряет разработчиков определять схемы JSON для каждого маршрута, который они регистрируют. Было бы здорово, если бы существовал автоматический способ преобразования этих определений в спецификацию Swagger. И, конечно же, он есть. Но сначала мы должны добавить две новые зависимости в наш проект и использовать их в точке входа приложения. Итак, давайте установим плагины @fastify/swagger и @fastify/swagger-ui Fastify через терминальное приложение. Для этого в корне проекта введите следующую команду:

1
$> npm install @fastify/swagger @fastify/swagger-ui

Теперь мы можем зарегистрировать два новых пакета в экземпляре Fastify в файле src/server.ts. Оба пакета поддерживают спецификации Swagger и OpenAPI v3. Мы можем выбрать, какой из них генерировать, передав определенную опцию. Следующий фрагмент настраивает плагин на генерацию спецификации Swagger (OpenAPI v2) и сайта документации:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { join } from 'node:path';
import Fastify from 'fastify';
import AutoLoad from '@fastify/autoload';
import Swagger from '@fastify/swagger';
import SwaggerUI from '@fastify/swagger-ui';
const fastify = Fastify({ logger: true });
void fastify.register(Swagger, {
    swagger: {
        // [1]
        info: {
            // [2]
            title: 'Hello World App Documentation',
            description: 'Testing the Fastify swagger API',
            version: '1.0.0',
        },
        consumes: ['application/json'],
        produces: ['application/json'], // [3]
        tags: [
            {
                name: 'Hello World', // [4]
                description:
                    'You can use these routes to salute whomever you want.',
            },
        ],
    },
});
void fastify.register(SwaggerUI, {
    routePrefix: '/documentation', // [5]
});
// ... omitted for brevity

Этот фрагмент настраивает плагины swagger и swagger-ui для генерации определений спецификации и сайта документации. Вот разбивка кода:

  • Плагин @fastify/swagger зарегистрирован. Мы передаем свойство swagger для генерации спецификаций для OpenAPI v2 ([1]).
  • Мы определяем общую информацию о нашем API внутри объекта swagger, такую как его название, описание и версия, передавая их в свойство info ([2]). swagger-ui будет использовать это для создания сайта с более подробной информацией.
  • Мы определяем массивы consumes и produces ([3]), чтобы указать ожидаемые типы содержимого запроса и ответа. Эта информация важна для пользователей API, и она поможет при тестировании конечных точек.
  • Мы определяем массив tags для группировки конечных точек API по тематике или функциональности. В данном случае существует только один тег с именем Hello World ([4]). В следующем фрагменте src/routes/root.ts мы увидим, как группировать уже определенные маршруты.
  • Наконец, мы регистрируем плагин @fastify/swagger-ui, вызывая fastify.register(SwaggerUI, {...}). Сгенерированная документация может быть доступна через веб-браузер по URL, указанному в routePrefix ([5]) (в данном случае /documentation).

Далее мы изменим исходные определения маршрутов, чтобы улучшить автогенерируемую документацию. Мы хотим сделать это для лучшей группировки маршрутов в интерфейсе и более точных описаний.

В следующем фрагменте мы опустим те части, которые не относятся к делу, но полный код вы можете найти в файле src/routes/root.ts в репозитории GitHub:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import {
    type FastifyPluginAsyncTypebox,
    Type,
} from '@fastify/type-provider-typebox';
const plugin: FastifyPluginAsyncTypebox = async function (
    fastify,
    _opts
) {
    fastify.get('/', {
        schema: {
            tags: ['Hello World'], // [1]
            description: 'Salute someone via a GET call.', // [2]
            summary: 'GET Hello Route', // [3]
            querystring: Type.Object({
                name: Type.String({
                    default: 'world',
                    description:
                        'Pass the name of the person you want to salute.', // [4]
                }),
            }),
        },
        // ... omitted
    });
    fastify.post('/', {
        schema: {
            tags: ['Hello World'],
            description: 'Salute someone via a POST call.',
            summary: 'POST Hello Route',
            body: Type.Object(
                {
                    name: Type.Optional(Type.String()),
                },
                {
                    description:
                        'Use the name property to pass the name of the person you want to salute.',
                }
            ),
            // ... omitted
        },
    });
};

Несмотря на то, что мы показали дополнения к коду для обоих маршрутов, мы разберем только первый, потому что структура второго такая же:

  • Свойство tags ([1]) указывает, что маршрут относится к тегу Hello World, который мы определили при регистрации плагина @fastify/swagger. Это свойство позволяет группировать связанные маршруты вместе в документации Swagger/OpenAPI.
  • Поле description кратко описывает, что делает маршрут ([2]). Оно будет отображаться в верхней части документации Swagger.
  • В поле ummary кратко описывается, что делает маршрут ([3]). Оно будет отображаться рядом с определением маршрута в документации.
  • Чтобы лучше понять параметры, принимаемые конечной точкой, мы можем добавить специальное описание ([4]). Оно будет отображаться в документации Swagger в деталях параметров.

Чтобы проверить все, что мы добавили в этом разделе, мы можем запустить наш сервер в режиме разработки (npm run dev) и с помощью браузера перейти по адресу http://localhost:3000/documentation. Перед нами откроется симпатичный веб-сайт, по которому можно перейти, чтобы узнать больше о разработанном нами приложении. Более того, на странице также интегрирован клиент, который мы можем использовать для осуществления реальных вызовов нашего API.

Резюме

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

Поздравляем вас с тем, что вы добрались до заключительной главы этой книги! На протяжении всего этого путешествия мы узнали о веб-фреймворке Fastify и о том, как он может помочь нам создавать высокопроизводительные веб-приложения.

Комментарии