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

Структура проекта и управление конфигурацией

Начиная с этой главы, мы будем создавать реальное RESTful облачное нативное приложение, начиная с начальной структуры проекта. Больше никаких примеров foo/bar и теории Fastify. Мы будем применять на практике то, что узнали в предыдущих главах. Это приведет нас к пониманию того, как построить приложение.

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

Именно этот путь обучения мы рассмотрим в этой главе:

  • Проектирование структуры приложения
  • Улучшение структуры приложения
  • Отладка приложения
  • Совместное использование конфигурации приложения плагинами
  • Использование плагинов Fastify

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

Как уже упоминалось в предыдущих главах, вам понадобится следующее:

  • Работающая установка Node.js 18.
  • VS Code IDE.
  • Рабочая установка Docker.
  • Репозиторий Git — настоятельно рекомендуется, но не является обязательным.
  • Рабочая командная оболочка.

Все фрагменты в этой главе находятся на GitHub.

Проектирование структуры приложения

В этой главе мы спроектируем внутреннее приложение, которое будет открывать некоторые RESTful API.

Структура приложения позволяет писать приложения, которые легко внедрять, развивать и поддерживать. Хорошая система должна быть гибкой к вашим потребностям и изменениям в приложении. Кроме того, она должна предусматривать определенный дизайн реализации, чтобы позволить вам и вашей команде избежать основных «подводных камней», которые могут привести к нестабильному и не поддающемуся тестированию приложению.

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

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

Важнейшим моментом здесь является понимание того, почему мы построим следующую структуру именно таким образом. Структура, которую мы рассмотрим, не является обязательной, и вы можете критически отнестись к предложенному дизайну. Мы считаем, что важно персонализировать приложение, чтобы адаптировать его к вашим собственным потребностям и предпочтениям.

Мы закончили с разговорами. Давайте начнем создавать наше приложение!

Настройка репозитория

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

1
2
3
mkdir fastify-app
cd ./fastify-app
npm init fastify

Выполнение этих команд создает новую папку fastify-app и запускает из нее команду npm init.

Команда init

Когда вы выполняете команду init, npm запускает модуль create-fastify, который вы можете найти в этом репозитории GitHub. Вы можете создать полномочия create-my-app для создания подмостков приложения, чтобы ускорить инициализацию проекта.

В результате вы увидите следующие файлы и каталоги:

  • package.json: Это точка входа в проект.
  • app.js: Это основной файл приложения. Он загружается первым.
  • plugins/: В этой папке хранятся пользовательские плагины. Она содержит несколько файлов примеров.
  • routes/: В этой папке хранятся конечные точки приложения. Она содержит несколько примеров конечных точек.
  • test/: Это папка, в которой мы пишем тесты нашего приложения.

Стартовое приложение готово к установке с помощью команды npm install. После установки вам могут пригодиться эти скрипты, которые уже настроены:

  • npm test: Этот скрипт запускает тест приложения-скаффолдинга.
  • npm start: Этот скрипт запустит ваше приложение.
  • npm run dev: Этот скрипт запустит ваше приложение в режиме разработки. Сервер автоматически перезагружает ваше приложение при каждом изменении файла.

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

Версионирование приложений

Чтобы начать создание реального приложения, необходимо установить систему управления версией (VCS). Это программное обеспечение позволяет нам версионировать исходный код и управлять изменениями. Для решения этой задачи следует использовать программу Git, поскольку она является стандартным программным обеспечением де-факто в технологической индустрии. Однако изучение Git не является целью этой книги. Чтобы узнать, как установить и использовать Git, ознакомьтесь с разделом Технические требования.

На данном этапе все команды, о которых мы упоминали в этом разделе, должны работать на вашем компьютере. Потратьте немного времени, чтобы попробовать режим разработки, отредактировав файл routes/root.js и добавив новый маршрут GET /hello, пока сервер работает!

Понимание структуры приложения

Структура приложения, которую мы создали до сих пор, обладает отличными возможностями из коробки. Она основана на некоторых из следующих столпов:

  • Она опирается на плагин fastify-cli для запуска приложения и предоставления режима разработчика.
  • Он использует преимущества плагина @fastify/autoload для автоматической загрузки всех файлов, содержащихся в папках plugins/ и routes/.

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

Плагин fastify-cli

Плагин fastify-cli интерфейс командной строки (CLI) помогает нам запустить наше приложение. Он используется в файле package.json. Свойство scripts использует команду fastify startс некоторыми опциями для создания файлаapp.js. Как вы заметили, файл app.js экспортирует типичный интерфейс плагина Fastify async function (fastify, opts). Файл загружается в CLI как обычный вызов app.register(), как мы видели в разделе Добавление экземпляра базового плагина главы 1. В данном случае мы не инстанцируем экземпляр корневого сервера Fastify и не вызываем метод listen. Все эти задачи решает fastify-cli, избавляя нас от кодового шаблона.

Более того, CLI улучшает процесс разработки, реализуя соответствующие настройки и опции:

  • Он добавляет изящное завершение работы, о котором мы читали в разделе Завершение работы приложения в главе 1.
  • По умолчанию он считывает корневой файл .env. Это файл ключ=значение, содержащий только строку. Он используется для описания параметров, которые будут считываться из настроек окружения операционной системы. Все эти переменные отображаются в объект Node.js process.env.
  • Он начинает прослушивать переменную окружения PORT для всех хостов.
  • Он принимает опцию --debug для запуска приложения в режиме debug для отладки вашего кода.
  • Он раскрывает флаг --options для настройки параметров сервера Fastify, поскольку мы не инстанцируем его напрямую.
  • Аргумент --watch включает автозагрузку сервера при изменении файла в проекте.
  • Аргумент --pretty-logs делает журналы вывода читаемыми в оболочке.

Подробную документацию по CLI вы можете найти в репозитории.

Мы собираемся настроить нашу установку fastify-cli в следующем разделе: Улучшение структуры приложения.

Плагин @fastify/autoload

Плагин autoload автоматически загружает плагины, найденные в директории, и настраивает маршруты в соответствии со структурой папок. Другими словами, если вы создадите новый файл routes/test/foo.js со следующим содержимым, будет объявлен новый маршрут GET /test/:

1
2
3
4
5
module.exports = async function (fastify, opts) {
    fastify.get('/', async function (request, reply) {
        return 'this is an example';
    });
};

Это поведение следует парадигме проектирования конвенция над конфигурацией. Ее фокус заключается в сокращении реализации для обеспечения желаемого поведения по умолчанию.

Используя плагин @fastify/autoload, вы можете рассматривать каждый файл как инкапсулированный контекст, где каждая папка составляет prefix плагина, как мы видели в Глава 2.

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

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

Улучшение структуры приложения

Мы становимся уверенными в нашей базовой структуре приложения, но она еще не готова. Мы можем улучшить ее, чтобы определить наши собственные правила и обеспечить лучший опыт разработчика. Сначала мы создадим прочный каркас проекта, сосредоточившись на общей картине. После этого мы завершим настройку Fastify.

Начало оптимального проекта

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

Файл README

Первое дополнение к нашему проекту — это файл README.md. Типичный файл readme знакомит новичков с существующим проектом, отвечая на ряд основных информационных вопросов, таких как:

  • Каковы требования к проекту? Нужна ли вам база данных или другие внешние ресурсы?
  • Как установить приложение? Какой менеджер пакетов и версию Node.js оно использует?
  • Как запустить приложение? Где можно найти недостающие данные (например, переменные окружения)?
  • Как разрабатывать приложение? Есть ли какие-то соглашения, которым должны следовать разработчики?
  • Как вы тестируете приложение? Требуются ли модульные или сквозные тесты?
  • Как вы развертываете приложение? Каковы процессы и URL-адреса окружения?
  • Что делать разработчику, если не хватает информации?

На этот набор вопросов может не быть ответов в начале проекта, но полезно отметить те, на которые нет ответов, и найти их в будущем.

Позитивные вибрации

Файл README оказывает множество других положительных эффектов на моральный дух команды. Прочитав его, разработчик почувствует себя продуктивным. Мы предлагаем вам прочитать следующую статью, если вы хотите узнать больше о важности файла README. Автор статьи — Том Престон-Вернер, сооснователь GitHub и многих других проектов с открытым исходным кодом.

Самое главное — поддерживать файл README в актуальном состоянии. Каждый читатель должен улучшать его, добавляя недостающие части или удаляя старые. Если этот описательный файл станет слишком большим, он станет практически нечитабельным. Поэтому подумайте о создании папки docs/, чтобы разделить его на более удобные для чтения части.

Линтер кода

Еще одним важным шагом, который вы должны рассмотреть, является использование линтера. Линтер — это программа, которая статически анализирует исходный код и предупреждает вас о возможных проблемах или опечатках, чтобы вы не тратили часы на отладку и добавление сообщений console.log.

Если вы не хотите выбирать линтер и настраивать его, мы предлагаем выбрать standard. Это линтер с нулевой конфигурацией, который можно установить, выполнив команду npm install standard --save-dev, и который готов к интеграции в пользовательские скрипты package.json, как показано ниже:

1
2
3
4
5
"scripts": {
    "lint": "standard",
    "lint:fix": "standard --fix",
    // file continues
}

Таким образом, мы сможем запустить npm run lint, чтобы проверить наш исходный код и получить обратную связь. Если результата не будет, значит, все в порядке! Запуск npm run lint:fix автоматически исправит ошибки, когда это возможно — это полезно для проблем с форматированием.

Обратите внимание, что мы можем интегрировать проверку lint в требования к проекту. Для этого нам нужно изменить скрипты package.json следующим образом:

1
2
"pretest": "npm run lint",
"test": "tap \"test/**/*.test.js\"",

Команда npm test автоматически выполнит скрипты pretest и posttest, если они присутствуют. Вам может быть полезно прочитать документацию по npm pre и post, чтобы выполнить дополнительные команды до и после выполнения скрипта.

Сборка контейнера

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

Мы не будем углубляться в тему контейнеров, поскольку она выходит за рамки этой книги. Полезно вникнуть в эту тему на примере безопасного контейнера, который вы увидите далее. Эта конфигурация готова для создания производственных контейнеров Node.js.

Чтобы собрать контейнер Docker, содержащий наше программное обеспечение, мы должны создать документ Dockerfile в корневом каталоге. Этот файл содержит инструкции по сборке образа нашего контейнера наиболее безопасным способом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
FROM node:18-alpine as builder
WORKDIR /build
COPY package.json ./
COPY package-lock.json ./
ARG NPM_TOKEN
ENV NPM_TOKEN $NPM_TOKEN
RUN npm ci --only=production --ignore-scripts
FROM node:18-alpine
RUN apk update && apk add --no-cache dumb-init
ENV HOME=/home/app
ENV APP_HOME=$HOME/node/
ENV NODE_ENV=production
WORKDIR $APP_HOME
COPY --chown=node:node . $APP_HOME
COPY --chown=node:node --from=builder /build $APP_HOME
USER node
EXPOSE 3000
ENTRYPOINT ["dumb-init"]
CMD ["npm", "start"]

В предыдущем фрагменте сценария определено, как Docker должен создать контейнер. Эти шаги описаны следующим образом:

  1. FROM: Это запускает многоступенчатую сборку нашего приложения из базового образа в Node.js и с установленным npm.
  2. ENV: Здесь задаются некоторые полезные переменные окружения, которые всегда будут установлены в контейнере.
  3. COPY: Копирует файлы package.json в папку контейнера.
  4. WORKDIR: Устанавливает текущий рабочий каталог, из которого будут выполняться последующие команды.
  5. RUN npm ci: Устанавливает зависимости проекта с помощью файла package-lock.
  6. COPY: Копирует исходный код приложения в контейнер.
  7. RUN apk: Устанавливает программу dumb-init в контейнер.
  8. USER: Устанавливает пользователя контейнера по умолчанию во время выполнения. Это пользователь с наименьшими привилегиями для обеспечения безопасности нашей производственной среды.
  9. EXPOSE, ENTRYPOINT и CMD: Они определяют внешний интерфейс контейнера и устанавливают запуск приложения в качестве команды по умолчанию при инициализации контейнера.

Этот файл представляет собой безопасный и полный дескриптор для создания контейнера приложений и является идеальной базовой основой для нашего проекта. Со временем он будет меняться по мере изменения логики запуска приложения.

Мы выбрали многоступенчатую сборку, потому что для успешной установки приложения вам может потребоваться предоставить некоторые секреты. Типичный пример — использование частного реестра npm. Эти секреты не должны храниться в Docker-образе приложения, иначе любой, кто получит доступ к Docker-образу, сможет слить токен npm и получить доступ к вашему приватному реестру npm. Вместо этого многоступенчатая сборка состоит из двух этапов:

  1. Создание образа builder, имеющего доступ к приватному реестру npm, и загрузка зависимостей приложения.
  2. Скопируйте зависимости из образа builder в образ приложения, а затем выбросьте образ builder и его секреты.

Наконец, чтобы использовать этот файл, необходимо выполнить команду docker build -t my-app, после чего начнется процесс сборки. Более подробно мы рассмотрим эту тему в Главе 10.

Папка test

Мы не должны забывать о тестировании нашего приложения. Напоминаем, что мы должны создать папку test/, содержащую все тесты приложения, которые мы реализуем в Глава 9. Однако сначала нам нужно разобраться со структурой нашего приложения. Это связано с тем, что реализация тестов зависит от реализации проекта. Только после того, как мы достигнем стабильного решения, мы сможем написать наши основные утверждения, такие как следующее:

  • Приложение запускается правильно
  • Конфигурации загружаются в правильном порядке

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

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

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

Управление директориями проекта

В настоящее время в скаффолдинге приложения есть две основные папки, загружаемые одинаковым образом плагином @fastify/autoload. Но эти папки вовсе не равнозначны.

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

Мы создадим новые папки проекта, чтобы определить окончательную структуру проекта и объявить, что они должны содержать. Каждая папка будет иметь свою собственную конфигурацию @fastify/autoload.

Папки представлены в порядке загрузки, и вы не должны забывать об этом при написании:

1
2
3
4
5
6
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'plugins'),
});
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'routes'),
});

Код сначала загрузит плагины, а затем маршруты. Если бы процесс был обратным, это привело бы к ошибкам. По этой причине следующие папки проекта представлены в порядке загрузки.

Загрузка плагинов.

Папка plugins/ должна содержать плагины, основанные на модуле fastify-plugin. С этим модулем мы познакомились в Главе 2.

В этом каталоге следует хранить все компоненты приложения, которые необходимо зарегистрировать следующим образом:

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

По этой причине хорошим подходом является редактирование настроек автозагрузки папки plugins следующим образом:

1
2
3
4
5
6
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'plugins'),
    ignorePattern: /.*.no-load\.js/,
    indexPattern: /^no$/i,
    options: Object.assign({}, opts),
});

С этой новой установкой свойство ignorePattern позволяет нам игнорировать все те файлы, которые заканчиваются именем .no-load.js. Эта опция позволяет с первого взгляда понять, что загружается, а что нет, что улучшает наглядность проекта. Обратите внимание, что шаблон не учитывает имя директории.

Настройте в соответствии с вашими предпочтениями

Если вам не нравится шаблон «no-load», вы можете изменить логику, установив значение свойства ignorePattern: /^((?!load\.js).)*$/, и загружать только файлы с суффиксом .load.js.

Свойство indexPattern вместо этого отключает плагин @fastify/autoload. По умолчанию, если каталог содержит файл index.js, он будет загружен только один, пропуская все остальные файлы. Это может быть нежелательным поведением, которое предотвращает опция indexPattern.

Наконец, свойство options позволяет нам предоставить объект конфигурации в качестве входных данных для загружаемых плагинов. В качестве примера возьмем файл plugins/support.js. Он экспортирует интерфейс module.exports = fp(async function (fastify, opts). Параметр options автозагрузки совпадает с аргументом opts. Таким образом, можно предоставить конфигурацию для всех плагинов. Более подробно мы рассмотрим этот аспект в разделе Загрузка конфигурации.

Загрузка схем

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

В эту папку мы добавим файл loader.js, у которого будет одна задача. Он должен добавить все JSON-схемы, которые понадобятся для нашего приложения:

1
2
3
4
5
const fp = require('fastify-plugin');
module.exports = fp(function (fastify, opts, next) {
    fastify.addSchema(require('./user-input-headers.json'));
    next();
});

Этот фрагмент кода на самом деле является плагином, но он может становиться все больше и больше. Выделение его из папки plugins/ позволяет нам избежать хаотичной, бесконечной прокрутки при навигации по кодовой базе.

Будет много схем для работы, потому что настоятельно рекомендуется определять схему для каждой части HTTP, которую нужно валидировать или сериализовать. Все HTTP-главы требуют различных типов валидации; например, поле id не должно вводиться в маршруте POST, но оно обязательно в маршруте PUT для обновления связанного ресурса. Попытка вписать общий объект схемы JSON в несколько частей HTTP может привести к неожиданным ошибкам валидации.

Существует автоматизация для загрузки схем в каталог. Поэтому нам нужно будет перечислить все файлы в текущей директории и запустить метод addSchema. Как это реализовать, мы увидим в Главе 7.

Для загрузки файла loader.js в наш проект используется плагин @fastify/autoload. Это может показаться убийством мухи кувалдой, но это хороший метод, который можно использовать для еще большего разделения наших схем. Зарегистрируйте плагин в файле app.js, как показано ниже:

1
2
3
4
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'schemas'),
    indexPattern: /^loader.js$/i,
});

Таким образом, плагин автозагрузки будет загружать только файлы loader.js, созданные в дереве каталогов. Таким образом, мы сможем сделать вложенные папки, например, следующую:

  • schemas/headers/loaders.js.
  • schemas/params/loader.js
  • schemas/body/loader.js.

Точно так же мы можем сделать больше подпапок для каждого HTTP-метода, и вы найдете оптимальную древовидную структуру, подходящую для вашего приложения. Мы обнаружили, что разделение схем по частям HTTP — лучший способ упорядочивания. Такое разделение еще больше ускоряет навигацию по источникам. Мы сможем создать несколько утилит, предназначенных для каждой части HTTP, например, несколько регулярных выражений для заголовков и сложные многократно используемые объекты для ввода тела.

Загрузка маршрутов

Папка routes/ содержит конечные точки приложения. Все файлы загружаются автоматически, что затрудняет разбиение кодовой базы на более мелкие части. На данном этапе файл utility.js будет загружен командой @fastify/autoload. Более того, определение файла index.js будет препятствовать загрузке других файлов, как мы видели ранее в разделе «Загрузка плагинов».

Лучшие правила, которые мы предлагаем применять, когда речь идет о папке routes/, следующие:

  • Загружайте в автозагрузку только те файлы, которые заканчиваются на *routes.js. Все остальные файлы в папке отбрасывайте. Игнорируемые файлы можно зарегистрировать вручную или использовать в качестве кода утилиты.
  • Мы не должны использовать модуль fastify-plugin в этой папке. Если он нам понадобится, мы должны остановиться и подумать, можно ли перенести этот код в папку plugins/.
  • Включите функцию autohook в @fastify/autoload.

Чтобы применить эти правила, нам нужно настроить плагин автозагрузки, как показано в следующем примере кода:

1
2
3
4
5
6
7
8
9
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'routes'),
    indexPattern: /.*routes(\.js|\.cjs)$/i,
    ignorePattern: /.*\.js/,
    autoHooksPattern: /.*hooks(\.js|\.cjs)$/i,
    autoHooks: true,
    cascadeHooks: true,
    options: Object.assign({}, opts),
});

Этот фрагмент кода вводит два новых параметра. Флаг autoHooks позволяет зарегистрировать несколько хуков для каждого файла routes.js. Параметр cascadeHooks также включает эту функцию для подкаталогов.

Приведем пример этой структуры, которую мы рассмотрим далее в Глава 8. Учитывая это дерево папок, файл authHooks.js экспортирует стандартный интерфейс плагина Fastify, но он должен настраивать только хуки жизненного цикла:

1
2
3
4
5
6
7
routes/
└─┬ users/
  ├── readRoutes.js
  ├── writeRoutes.js
  ├── authHooks.js
  └─┬ games/
    └── routes.js

В этом примере настроены некоторые хуки onRequest для проверки авторизации клиента. Итак, ожидаете ли вы, что routes/games/routes.js будет аутентифицирован?

Если вы ответили «да», то вам подойдет cascadeHooks: true. Мы думаем, что большинство из вас естественным образом обнаружит, что хуки, зарегистрированные как autoHooks в родительской папке, добавляются в дочерние.

Если ответ отрицательный, вы можете изменить опцию cascadeHooks на false, чтобы не добавлять хуки в дочерние папки. В этом случае хуки будут загружены только для файлов readRoutes.js и writeRoutes.js. Возможно, вам придется продублировать файл authHooks.js для каждой папки, для которой вам нужна аутентификация, или зарегистрировать его как plugins/ в корневом контексте.

Все эти правила должны быть перечислены в файле README.md, чтобы прокричать о том, как разрабатывать новый набор маршрутов. Это поможет вашей команде присоединиться к проекту без детального изучения всех плагинов Fastify.

Теперь у нас есть четкая структура, готовая принять конечные точки бизнес-логики. Тем не менее, прежде чем двигаться дальше, нам нужно разобраться с последним аспектом структуры репозитория: как управлять конфигурацией.

Загрузка конфигурации

Конфигурация — это первый шаг, который должно выполнить наше приложение для корректного запуска. В разделе Понимание типов конфигурации главы 1 мы обсудили три типа конфигурации, которые необходимы нашему приложению:

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

Эти конфигурации имеют разные источники: опция сервера — это объект, конфигурации плагинов — сложные объекты, а конфигурация приложения зависит от окружения. По этой причине их нужно обрабатывать по-разному.

Загрузка конфигурации сервера.

Инстанцирование сервера не находится под нашим контролем. За нас это делает плагин fastify-cli, поэтому нам нужно настроить его, чтобы задать опции сервера.

Мы должны отредактировать скрипты start и dev в нашем файле package.json:

1
2
"start": "fastify start -l info --options app.js",
"dev": "npm run start -- --watch --pretty-logs"

Мы модифицировали скрипт dev для выполнения скрипта start, чтобы уменьшить дублирование кода и избежать ошибок copy-and-paste. Двойное тире (--) позволяет нам передавать дополнительные аргументы предыдущей команде. Таким образом, это похоже на добавление параметров к скрипту start.

Добавление флага --option в скрипт start равносильно добавлению его в обе команды без дублирования.

Флаг --option использует свойство app.js options. Не забудем добавить опции сервера, которые мы хотим предоставить при инициализации Fastify, и поместить их в нижнюю часть файла:

1
2
3
4
5
6
7
module.exports.options = {
    ajv: {
        customOptions: {
            removeAdditional: 'all',
        },
    },
};

При этом мы экспортируем тот же объект JSON, который мы предоставляли фабрике Fastify. Перезапуск сервера загрузит эти настройки. Внимательный разработчик может заметить, что мы не настроили опции logger, но мы можем видеть логи в нашей консоли. Это происходит потому, что наши настройки объединены в аргументах fastify-cli, а опция -l info устанавливает уровень журнала на info.

Централизация всех настроек в одном месте — лучшая практика, поэтому удалите аргумент -l из скрипта package.json и добавьте обычную конфигурацию logger в экспортируемый JSON.

В целях централизации мы можем переместить app.js module.exports.options в новую выделенную папку configs/server-options.js. Серверная опция не нуждается в асинхронной загрузке и может читать объект process.env для доступа ко всем значениям файла .env, загружаемым при запуске fastify-cli.

Загрузка конфигурации приложения

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

Мы сосредоточимся на одном основном типе загрузки: переменной окружения. Это самый распространенный тип, и он позволяет нам использовать Secret Managers. Чтобы углубиться в понимание внешних Secret Managers, мы предлагаем вам прочитать эту исчерпывающую статью. В ней рассказывается, как загрузить конфигурацию от самых известных провайдеров, таких как AWS, Google Cloud Platform и Hashicorp Vault.

Конфигурация среды привязана к системе, на которой работает программное обеспечение. В нашем случае это может быть наш компьютер, удаленный сервер или компьютер коллеги. Как уже говорилось ранее, Node.js по умолчанию загружает все переменные окружения операционной системы (ОС) в объект process.env. Поэтому работа над несколькими проектами может быть неудобной, так как каждый раз придется менять конфигурацию ОС. Создание Replace с текстовым файлом .env в корневой папке проекта является решением этой неприятной проблемы:

1
2
3
NODE_ENV=development
PORT=3000
MONGO_URL=mongodb://localhost:27017/test

Этот файл будет прочитан при запуске, и он будет готов к доступу. Обратите внимание, что этот файл перезапишет свойство process.env, если оно уже существует. Поэтому вы должны быть уверены, что то, что находится в вашем файле .env, является источником истины для конфигурации приложения. Вы можете проверить правильность загрузки файла app.js, добавив простой журнал:

1
2
3
module.exports = async function (fastify, opts) {
  fastify.log.info('The .env file has been read %s',
  process.env.MONGO_URL)

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

Сохранение конфиденциальных данных в репозитории опасно тем, что тот, кто имеет к ним доступ, может получить доступ к верхним окружениям, таким как production. Таким образом, безопасность переносится с доступа к среде, например серверу, на настройки Git-репозитория. Более того, если необходимо обновить переменную окружения, следует зафиксировать ее и опубликовать новую версию ПО, чтобы развернуть изменения в других окружениях. Это неправильно: программное обеспечение не должно быть привязано к значениям переменных окружения. Не забывайте отслеживать все файлы, которые должны быть секретными, в файлах .gitignore и .dockerignore.

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

Прежде всего, нам нужна JSON-схема, описывающая наш файл .env. Поэтому мы можем создать файл schemas/dotenv.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
    "type": "object",
    "$id": "schema:dotenv",
    "required": ["MONGO_URL"],
    "properties": {
        "NODE_ENV": {
            "type": "string",
            "default": "development"
        },
        "PORT": {
            "type": "integer",
            "default": 3000
        },
        "MONGO_URL": {
            "type": "string"
        }
    }
}

Среда схемы JSON довольно линейна. Она определяет свойство для каждой переменной, которую мы ожидаем. Мы можем задать значение по умолчанию и принудительно указать тип, как мы это сделали для свойства PORT в схеме JSON. Еще один приятный момент — формат $id. Он имеет синтаксис Uniform Resource Name (URN). Спецификация, рассмотренная в главе 5, объясняет, как он может быть Uniform Resource Identifier (URI). URI может быть URL, определяющим местоположение, или URN, когда он определяет имя ресурса, не указывая, где его можно получить.

Теперь мы не должны забыть обновить файл schemas/loader.js, написав fastify.addSchema(require('./dotenv.json')) для загрузки схемы.

Чтобы интегрировать плагин @fastify/env, мы создадим первый плагин нашего приложения, plugins/config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const fp = require('fastify-plugin');
const fastifyEnv = require('@fastify/env');
module.exports = fp(
    function (fastify, opts, next) {
        fastify.register(fastifyEnv, {
            confKey: 'secrets',
            schema: fastify.getSchema('schema:dotenv'),
        });
        next();
    },
    { name: 'application-config' }
);

Плагин будет загружен функцией автозагрузки, поэтому нам нужно запустить сервер и опробовать его. Запуск сервера без свойства MONGO_URL остановит запуск и сообщит, что ключ отсутствует. Более того, он добавит декоратор к экземпляру Fastify, назвав значение confKey. Таким образом, ключи .env будут доступны для чтения свойства fastify.secrets, отделяя код от глобального объекта process.env.

Прежде чем углубляться в загрузку конфигурации плагинов, стоит обратить внимание на опцию name, которую мы только что задали в качестве входного параметра для функции fp. Она станет ключом к пониманию раздела «Загрузка конфигураций плагинов».

Загрузка конфигураций плагинов

Настройки плагина зависят от конфигурации приложения. Он требует секретов приложения, чтобы настроить себя на работу так, как ожидается, но как мы можем настроить его?

Прежде чем приступить к работе, вам понадобится экземпляр MongoDB, запущенный в вашей среде разработки. Мы будем использовать эту базу данных в последующих главах. Вы можете скачать community edition бесплатно или использовать временный Docker-контейнер, запустив его с помощью следующей команды:

1
2
docker run -d -p 27017:27017 –rm –name fastify-mongo mongo:5
docker container stop fastify-mongo

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

Отслеживайте все команды

Лучше всего хранить все полезные команды в свойстве скриптов package.json для корректного запуска проекта. Таким образом, независимо от того, выберете ли вы Docker-контейнер или локальную установку MongoDB, вы сможете запустить npm run mongo:start, чтобы получить работающий экземпляр, готовый к использованию.

После настройки MongoDB давайте интегрируем плагин @fastify/mongodb. Он предоставляет доступ к базе данных MongoDB для использования в конечных точках приложения. Его нужно установить, выполнив команду npm install @fastify/mongodb, а затем создать новый файл plugins/mongo-data-source.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const fp = require('fastify-plugin');
const fastifyMongo = require('@fastify/mongodb');
module.exports = fp(
    async function (fastify, opts) {
        fastify.register(fastifyMongo, {
            forceClose: true,
            url: fastify.secrets.MONGO_URL,
        });
    },
    {
        dependencies: ['application-config'],
    }
);

В приведенном фрагменте кода конфигурация MongoDB содержится в файле самого плагина, но для корректной загрузки ему требуется конфигурация приложения. Это обеспечивается опцией dependencies. Она представляет собой аргумент функции fp, в котором перечислены все плагины, которые были загружены ранее. Как видите, параметр name, который мы задали в предыдущем разделе «Загрузка конфигурации приложения», дает нам контроль над порядком загрузки плагинов.

Мы создаем прочную и четкую структуру проекта, которая поможет нам в повседневной работе:

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

Можем ли мы использовать преимущества архитектуры Fastify в рамках этой структуры? Если что-то пойдет не так, как мы сможем справиться с ошибками? Давайте посмотрим на это дальше.

Отладка вашего приложения

В реальном приложении могут возникать ошибки! Как же мы можем справиться с ними в нашей структуре? Да, вы правильно догадались, с помощью плагина!

В производстве файлы журналов являются нашим отладчиком. Поэтому, прежде всего, важно писать хорошие и безопасные журналы. Давайте создадим новый файл плагина plugins/error-handler.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const fp = require('fastify-plugin');
module.exports = fp(function (fastify, opts, next) {
    fastify.setErrorHandler((err, req, reply) => {
        if (reply.statusCode >= 500) {
            req.log.error(
                { req, res: reply, err: err },
                err?.message
            );
            reply.send(`Fatal error. Contact the support team. Id
      ${req.id}`);
            return;
        }
        req.log.info(
            { req, res: reply, err: err },
            err?.message
        );
        reply.send(err);
    });
    next();
});

Этот фрагмент кода представляет собой настройку обработчика ошибок, о котором мы узнали в главе 3. Его приоритеты заключаются в следующем:

  • Зафиксировать ошибку в журнале приложения
  • Скрыть полезную информацию при возникновении неожиданной ошибки и предоставить полезную информацию вызывающему пользователю, а также эффективно связаться со службой поддержки.

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

После того как мы только что установили базовый уровень обработки ошибок, мы можем использовать отладчик IDE для обнаружения и решения проблем. Для этого нам нужно отредактировать скрипт package.json приложения:

1
"dev": "npm run start -- --watch –pretty-logs –debug"

Это запустит процесс Node.js в режиме отладки. При запуске мы прочитаем сообщение в консоли, которое сообщит нам, как подключить отладчик:

1
2
3
Debugger listening on ws://127.0.0.1:9320/da49367c-fee9-42ba-b5a2-
5ce55f0b6cd8
For help, see: https://nodejs.org/en/docs/inspector

Используя VS Code в качестве IDE, мы можем создать файл .vscode/launch.json, как показано ниже:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to Fastify",
            "type": "node",
            "request": "attach",
            "port": 9320
        }
    ]
}

Нажатие F5 подключит наш отладчик к процессу Node.js, и мы будем готовы установить точки останова и проверить, что происходит в нашем приложении, чтобы исправить это.

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

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

Совместное использование конфигурации приложения между плагинами

В разделе «Загрузка конфигураций плагинов» обсуждалось, как плагин может получить доступ к конфигурации приложения. В этом случае плагин обращается к обычному объекту fastify.secret для доступа к переменным окружения.

Конфигурация может развиваться и становиться более сложной. Но если бы вы просто хотели централизовать все настройки плагина в отдельном плагине, как бы вы могли это сделать?

Мы можем модифицировать плагин config.js и переместить его в директорию configs/. Таким образом, мы больше не будем загружать его автоматически. Затем мы можем интегрировать конфигурацию @fastify/mongodb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module.exports = fp(async function configLoader(
    fastify,
    opts
) {
    await fastify.register(fastifyEnv, {
        confKey: 'secrets',
        schema: fastify.getSchema('schema:dotenv'),
    });
    fastify.decorate('config', {
        mongo: {
            forceClose: true,
            url: fastify.secrets.MONGO_URL,
        },
    });
});

В фрагменте кода вы можете увидеть следующие основные изменения:

  • Плагин экспонирует интерфейс async.
  • Для выполнения плагина ожидается реестр плагина @fastify/env. Таким образом, fastify.secrets будет немедленно доступен.
  • К экземпляру Fastify добавлен новый декоратор.
  • У плагина больше нет параметра name. Поскольку мы собираемся загружать его вручную, имя не обязательно. В любом случае, это хорошая практика, чтобы оставить его: мы хотим показать вам, что мы ломаем мосты между файлами mongo-data-source.js и config.js.

Эти изменения нарушают нашу настройку по следующим причинам:

  • Файл config.js не загружается
  • Файл mongo-data-source.js полагается на fastify.secrets.

Чтобы исправить это, нам нужно отредактировать app.js, как показано ниже:

1
2
await fastify.register(require('./configs/config'));
fastify.log.info('Config loaded %o', fastify.config);

Эти строки должны быть добавлены после конфигурации схем автозагрузки, потому что мы проверяем переменную окружения через схему schema:dotenv.

После этого мы можем обновить параметры автозагрузки плагинов следующим образом:

1
2
3
4
5
6
7
fastify.register(AutoLoad, {
    dir: path.join(__dirname, 'plugins'),
    dirNameRoutePrefix: false,
    ignorePattern: /.*.no-load\.js/,
    indexPattern: /^no$/I,
    options: fastify.config,
});

Наконец, мы можем исправить файл mongo-data-source.js, удалив из него много кода:

1
2
3
module.exports = fp(async function (fastify, opts) {
    fastify.register(fastifyMongo, opts.mongo);
});

Как видите, он стал намного легче. Мы также убрали параметр dependencies, потому что не хотим обращаться к декоратору fastify.secret.

Это изменение существенно повлияло на логику кода. При таком рестайле кода файл mongo-data-source.js отделен от остальной части приложения, поскольку все настройки предоставляются входным аргументом opts. Этот объект предоставляется плагином @fastify/autoload, отображая параметр options.

Теперь у вас есть полные и прочные знания о конфигурации и о том, как лучше всего ею управлять. Вы можете использовать предыдущий пример кода, чтобы уверенно настраивать плагины и играть внутри плагина автозагрузки. Вы увидите, что исходный код в репозитории книги использует первое решение, которое мы рассмотрели в разделе «Загрузка конфигураций плагинов».

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

Использование плагинов Fastify

Структура проекта почти завершена. Экосистема Fastify помогает нам улучшить нашу кодовую базу строительных лесов с помощью набора плагинов, о которых вы захотите узнать. Давайте узнаем о них и добавим их в папку plugins/.

Как получить обзор проекта

Документирование полного списка всех конечных точек приложения — утомительная задача, но кто-то в команде все равно должен это делать. К счастью, у Fastify есть решение для этого: плагин @fastify/swagger.

Вы можете интегрировать его, создав новый файл plugins/swagger.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
module.exports = fp(
    async function (fastify, opts) {
        fastify.register(require('@fastify/swagger'), {
            routePrefix: '/docs',
            exposeRoute:
                fastify.secrets.NODE_ENV !== 'production',
            swagger: {
                info: {
                    title: 'Fastify app',
                    description: 'Fastify Book examples',
                    version: require('../package.json')
                        .version,
                },
            },
        });
    },
    { dependencies: ['application-config'] }
);

Предыдущий код зарегистрирует плагин. Он автоматически создаст веб-страницы http://localhost:3000/docs, на которых будут перечислены все конечные точки приложения. Обратите внимание, что документация будет опубликована только в том случае, если среда не находится в производстве, из соображений безопасности. Интерфейсы API должны быть доступны только тем людям, которые их используют.

Обратите внимание, что Swagger (OAS 2.0) и бывшая OpenAPI Specification (OAS 3.0) определяют стандарт для создания документации API. Возможно, вам будет интересно узнать больше об этом.

Как быть доступным

Одной из самых распространенных проблем при реализации API бэкенда является настройка кросс-оригинального обмена ресурсами (CORS). Вы столкнетесь с этой проблемой, когда фронтенд попытается вызвать ваши конечные точки из браузера, а запрос будет отклонен.

Чтобы решить эту проблему, вы можете установить плагин @fastify/cors:

1
2
3
4
5
6
const fp = require('fastify-plugin');
module.exports = fp(async function (fastify, opts) {
    fastify.register(require('fastify-cors'), {
        origin: true,
    });
});

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

Резюме

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

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

Прочная и чистая структура, которую мы создали на данный момент, будет развиваться на протяжении всей книги. Прежде чем уделять больше времени структуре, нам нужно понять бизнес-логику приложения. Итак, приготовьтесь к следующей главе, в которой мы обсудим, как построить RESTful API.

Комментарии