Как стать автором
Обновить

Монорепозиторий с pnpm и typescript для фронтенда на React и бэкенда на Node.js

Уровень сложностиСредний
Время на прочтение3 мин
Количество просмотров5.8K

Устанавливаем pnpm, создаем воркспейсы для фронта и бека, импортируем что угодно из одного в другое, типизируем запросы и избавляемся от бойлерплейта.

pnpm. Что, зачем, почему, впечатление

  • Менеджер пакетов вместо npm

  • Создает единую глобальную папку node_modules на компьютере и используем символические ссылки на нее в проектах

  • Поддерживает workspaces

  • Быстрее npm

Заменили им npm без проблем. Увеличение скорости по сравнению с npm на уровне "вроде быстрее", производительность не измеряли. Краем уха слышал что коллеги столкнулись с трудностями при создании докер образа из-за симссылок, но раз сейчас докер крутится - делаю вывод, что трудности преодолели.

Использование pnpm вызвало только положительное впечатление.

Установка и организация проекта

Инструкция по установке от разработчиков на всякий случай.
Для большинства подойдет установка через npm:

npm install -g pnpm

В корне проекта необходимо создать файл pnpm-workspace.yaml. В нем указываются все части приложения. В соответствии с этим списком необходимо создать папки.

Список воркспейсов
Список воркспейсов
packages:
  - frontend
  - backend

Важно, что в корне проекта и в каждом воркспейсе должны быть свои файлы package.json.

package.json
package.json
  • В общем package.json может не быть зависимостей вообще.

  • Флаг --parallel запускает скрипты одновременно во всех воркспейсах (если в них есть скрипт с таким названием).

  • С помощью флага --filter можно запустить скрипт в конкретном воркспейсе.

  • Ключевое слово exec - аналог npx

Фронтенд package.json
Фронтенд package.json

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

"backend": "workspace:*"

На этом с организацией проекта мы закончили. Пара напутствий:

  • Не забывайте писать в терминале pnpm i вместо npm i.

  • Чтобы установить пакет в конкретный воркспейс, в него нужно сначала перейти - cd frontend, pnpm i something.

Типизация запросов из бекенда

Импорт из одного воркспейса в другой работает также, как если бы они были одним проектом.

Экспорт переменной в воркспейсе бекенда
Экспорт переменной в воркспейсе бекенда
Импорт этой переменной во фронтенде
Импорт этой переменной во фронтенде

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

Вот таким образом я реализовал типизацию запросов

  1. На бекенде экспортирую функции эндпоинтов (обычно они обращаются к бд и отправляют массив данных).

  2. На фронтенде в промежуточном файле types.ts импортирую функции и с помощью TS дженериков описываю типы.

  3. Использую эти типы в проекте, в том числе для аргументов и возвращаемых значений асинхронных экшнов 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 не было.

Монорепа помогла избавиться от огромного количества кода и головной боли. Рекомендую.

Жду советов / критики / предложений

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+7
Комментарии9

Публикации