Монорепозиторий с pnpm и typescript для фронтенда на React и бэкенда на Node.js
Устанавливаем pnpm, создаем воркспейсы для фронта и бека, импортируем что угодно из одного в другое, типизируем запросы и избавляемся от бойлерплейта.
pnpm. Что, зачем, почему, впечатление
Менеджер пакетов вместо npm
Создает единую глобальную папку node_modules на компьютере и используем символические ссылки на нее в проектах
Поддерживает workspaces
Заменили им npm без проблем. Увеличение скорости по сравнению с npm на уровне "вроде быстрее", производительность не измеряли. Краем уха слышал что коллеги столкнулись с трудностями при создании докер образа из-за симссылок, но раз сейчас докер крутится - делаю вывод, что трудности преодолели.
Использование pnpm вызвало только положительное впечатление.
Установка и организация проекта
Инструкция по установке от разработчиков на всякий случай.
Для большинства подойдет установка через npm:
npm install -g pnpm
В корне проекта необходимо создать файл pnpm-workspace.yaml. В нем указываются все части приложения. В соответствии с этим списком необходимо создать папки.
packages:
- frontend
- backend
Важно, что в корне проекта и в каждом воркспейсе должны быть свои файлы package.json.
В общем package.json может не быть зависимостей вообще.
Флаг --parallel запускает скрипты одновременно во всех воркспейсах (если в них есть скрипт с таким названием).
С помощью флага --filter можно запустить скрипт в конкретном воркспейсе.
Ключевое слово exec - аналог npx
Чтобы иметь возможность подключаться к другому воркспейсу, его нужно вписать в зависимости:
"backend": "workspace:*"
На этом с организацией проекта мы закончили. Пара напутствий:
Не забывайте писать в терминале pnpm i вместо npm i.
Чтобы установить пакет в конкретный воркспейс, в него нужно сначала перейти - cd frontend, pnpm i something.
Типизация запросов из бекенда
Импорт из одного воркспейса в другой работает также, как если бы они были одним проектом.
Если на этом этапе не работает импорт - проверьте все ли шаги из этой статьи учтены, если все в порядке - вот неплохая статья по организации фулстек монорепозитория с pnpm.
Вот таким образом я реализовал типизацию запросов
На бекенде экспортирую функции эндпоинтов (обычно они обращаются к бд и отправляют массив данных).
На фронтенде в промежуточном файле types.ts импортирую функции и с помощью TS дженериков описываю типы.
Использую эти типы в проекте, в том числе для аргументов и возвращаемых значений асинхронных экшнов redux-toolkit
// Бэкенд. Функция эндпоинта
export async function getGateConfigAllBack() {
return await prisma.gate__config.findMany({
orderBy: {
index: "asc"
},
})
}
// Фронтенд. Описание типов с помощью дженериков
import { getGateConfigAllBack, upsertGateConfigBack } from "backend/src/directory/GATE__CONFIG/service"
import { ArrayToElement, GetArgs } from "../../../shared/UTILS/typeGenerics"
export type IGateConfigUpsertArgs = GetArgs<typeof upsertGateConfigBack>
export type IGateConfigResponse = ReturnType<typeof getGateConfigAllBack>
export type IGateConfig = ArrayToElement<Awaited<IGateConfigResponse>> & {}
// Фронтенд. AsyncThunk для обращения к эндпоинтам и записи данных в стор
import axiosClient from "../../../axios-client"
import { createAsyncThunk } from "@reduxjs/toolkit"
import { IGateConfigResponse, IGateConfigUpsertArgs } from "../types"
export const getGateConfigAll = createAsyncThunk("DIRECTORY/GATE_CONFIG/getAll",
async (): IGateConfigResponse => {
const res = await axiosClient.get("directory/gate-config/get-all")
return res.data
})
export const upsertGateConfig = createAsyncThunk("DIRECTORY/GATE_CONFIG/upsert",
async (data: IGateConfigUpsertArgs): IGateConfigResponse => {
const res = await axiosClient.post("directory/gate-config/upsert", data)
return res.data
})
Вот все используемые дженерики
export type ArrayToElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never
export type GetReturn<T extends (...args: any) => any> = ReturnType<T>
export type GetReturnAwaited<T extends (...args: any) => any> = Awaited<ReturnType<T>>
export type GetArgs<T extends (...args: any) => any> = Parameters<T> extends [infer a] ? a : Parameters<T>
Заключение
До перехода на монорепозиторий, организация фронта и бэкенда на этом проекте была точно такой же. За исключением файла types.ts, типы в нем описывались вручную - Копировали схему таблицы базы данных, вставляли в файл, корректировали. При каждом изменении схемы таблицы в базе данных, файл типов необходимо было править, причем корректно. Это очень сильно тормозило процесс, потому что баги были на каждом шагу, а ошибок TS не было.
Монорепа помогла избавиться от огромного количества кода и головной боли. Рекомендую.
Жду советов / критики / предложений