NextJS API + React Query + Zod
Одним из преимуществ NextJS является возможность совмещать frontend и backend в рамках одного проекта и использовать общие типы и интерфейсы. К сожалению, серверная часть фреймворка работает в отрыве от фронта, являясь во многом самостоятельным приложением. Поэтому, обеспечение typesafe требует дополнительной работы. В этой статья я поделюсь своим опытом решения данного вопроса.
Организация папок
Для каждой сущности заводится отдельная папка, внутри которой прописываются все связанные с ней методы.
Пройдемся по этим уровням, начиная от базового.
items.types.ts
Основой файл. Здесь прописываются типы, связанные с данной сущностью: в каком виде они хранятся в базе, в каком виде данные поступают из базы данных, необходимые поля для создания или редактирования.
Для определения структуры использется zod. В моем последнем проекте использовался Strapi, поэтому выглядело все примерно так:
/lib - взимодействие с базой данных
В эту папку прописываются все методы, которые будут использоваться далее в коде. Это единственное место, где происходит взаимодействие с базой напрямую.
В файлах методов прописываются схемы zod и ожидаемого ответа. Например:
Теперь у нас есть изолированные в отдельной папке методы для работой с БД. Если что-то поменяется в ее логике или нужно будет оптимизировать какой-нибудь запрос, то это можно будет сделать именно здесь, сохранив типы аргументов и ответа.
/api - связка frontend и backend
Функции, размещенные в этой папке, отвечают выполнение бизнес-логики приложения. Каждая такая фича изолирована в отдельной папке и содержит как функцию, которая будет вызываться на стороне frontend (getItemByIdClient), так и функцию, которая будет выполнять работу на сервере (getItemByIdServer).
Главным же здесь является файл config.ts содержащий в себе информацию об используемых функциями типах и интерфейсах, а также о пути к api route, который будет вызываться со стороны frontend.
По сути это набор re-exports нужный для того, чтобы упростить организацию кода.
Аргументы для Client и Server в данном примере одинаковы, но могут различаться, если, например, для выполнения Server-функции требуется еще id пользователя. Он будет получен из cookies в файле route.ts и передан в функцию getItemByIdServer.
Да. Необходимая часть кода находится в другой папке. Пока приходится организовывать все таким образом. Когда Server Actions перестанут находится в статусе экспериментальных, от этого звена можно бдует избавиться. Также, я еще не пробовал библиотеку ts-rest, котора нацелена на эту же проблему.
Файл выглядит примерно так:
Функция getItemByIdServer возвращает не только ответ, но и Http-код ответа:
Здесь содержится вся бизнес-логика приложения. Функция на стороне frontend выполняет простую роль передачи запроса на сервер.
Здесь вместо обычного возврата кода ошибки происходит throw error, так как функция будет использоваться в хуке useQuery от ReactQuery.
Как серверная часть, так и клиентская, осуществляют валидацию полученных данных через zod.
/hooks - добавляем ReactQuery
Хуки располагаются в отдельных папках
В файле queryKey просто прописывается и экспортируется ключ, который будет использов��ться для данного хука. Ее можно будет исопльзовать в других частях приложения, например, если нуждо будет сделать invalidate для запроса.
Сам хук простой - он лишь вызывает уже созданную ранее функцию для frontend части:
и все это экспортируется в файле index.ts
Теперь в компоненте достаточно вызывать хук, чтобы получить необходимые данные:
Заключение
В приведенном примере может показаться излишним разделение простого запроса на столько частей. Но он позволяет организовывать код через изолированные и переиспользуемые части, каждая из которых сосредоточена на выполненни узкого круга задач соответствующего уровня абстракции.
Соответственно, разобраться в коде, а также провести его рефакторинг становится проще, так как сразу понятно, какого типа задачи где решаются.