Monorepository with pnpm and typescript for frontend on React and backend on Node.js

We install pnpm, create workspaces for the front and back, import anything from one to the other, type queries and get rid of the boilerplate.

pnpm. What, why, why, impression

  • Package manager instead of npm

  • Creates a single global node_modules folder on the computer and uses symbolic links to it in projects

  • Supports workspaces

  • Faster than npm

They replaced it with npm without any problems. Speed ​​increase compared to npm at the “seemingly faster” level, performance was not measured. I heard from the corner of my ear that colleagues encountered difficulties in creating a docker image due to simlinks, but since docker is now spinning, I conclude that the difficulties have been overcome.

Using pnpm gave me only positive impressions.

Installation and project organization

Installation instructions from the developers just in case.
For most people, installing via npm will do:

npm install -g pnpm

You need to create a file in the project root pnpm-workspace.yaml. It identifies all parts of the application. In accordance with this list, you need to create folders.

List of workspaces

List of workspaces

packages:
  - frontend
  - backend

It is important that the root of the project and each workspace must have its own files package.json.

package.json

package.json

  • In general package.json may not have dependencies at all.

  • Flag –parallel runs scripts simultaneously in all workspaces (if they have a script with the same name).

  • Using a flag –filter you can run the script in a specific workspace.

  • Keyword exec – analog npx

Frontend package.json

Frontend package.json

To be able to connect to another workspace, you need to include it in the dependencies:

"backend": "workspace:*"

This is where we finished organizing the project. A couple of parting words:

  • Don't forget to write in the terminal pnpm i instead of npm i.

  • To install a package in a specific workspace, you must first go to it – cd frontend, pnpm i something.

Typing requests from the backend

Importing from one workspace to another works the same as if they were one project.

Exporting a variable in the backend workspace

Exporting a variable in the backend workspace

Importing this variable in the frontend

Importing this variable in the frontend

If the import does not work at this stage, check whether all the steps from this article are taken into account; if everything is in order, here is a good one article on organizing a full stack monorepository with pnpm.

This is how I implemented query typing

  1. On the backend I export endpoint functions (usually they access the database and send an array of data).

  2. On the frontend in an intermediate file types.ts I import functions and use TS generics to describe types.

  3. I use these types in the project, including for arguments and return values ​​of asynchronous redux-toolkit actions

// Бэкенд. Функция эндпоинта
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
    })

Here are all the generics used

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>

Conclusion

Before switching to a monorepository, the organization of the front and backend on this project was exactly the same. Except file types.ts, the types in it were described manually – The database table schema was copied, pasted into a file, corrected. Every time the table schema in the database changed, the type file had to be edited, and correctly. This greatly slowed down the process, because there were bugs at every step, but there were no TS errors.

Monorep helped get rid of a huge amount of code and headaches. I recommend.

I'm looking for advice/criticism/suggestions

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *