Создание RESTful API¶
В этой главе мы будем опираться на структуру приложения, созданную в предыдущей главе, и погрузимся в написание основных частей нашего приложения.
Мы начнем с определения маршрутов нашего приложения, а затем перейдем к подключению к источникам данных. Мы также реализуем необходимую бизнес-логику и научимся решать сложные повседневные задачи, с которыми мы можем столкнуться при разработке реального приложения Fastify.
Глава будет разделена на несколько основных разделов, начиная с определения маршрутов, затем подключения к источникам данных, реализации маршрутов, защиты конечных точек и применения принципа Не повторяйся (DRY) для повышения эффективности нашего кода.
К концу этой главы мы узнаем следующее:
- Как объявлять и реализовывать маршруты с помощью плагинов Fastify
- Как добавлять JSON-схемы для защиты конечных точек
- Как загружать схемы маршрутов
- Как использовать декораторы для реализации паттерна DRY
Технические требования
Чтобы следовать этой главе, вам понадобятся именно эти технические требования, упомянутые в предыдущих главах:
- Работающая установка Node.js 18
- VS Code IDE
- Активная установка Docker
- Репозиторий Git — рекомендуется, но не является обязательным.
- Рабочая командная оболочка
Все фрагменты кода для этой главы доступны на GitHub.
Итак, давайте приступим к работе и создадим надежное и эффективное приложение, которое можно будет использовать в качестве эталона для будущих проектов!
Конспект приложения¶
В этом разделе мы начнем создавать RESTful-приложение для работы с делами. Приложение позволит пользователям выполнять операции Создание, Чтение, Обновление и Удаление (CRUD) над своим списком дел, используя такие HTTP-методы, как GET
, POST
, PUT
и DELETE
. Помимо этих операций, мы реализуем одно настраиваемое действие для пометки задач как «выполненных».
Что такое RESTful?
Передача состояния представления (RESTful) — это архитектурный стиль для создания веб-сервисов, которые следуют четко определенным ограничениям и принципам. Это подход для создания масштабируемых и гибких веб-интерфейсов, которые могут использовать различные клиенты. В архитектуре RESTful ресурсы идентифицируются Uniform Resource Identifiers (URI). Операции, выполняемые с этими ресурсами, основаны на предопределенных методах HTTP (GET
, POST
, PUT
, DELETE
и т.д.). Каждый вызов API не имеет статических данных и содержит всю информацию, необходимую для выполнения операции.
Fastify — отличный выбор для разработки RESTful API, благодаря своей скорости, гибкости, масштабируемости и удобству для разработчиков. Кроме того, как мы видели в предыдущих главах, модульная архитектура плагинов позволяет легко добавлять и удалять функциональность по мере необходимости, а низкоуровневые оптимизации делают его надежным выбором для приложений с высоким трафиком. Наконец, мы воспользуемся преимуществами этой архитектуры, чтобы организовать нашу кодовую базу в масштабе, сделав каждый ее фрагмент независимым от других.
Определение маршрутов¶
Давайте начнем развивать приложение с нашей базовой структуры проекта, добавив новый плагин, определяющий наши RESTful-маршруты. Однако сейчас мы не будем реализовывать логику отдельных маршрутов, поскольку сначала нам нужно рассмотреть источник данных в предстоящем разделе Источник данных и модель.
Следующий фрагмент routes/todos/routes.js
определяет базовую структуру нашего плагина маршрутов:
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 |
|
Наш модуль exports ([1]
) представляет собой плагин Fastify под названием todoRoutes
. Внутри него мы определили шесть маршрутов, пять для основных CRUD-операций и одно дополнительное действие для пометки задач как выполненных. Давайте вкратце рассмотрим каждый из них:
listTodo GET /
: Реализует операцию List. Возвращает массив задач и общее количество элементов ([2]
).createTodo POST /
: Реализует операцию Создать. Она создает задачу из данных телаrequest
и возвращаетid
созданного элемента ([3]
).readTodo GET /:id
: Реализует операцию Чтение. Возвращает задачу, соответствующую параметру:id
([4]
).updateTodo PUT /:id
: Выполняет операцию Обновить. Она обновляет элемент дел, соответствующий параметру:id
, используя данные телаrequest
([5]
).deleteTodo DELETE /:id
: Реализует операциюDelete
. Она удаляет задачу, соответствующую параметру:id
([6]
).changeStatus POST /:id/:status
: Выполняет заказное действие. Оно помечает задачу как «выполненную» или «не выполненную» ([7]
).
Обратите внимание, что мы добавили имя к каждой функции-обработчику для ясности и в качестве лучшей практики, поскольку это помогает получить более качественные трассировки стека.
Теперь давайте посмотрим, как использовать этот модуль плагина в нашем приложении.
Регистрация маршрутов¶
Простое объявление плагина routes
не добавляет никакой ценности нашему приложению. Поэтому перед использованием нам необходимо его зарегистрировать. К счастью, у нас уже есть все необходимое из предыдущей главы для автоматической регистрации маршрутов. Следующий отрывок из файла apps.js
показывает важную часть:
1 2 3 4 5 6 7 8 9 10 11 |
|
Этот фрагмент кода использует плагин под названием @fastify/autoload
для автоматической загрузки маршрутов и хуков из указанной директории.
Мы указали папку routes
([1]
) в качестве пути, где находятся наши маршруты, а затем определили шаблон регулярного выражения ([2]
) для идентификации файлов маршрутов. Поэтому, чтобы Fastify выбрал наш предыдущий файл routes.js
, мы должны сохранить его в файле ./routes/todos/routes.js
.
Вам может быть интересно, почему мы добавили эту подпапку todos
в наш путь. У AutoLoad
есть еще одно замечательное поведение — она автоматически загружает все подпапки указанного пути, используя имя папки в качестве префикса пути для маршрутов, которые мы определяем. Наши обработчики будут иметь префикс todos
при регистрации в приложении Fastify. Эта возможность помогает нам организовать наш код в подпапках, не заставляя нас определять префикс вручную. Давайте сделаем пару вызовов маршрутов нашего приложения, чтобы привести несколько конкретных примеров.
Нам нужно открыть два терминала: первый — для запуска приложения, второй — для выполнения вызовов с помощью curl
.
В первом терминале перейдите в корень проекта и введите npm start
, как показано здесь:
1 2 3 |
|
Теперь, когда сервер запущен, мы можем оставить первый терминал открытым и перейти ко второму. Мы готовы к выполнению вызовов API:
1 2 3 4 |
|
В предыдущем фрагменте видно, что мы сделали два вызова. В первом мы успешно вызвали обработчик listTodo
, а во втором — readTodo
.
Источник данных и модель¶
Прежде чем реализовать логику обработчиков, нам нужно рассмотреть сохранение данных. Поскольку мы зарегистрировали плагин MongoDB внутри приложения в главе 6, Структура проекта и управление конфигурацией, у нас уже есть все необходимое для сохранения наших дел в реальной базе данных.
Благодаря системе плагинов Fastify мы можем использовать клиент базы данных внутри нашего плагина маршрута, поскольку экземпляр, который мы получаем в качестве первого аргумента, декорируем свойством mongo
. Кроме того, мы можем присвоить коллекцию 'todos'
локальной переменной и использовать ее в обработчиках маршрутов:
1 2 3 4 5 6 7 8 |
|
Теперь мы можем перейти к определению нашей модели данных. Даже если MongoDB является бессхемной базой данных и нам не нужно ничего определять заранее, мы набросаем простой интерфейс для задачи, которую нужно выполнить. Важно помнить, что нам не нужно добавлять этот фрагмент кода в наше приложение или базу данных. Мы показываем его здесь просто для наглядности:
1 2 3 4 5 6 7 8 |
|
Давайте посмотрим на свойства, которые мы только что определили:
_id
([1]
) иid
([2]
) имеют одинаковое значение. Мы добавляем свойствоid
, чтобы не раскрывать никакой информации о нашей базе данных. Свойство_id
определяется и используется в основном серверами MongoDB.- Свойство
title
([3]
) является настраиваемым пользователем и содержит название задачи. - Свойство
done
([4]
) сохраняет статус задачи. Задача завершена, если ее значение равноtrue
. В противном случае задача все еще находится в процессе выполнения. - Свойства
createdAt
([5]
) иmodifiedAt
([6]
) автоматически добавляются приложением для отслеживания времени создания и последнего изменения элемента.
Теперь, когда мы определили все, что нам нужно с точки зрения источника данных, мы можем перейти к реализации логики обработчиков маршрутов в следующем разделе.
Реализация маршрутов¶
До сих пор мы реализовывали наши обработчики как фиктивные функции, которые вообще ничего не делают. В этом разделе мы научимся сохранять, извлекать, изменять и удалять реальные задачи, используя MongoDB в качестве источника данных. Для каждого подраздела мы рассмотрим только один обработчик, зная, что он заменит тот же самый обработчик, который мы уже определили в ./routes/todos/routes.js
.
Уникальные идентификаторы
Этот раздел содержит несколько фрагментов кода и команд для выполнения в терминале. Важно помнить, что уникальные идентификаторы, которые мы здесь показываем, отличаются от тех, которые будут у вас при тестировании маршрутов. На самом деле, идентификаторы генерируются при создании задачи. Измените фрагменты команд соответствующим образом.
Мы начнем с createTodo
, поскольку наличие элементов, сохраненных в базе данных, поможет нам реализовать и протестировать другие обработчики.
createTodo¶
Как следует из названия, эта функция позволяет пользователям создавать новые задачи и сохранять их в базе данных. Следующий фрагмент кода определяет маршрут, который обрабатывает POST
запрос, когда пользователь переходит по пути /todos/
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
При вызове маршрута функция-обработчик ([1]
) генерирует новый уникальный идентификатор ([2]
) для элемента дел и устанавливает даты создания и модификации ([3]
) на текущее время. Затем обработчик создает новый объект дел из тела запроса ([4]
). Затем объект вставляется в базу данных с помощью коллекции todos
, которую мы создали в начале работы над плагином маршрутов ([5]
). Наконец, функция отправляет ответ с кодом состояния 201
([6]
), указывающим на то, что ресурс был создан, и телом, содержащим ID вновь созданного элемента.
Наконец, мы можем протестировать наш новый маршрут. Как обычно, мы можем использовать два терминальных окна и curl
для осуществления вызовов, передавая тело.
В первом терминале запустите сервер:
1 2 3 |
|
Теперь во втором случае мы можем использовать curl
для выполнения запроса:
1 2 3 |
|
Мы видим, что приложение вернуло id
только что созданного элемента. Поздравляем! Вы реализовали свой первый рабочий маршрут!
В следующем подразделе мы будем читать список задач из базы данных!
listTodo¶
Теперь, когда наш первый элемент сохранен в базе данных, давайте реализуем маршрут со списком. Он позволит нам вывести список всех задач с их общим количеством.
Мы можем начать непосредственно с выдержки из routes/todos/routes.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Внутри функции listTodo
объект запроса используется для извлечения параметров запроса ([1]
), таких как skip
, limit
и title
. Параметр title
используется для создания фильтра регулярных выражений для поиска элементов дел, названия которых частично совпадают с параметром title
([2]
). Если параметр title
не указан, то filter
будет пустым объектом, возвращающим все пункты.
Затем переменная data заполняется элементами дел, которые соответствуют filter
, путем вызова todos.find()
и передачи его в качестве параметра. Кроме того, передаются параметры запроса limit
и skip
, чтобы реализовать правильную распаковку ([3]
). Поскольку драйвер MongoDB возвращает курсор, мы преобразуем результат в массив с помощью метода toArray()
.
Пагинация
Пагинация — это техника, используемая в запросах к базам данных для ограничения количества результатов, возвращаемых запросом, и получения только определенного подмножества данных за один раз. Когда запрос возвращает большое количество результатов, может быть сложно отобразить или обработать их все сразу. При работе со списками элементов пагинация позволяет пользователям получать доступ и обрабатывать большие объемы данных более удобно и эффективно. В результате улучшается пользовательское восприятие и снижается нагрузка на приложение и базу данных, что приводит к повышению производительности и масштабируемости.
Переменная totalCount
вычисляется путем вызова todos.countDocuments()
с тем же объектом filter
, чтобы клиент API мог правильно реализовать пагинацию. Наконец, функция-обработчик возвращает объект, содержащий массив данных и число totalCount
([4]
).
Теперь мы снова можем вызвать маршрут, используя два экземпляра терминала и бинарник curl
, и ожидаем, что в ответе будет наш первый пункт дел.
В первом терминале запустите сервер:
1 2 3 |
|
Теперь во втором случае мы можем использовать curl
для выполнения запроса:
1 2 3 4 5 |
|
Мы видим, что все работает, как и ожидалось, и «моя первая задача»
является единственным элементом, возвращаемым в массиве data
. Кроме того, totalCount
правильно равно 1
.
Следующий маршрут, который мы реализуем, позволяет нам запрашивать один конкретный элемент.
readTodo¶
Этот RESTful-маршрут позволяет клиентам получить из базы данных один элемент дел, основываясь на его уникальном идентификаторе id
. Следующий фрагмент иллюстрирует реализацию функции-обработчика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Синтаксис /:id
в свойстве url
([1]
) указывает на то, что этот параметр маршрута будет заменен определенным значением, когда клиент вызовет этот маршрут. На самом деле, функция-обработчик сначала извлекает этот id
из объекта request.params
и создает из него новый ObjectId
, используя this.mongo.ObjectId()
([2]
). Затем он использует метод findOne
коллекции todos
для получения задачи с совпадающим _id
. Мы исключаем поле _id
из результата, используя опцию projection
, чтобы не разглашать информацию об используемом нами сервере базы данных ([3]
). На самом деле, MongoDB — единственный, кто использует поле _id
в качестве первичной ссылки.
Если подходящий элемент дел найден, он возвращается в качестве ответа ([5]
). В противном случае обработчик устанавливает код состояния HTTP на 404
и возвращает объект ошибки с сообщением о том, что задача не найдена ([4]
).
Чтобы протестировать маршрут, мы можем использовать обычный процесс. В дальнейшем мы не будем указывать терминал, на котором запущен сервер, и покажем только тот, который мы используем для вызова:
1 2 3 4 |
|
И снова все работает, как и ожидалось. Нам удалось передать ID задачи, которую мы добавили в базу данных, в качестве параметра маршрута и получить в качестве ответа задачу с заголовком «моя первая задача»
.
Пока что, если пользователь ошибется в названии, изменить его не получится. Об этом мы позаботимся в следующий раз.
updateTodo¶
Следующий фрагмент кода добавляет маршрут, который обрабатывает запросы PUT
для обновления задачи, уже сохраненной в базе данных:
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 |
|
Мы снова используем параметр :id
, чтобы определить, какой элемент пользователь хочет изменить ([1]
).
Внутри обработчика маршрута мы используем метод клиента MongoDB updateOne()
для обновления элемента дел в базе данных. Мы снова используем свойство request.params.id
, чтобы создать объект фильтрации для соответствия задаче с указанным _id
([2]
). Затем мы используем оператор $set
для частичного обновления элемента новыми значениями из request.body
. Мы также устанавливаем свойство modifiedAt
в текущее время ([3]
).
После завершения обновления проверяется свойство modifiedCount
результата, чтобы узнать, было ли обновление успешным ([4]
). Если ни один документ не был изменен, возвращается ошибка 404
. Если обновление прошло успешно, то выдается код состояния 204
, указывающий на успешное завершение обновления без возврата тела ([5]
).
Запустив сервер обычным способом, мы можем протестировать только что реализованный маршрут с помощью терминала и curl
:
1 2 3 |
|
На этот раз мы передаем аргумент -X
в curl
, чтобы использовать HTTP-метод PUT
. Затем в теле запроса мы изменяем название наших задач и передаем уникальный ID задачи в качестве параметра route. Одна вещь, которая может вызвать замешательство, — это то, что сервер не вернул тело запроса, но, глядя на возвращаемое значение updateTodo
, это не должно быть сюрпризом.
Мы можем проверить, правильно ли был обновлен элемент дел, вызвав маршрут readTodo
:
1 2 3 4 |
|
В ответе мы сразу видим обновленный заголовок и дату modifiedAt
, которая теперь отличается от createdAt
, сигнализируя о том, что элемент был обновлен.
В нашем приложении по-прежнему отсутствует функция удаления, поэтому пришло время исправить это. Следующий подраздел поможет преодолеть это ограничение.
deleteTodo¶
Следуя соглашениям RESTful, следующий фрагмент кода определяет маршрут Fastify, который позволяет пользователю удалить задачу, передавая ее уникальный :id
в качестве параметра запроса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Объявив HTTP-метод DELETE
, мы передаем параметр :id
в качестве пути маршрута, чтобы можно было определить, какой элемент нужно удалить ([1]
).
Внутри функции deleteTodo
мы создаем фильтр из свойства request.params.id
([2]
) и передаем его методу deleteOne
коллекции todos
для удаления задач с этим уникальным идентификатором. После возврата этого вызова мы проверяем, действительно ли элемент был удален из базы данных. Если ни один документ не был удален, обработчик возвращает ошибку 404
([3]
). С другой стороны, если удаление прошло успешно, мы возвращаем пустое тело с кодом состояния 204
, чтобы показать, что операция завершилась успешно ([4]
).
Тестирование вновь добавленного маршрута, как всегда, очень простое — мы используем тот же терминал и curl
, что и для предыдущих маршрутов.
Запустив сервер в одном терминале, мы выполняем следующую команду в другом:
1 2 3 |
|
Здесь мы выполняем два разных вызова. Первый удаляет сущность в базе данных и возвращает пустой ответ. Второй, напротив, проверяет, удалил ли предыдущий вызов ресурс. Поскольку он возвращает ошибку not found, мы уверены, что удалили его.
Ни одно приложение для составления списка дел не будет полным без возможности отмечать задачи как «выполненные» или перемещать их обратно в «прогресс», и именно это мы и добавим.
changeStatus¶
Это наш первый маршрут, который не следует принципам CRUD. Вместо этого он представляет собой пользовательскую логику, выполняющую определенную операцию над одной задачей. Следующий отрывок из routes/todos/routes.js
показывает действие POST
, которое при вызове помечает задачу как «выполненную» или «не выполненную», в зависимости от ее состояния. Это первый маршрут, в котором используются два разных параметра запроса:
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 |
|
Наш маршрут ожидает два параметра в URL — :id
, уникальный идентификатор элемента дел, и :status
, который указывает, должна ли текущая задача быть отмечена как «выполненная» или «не выполненная» ([1]
).
Функция-обработчик сначала проверяет значение параметра status
, чтобы определить новое значение свойства done
([2]
). Затем она использует метод updateOne()
для обновления свойств done
и modifiedAt
элемента в базе данных ([3]
). Если обновление прошло успешно, функция-обработчик возвращает ответ 204 No Content
([5]
). С другой стороны, если элемент не найден, функция-обработчик возвращает ответ 404 Not Found
с сообщением об ошибке ([4]
).
Прежде чем тестировать этот маршрут, нам нужно, чтобы в базе данных была хотя бы одна задача. Если необходимо, мы можем использовать маршрут createTodo
для ее добавления. Теперь мы можем протестировать реализацию с помощью curl
, как обычно:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
В выводе терминала мы устанавливаем свойство done
элемента в true
, передавая done
в качестве параметра :status
запроса. Затем мы вызываем маршрут GET
для одного элемента, чтобы проверить, эффективно ли операция изменяет статус. Затем, чтобы отменить процесс и пометить задачу как еще не выполненную, мы снова вызываем маршрут done
, передавая undone
в качестве параметра запроса статуса. Наконец, мы проверяем, что все работает так, как ожидалось, и снова вызываем обработчик readTodo
.
Этот последний маршрут завершает базовую функциональность нашего приложения для работы со списком дел. Однако мы еще не закончили. В следующем разделе мы узнаем больше о безопасности нашего приложения и о том, почему наша текущая реализация небезопасна по замыслу.
Защита конечных точек¶
До сих пор каждый объявленный нами маршрут не выполнял никакой проверки на вводимые пользователем данные. Это нехорошо, и мы, как разработчики, должны всегда проверять и обеззараживать входные данные API, которые мы предоставляем. В нашем случае все обработчики createTodo
и updateTodo
затронуты этой проблемой безопасности. Фактически, мы берем request.body
и передаем его прямо в базу данных.
Сначала, чтобы лучше понять суть проблемы, приведем пример того, как пользователь может внести нежелательную информацию в нашу базу данных с помощью нашей текущей реализации:
1 2 3 4 5 6 7 |
|
В предыдущем фрагменте терминала мы выполнили две команды curl
. В первой из них при создании элемента вместо того, чтобы передать только title
, мы также передаем свойство foo
. Посмотрев на возвращаемый результат, мы видим, что команда вернула ID созданной сущности. Теперь мы можем проверить, что сохранилось в базе данных, вызвав маршрут readTodo
. К сожалению, в выводе мы видим, что мы также сохранили "foo": "bar"
в базе данных. Как уже говорилось ранее, это проблема безопасности, и мы никогда не должны позволять пользователям писать напрямую в базу данных.
Есть еще одна проблема с текущей реализацией. Мы не прикрепили к нашим маршрутам схему сериализации ответов. Хотя это не так важно с точки зрения безопасности, они имеют решающее значение для пропускной способности нашего приложения. Заранее сообщая Fastify форму значений, которые мы возвращаем из наших маршрутов, мы помогаем ему быстрее сериализовать тело ответа. Поэтому мы всегда должны добавлять все схемы при объявлении маршрута.
В последующих разделах мы будем реализовывать только одну схему для каждого типа, чтобы сделать изложение более кратким. Все схемы можно найти в специальной папке сопутствующего репозитория.
Загрузка схем маршрутов¶
Перед реализацией схем давайте добавим выделенную папку, чтобы лучше организовать нашу кодовую базу. Мы можем сделать это внутри пути ./routes/todos/
. Более того, мы хотим автоматически загружать их из папки `schemas. Чтобы сделать это, нам нужно следующее:
- Специальный плагин в папке
schemas
. - Определение схем, которые мы хотим использовать
- Плагин autohooks, который будет загружать все автоматически, когда модуль
todos
будет зарегистрирован на экземпляре Fastify.
Мы подробно рассмотрим их в следующих подразделах.
Загрузчик схем¶
Начиная с первого пункта списка, который мы только что обсудили, мы хотим создать файл ./routes/todos/schemas/loader.js
. Проверить содержимое файла можно в следующем фрагменте кода:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Давайте разберем этот простой плагин:
- Мы определили плагин Fastify с именем
schemaLoaderPlugin
, который загружает JSON-схемы ([1]
) - Мы вызвали метод Fastify
addSchema
несколько раз, передавая путь к каждому JSON-файлу в качестве аргумента ([2]
)
Как мы уже знаем, каждое определение схемы определяет структуру и правила валидации тел ответов, параметров и запросов для различных маршрутов.
Теперь мы можем приступить к реализации первой схемы валидации тела ответа.
Валидация тела запроса createTodo
Приложение будет использовать эту схему при создании задачи. С помощью этой схемы мы хотим добиться двух вещей:
- Предотвратить добавление пользователями неизвестных свойств к сущности
- Сделать свойство
title
обязательным для каждой задачи.
Давайте посмотрим на код create-body.json
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Схема относится к типу object
и, даже будучи небольшой по объему, добавляет множество ограничений к допустимым входным данным:
$id
используется для уникальной идентификации схемы во всем приложении; его можно использовать для ссылок на нее в других частях кода ([1]
).- Ключевое слово
required
указывает, что свойствоtitle
является обязательным для данной схемы. Любой объект, не содержащий его, не будет считаться корректным по отношению к данной схеме ([2]
). - Ключевое слово
additionalProperties
имеет значениеfalse
([3]
), означающее, что любые свойства, не определенные в объектеproperties
, будут считаться недействительными по отношению к данной схеме и отбрасываться. - Единственным допустимым свойством является
title
типаstring
([4]
). Валидатор попытается преобразоватьtitle
в строку на этапе проверки тела.
В разделе Использование схем мы увидим, как прикрепить это определение к правильному маршруту. Теперь мы перейдем к защите параметров пути запроса.
Валидация параметров запроса changeStatus
На этот раз мы хотим проверить параметры пути запроса вместо тела запроса. Это позволит нам быть уверенными в том, что вызов содержит правильные параметры с правильным типом. В следующем файле status-params.json
показана реализация:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Давайте посмотрим, как работает эта схема:
- Поле
$id
определяет еще один уникальный идентификатор для этой схемы ([1]
). - В данном случае у нас есть два обязательных параметра —
id
иstatus
([2]
). - Свойство id должно быть строкой (
[3]
), аstatus
— строкой, значение которой может быть«выполнено»
или«не выполнено»
([4]
). Никакие другие свойства не допускаются.
Далее мы рассмотрим, как проверить параметры запроса на примере listTodos
.
Валидация запроса listTodos
На данный момент должно быть понятно, что все схемы подчиняются одним и тем же правилам. Схема запроса не является исключением. Однако в фрагменте list-query.json
мы впервые используем ссылку на схему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Теперь мы можем разобрать этот фрагмент на части:
- Как обычно, свойство
$id
присваивает схеме уникальный идентификатор, на который можно ссылаться в других местах кода ([1]
). - Свойство
title
имеет типstring
и является необязательным ([2]
). Оно может быть отфильтровано по частичномузаголовку
элемента дел. Если свойство не передано, фильтр будет создан пустым. - Свойство
limit
задает максимальное количество возвращаемых элементов и определяется ссылкой на схемуschema
schema:limit
([3]
). Свойствоskip
также определяется ссылкой на схемуschema
schema:skip
и используется для пагинации. Эти схемы настолько общие, что используются во всем проекте.
Теперь пришло время взглянуть на последний тип схемы — схему ответа.
Определение тела ответа createTodo
Определение тела ответа маршрута дает два основных преимущества:
- Предотвращает утечку нежелательной информации к клиентам.
- Увеличивает пропускную способность приложения, благодаря более быстрой сериализации
В файле create-response.json
показана реализация:
1 2 3 4 5 6 7 8 9 10 11 |
|
Давайте рассмотрим структуру этой схемы:
- И снова
$id
— уникальный идентификатор для этой схемы ([1]
) - Объект ответа имеет одно
required
([2]
) свойствоid
типаstring
([3]
)
Эта схема ответа завершает текущий раздел об определениях схем. Теперь пришло время узнать, как использовать и регистрировать эти схемы.
Добавление плагина Autohooks¶
И снова мы можем воспользоваться расширяемостью и системой плагинов, которую Fastify предоставляет разработчикам. Для начала вспомним из Главы 6, что мы уже зарегистрировали экземпляр @fastify/autoload
в нашем приложении. Следующий отрывок из файла app.js
показывает соответствующие части:
1 2 3 4 5 6 7 8 9 |
|
Для целей данного раздела нам важны три свойства:
autoHooksPattern
([1]
) используется для указания шаблона регулярного выражения, который соответствует именам файлов хуков в директорииroutes
. Эти файлы будут автоматически загружены и зарегистрированы как хуки для соответствующих маршрутов.autoHooks
([2]
) включает автоматическую загрузку этих файлов хуков.cascadeHooks
([3]
) гарантирует, что хуки будут выполняться в правильном порядке.
После этого краткого напоминания мы можем перейти к реализации нашего плагина autohook
.
Реализация плагина Autohook¶
Из autoHooksPattern
в предыдущем разделе мы узнали, что наш плагин можно поместить в файл с именем autohooks.js
в директории ./routes/todos
, и он будет автоматически зарегистрирован командой @fastify/autoload
. Следующий фрагмент содержит содержимое плагина:
1 2 3 4 5 6 7 8 9 |
|
Мы начинаем импортировать плагин загрузчика схем, который мы определили в предыдущем разделе ([1]
). Затем, внутри тела плагина, мы регистрируем его ([2]
). Одной этой строки достаточно, чтобы загруженные схемы стали доступны в приложении. Фактически, плагин прикрепляет их к экземплярам Fastify, чтобы сделать их легкодоступными.
Наконец, мы можем использовать эти схемы в наших определениях маршрутов, что мы и сделаем в следующем разделе.
Использование схем¶
Теперь у нас есть все, чтобы защитить наши маршруты и сделать пропускную способность приложения невероятно быстрой.
В этом разделе мы покажем, как это сделать только для одного обработчика маршрута. Полный код вы найдете в репозитории книги , и мы рекомендуем вам поэкспериментировать и с другими маршрутами.
Следующий фрагмент кода присоединяет схемы к определению маршрута:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Мы добавляем свойство schema
в определение маршрута. Оно содержит объект с двумя полями:
- Свойство
body
опцииschema
указывает схему JSON, по которой должно проверяться тело запроса ([1]
). Здесь мы используемfastify.getSchema('schema:todo:create:body')
, которая извлекает JSON-схему для тела запроса из коллекции схем, используя идентификатор, указанный нами в объявлении. - Свойство
response
опцииchema
задает JSON-схему для ответа клиенту ([2]
). Оно устанавливается в объект с единственным ключом201
, который определяет JSON-схему для ответа на успешное создание, поскольку именно этот код мы использовали в обработчике. И снова мы используемfastify.getSchema('schema:todo:create:response')
, чтобы получить JSON-схему для ответа из коллекции схем.
Если теперь мы попытаемся передать неизвестное свойство, валидатор схемы отделит его от тела. Давайте поэкспериментируем с этим, используя терминал и curl
:
1 2 3 4 5 6 7 |
|
Мы передаем свойство foo
в тело, и API возвращает успешный ответ с сохранением уникального id
задачи в базе данных. Второй вызов проверяет, что валидатор работает так, как ожидалось. Поле foo
не присутствует в ресурсе, а значит, наш API теперь безопасен.
На этом мы практически завершаем наше глубокое погружение в разработку RESTful API с помощью Fastify. Однако есть еще одна важная вещь, которая может сделать нашу кодовую базу более удобной, о которой мы должны упомянуть, прежде чем двигаться дальше.
Не повторяйтесь¶
Определение логики приложения внутри маршрутов подходит для простых приложений, таких как в нашем примере. Однако в реальном мире, когда нам нужно использовать нашу логику в приложении в нескольких маршрутах, было бы неплохо определить эту логику только один раз и повторно использовать ее в разных местах. И снова Fastify нас выручает.
Мы можем расширить наш плагин autohooks.cjs
, добавив то, что обычно называют источником данных. В следующем фрагменте мы расширяем предыдущий плагин, добавляя необходимый код, хотя для краткости изложения мы показываем только функцию для обработчика createTodo
; полную реализацию вы можете найти в репозитории кода книги:
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 |
|
Давайте разберем реализацию:
- Мы обернули наш плагин с помощью
fastify-plugin
, чтобы открыть источник данных для других диапазонов плагина ([1]
). - Поскольку мы больше не будем обращаться к коллекции MongoDB из маршрутов, мы перенесли ссылку на нее сюда (
[2]
). - Мы декорируем экземпляр Fastify объектом
mongoDataSource
([3]
), который имеет несколько методов, включаяcreateTodo
. - Мы перенесли логику создания элемента, которая находилась в обработчике маршрута, сюда (
[4]
). Функция возвращаетinsertedId
, который мы можем использовать для заполнения тела элемента, чтобы вернуть его клиентам.
Теперь мы должны обновить наш обработчик маршрута createTodo
, чтобы воспользоваться преимуществами нового кода. Давайте сделаем это в фрагменте кода routes/todos/routes.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Тело нашего обработчика — это однострочник. Его новая обязанность — принимать request.body
([1]
) и передавать его в метод источника данных createTodo
. После того как этот вызов вернется, он возьмет уникальный идентификатор и передаст его клиенту ([2]
). Даже на этом простом примере должно быть понятно, насколько мощной является эта функция. С ее помощью мы можем сделать наш код многократно используемым во всех частях приложения.
В этом заключительном разделе мы рассмотрели все, что нужно знать для разработки простого, но полноценного приложения с использованием Fastify.
Резюме
В этой главе мы шаг за шагом узнали, как реализовать RESTful API в Fastify. Сначала мы использовали мощную систему плагинов для инкапсуляции определений маршрутов. Затем мы защитили наши маршруты и доступ к базе данных с помощью определений схем. Наконец, мы перенесли логику приложения в специальный плагин, используя декораторы. Это позволило нам следовать паттерну DRY и сделать наше приложение более удобным для обслуживания.
В следующей главе мы рассмотрим управление пользователями, сессии и загрузку файлов, чтобы еще больше расширить возможности приложения.