Setup : Monorepo-turborepo-tRPC
Multi-repo VS Monorepo
Multirepo - react-frontend, express backend, postgress-db, redis server
Monorepo - sigle repo > one repo can contain frontent, backend, shared utils and types etc example project
apps -- web (fully deployed app) -- api // fully deployed backend
packages -- shared // common utils, db connection, schema
Benefits => code sharing, atomic commits, better team collab, unified cicd & tooling
PNPM => Performant NPM ,like npm,yarn or bun // popular in monorepo
Fast
Disk space efficient
insted of duplicate is store in central place and each time take refrence symlinks
PNPM vs NPM
pnpm-workspaces > depened on each other, import each other, share efficiently, install and run packages together
Project setup
npm i pnpmmkdir monorepo && cd monorepopnpm init // update with required things
package.json
{
"name": "monorepo",
"private": true,
"types": "module",
"packageManager": "pnpm@10.33.4"
}
Folder setup
mkdir apps packages apps/api apps/web packages/utils
pnpm-workspace.yaml
the line that makes it a workspace. Any folder under apps/ or packages/ is now a linkable package:
packages:
- "apps/*"
- "packages/*"
allowBuilds:
esbuild: true
Shared package setup
packages/utils
pnpm initpnpm add -D typescriptpnpm add zodtsc --init
packages/utils/package.json
{
"name": "@monorepo/utils",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "tsc --watch",
"build": "tsc"
},
"exports": {
".": "./src/index.ts"
},
"packageManager": "pnpm@10.33.4",
"devDependencies": {
"typescript": "^6.0.3"
},
"dependencies": {
"zod": "^4.4.3"
}
}
packages/utils/tsconfig.json
{
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": {
// File Layout
"rootDir": "./src", // ts
"outDir": "./dist", // js
// Environment Settings
// See also https://aka.ms/tsconfig/module
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}
packages/utils/src/index.ts
Create Zod schema // for frontend and backend validation
import { z } from 'zod';
export const createUserSchema = z.object({
name: z.string().min(3, "Name must be at least 3 characters long"),
email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long")
})
export type CreateUserSchema = z.infer<typeof createUserSchema>; // zod will create schema like this
packages/utils/package.json export the index file
"exports":{
".":"./src/index.ts"
}
packages/utils
pnpm build
Backend setup
apps/api
Backend setup for API
pnpm initpnpm add express @types/express -D typescript tsx @types/nodetsc --init
update .tsconfig.json
rootDir
outDir
lib
types
apps/api/package.json update with
{
"name": "@monorepo/api",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc"
},
"packageManager": "pnpm@10.33.4",
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^26.0.0",
"express": "^5.2.1",
"tsx": "^4.22.4",
"typescript": "^6.0.3"
}
}
apps/api/src/index.ts
Basic get request
import express from "express";
const app = express();
const PORT = 5000;
app.use(express.json());
app.get("/", (req, res) => {
return res.json({ message: "Hello from API" });
})
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Now import ZOD schema to the backend api
apps/api/package.json name:"@projectName/api" // "@repo/api" dependcies:{ "@projectName/utils": "workspace:*" // indicate get from local not version } apps/api
pnpm i// insalled and symlinks created
apps/api/src/index.ts Import Zod from symlinks or shared folder
import { createUserSchema } from "@monorepo/utils";
app.post("/users", (req, res) => {
const result = createUserSchema.safeParse(req.body);
if(!result.success) {
const message = result.error.issues.map((issue) => issue.message).join(", ")
return res.status(400).json({success:false, message:message });
}
console.log(result.data);
return res.json({success:true, message: "User created" });
});
Frontend setup
apps/web
pnpm create next-app .
remove pnpm-workspace.yaml its confilict remove public folder
package/web/package.json update the dependencies "name": "@monorepo/web",// "@repo/utils"
pnpm i
package/web/app/page.tsx
"use client";
import { createUserSchema } from "@monorepo/utils";
import axios from "axios";
import { useState } from "react";
import type { SubmitEvent } from "react";
export default function Home() {
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
const [errors, setErrors] = useState("");
const [success, setSuccess] = useState(false);
const handleSubmt = async (e: SubmitEvent<HTMLFormElement>) => {
setSuccess(false);
setErrors("");
e.preventDefault();
const result = createUserSchema.safeParse(formData);
if (!result.success) {
const message = result.error.issues
.map((issue) => issue.message)
.join(", ");
setErrors(message);
return alert(message);
}
console.log(result.data);
try {
const res = await axios.post("http://localhost:5000/users", result.data);
console.log(res);
setSuccess(true);
} catch (error) {
console.log(error);
}
};
return (
<form onSubmit={handleSubmt} noValidate>
{errors && errors.split(", ").map((error, i) => <p key={i}>{error}</p>)}
{success && <p>Success</p>}
<input
type="text"
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<input
type="email"
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<input
type="password"
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<button type="submit">Submit</button>
</form>
);
}
package/web
pnpm i axiosand update package/web/app/page.tsx post requiest on submit ""
const res = await axios.post("http://localhost:5000/users", result.data)
check for cors error and make form submit
all in one setup for all
for all run and build from one place
package.json
"scripts": {
"dev": "pnpm -r --parallel dev",
"build": "pnpm -r build"
}
TurboRepo setup
package is depandend on each other (dependcy graph). Build in Right order
Turbo repo // turborepo.dev
Build order manage
caching
task orchestration
parrlel excuation
Add turbo in project
pnpm add -D turbo -w // -w is for do it now
turbo.json
{
"$schema":"https://turbo.build/schema.json",
"ui":"tui", // terminal ui to looks good the console the logs for each server
"tasks":{
"build": {
"dependsOn": ["^build"], // check for dependent other build (resolve dependecy graph)
"outputs": ["dist/**", "next/**", "!dist/**/node_modules/**", "!next/**/node_modules/**", "!.next/cache/**"] //caching
},
"dev":{
"cache": false, // no need to cache
"persistent": true // long running
},
"prettier":{
"cache": false,
"persistent": true
},
"lint":{
"cache": false,
"persistent": true
}
}
}
package.json
"scripts": {
"dev": "turbo dev",
"build": "turbo build"
},
at Root folder pnpm dev pnpm build
tRPC (Remote procedure calls)
clinet will call the remote server RPC concept call without "stings" call the direct methods impelentation (gRPC and tRPC)
Packages or share utils
packages/trpc
pnpm initpnpm add -D typescriptpnpm add @trpc/servertsc --initpackages/trpc/package.json
"name":"@monorepo/trpc"
private:true
export:trpc
dependies:{
"@monorepo/utils": "workspace:*",
}
scripts:{
"dev":"tsc --watch",
"build":"tsc"
}
packages/trpc/src/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router; // rotuer > function declare
export const procedure = t.procedure; // functions
packages/trpc/src/router.ts
import { procedure, router } from "./trpc.js";
import { createUserSchema } from "@peer-class/utils";
export const appRouter = router({
health: procedure.query(() => {
return {
message: "healthy"
}
}),
register: procedure
.input(createUserSchema)
.mutation(({ input }) => {
// TODO: persist user to DB
console.log("register", input);
return {
message: "User Registered Successfully"
}
}),
});
export type AppRouter = typeof appRouter;
packages/trpc/src/index.ts
export { appRouter } from './router.js';
export type { AppRouter } from './router.js';
pnpm i
Backend
apps/api/
pnpm add @trpc/server
add trpc package at package.json pnpm i
apps/api/src/index.ts
import { createExpressMiddleware } from "@trpc/server/adapters/express";
import { appRouter } from "@peer-class/trpc";
// remove app.get (no need of rest api)
app.use(
"/trpc",
createExpressMiddleware({
router: appRouter
})
);
Frontend
apps/web
pnpm add @trpc/client @tanstack/react-query @trpc/react-queryapp/web/trpc/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@peer-class/trpc";
export const trpc = createTRPCReact<AppRouter>();
app/web/trpc/Provider.tsx
'use client';
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { trpc } from "./trpc";
export function Provider({ children }: { children: React.ReactNode }) {
// React Query client — manages server state caching, background refetching, etc.
const [queryClient] = useState(() => new QueryClient());
// tRPC client — batches multiple procedure calls into a single HTTP request to /trpc
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: "http://localhost:5000/trpc",
}),
],
})
);
// trpc.Provider bridges tRPC with React Query so tRPC hooks use the shared queryClient
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
/*
* What this file does:
* ---------------------
* Sets up the tRPC + React Query context for the entire Next.js app.
*
* - QueryClient → React Query's cache store. Holds all server-state (queries/mutations).
* - trpcClient → HTTP client that sends batched requests to the Express backend at
* http://localhost:5000/trpc using tRPC's httpBatchLink.
* - trpc.Provider → Makes tRPC hooks (trpc.*.useQuery / useMutation) available in the tree.
* - QueryClientProvider → Makes React Query hooks (useQuery, useMutation) available in the tree.
*
* Both providers share the same queryClient so tRPC and React Query stay in sync.
* Wrap the root layout (layout.tsx) with this Provider to enable tRPC across the whole app.
*/
apps/web/app/layout.tsx
import { Provider } from "../trpc/Provider";
<Provider>{children}</Provider> // get everywhere for query and mutations
apps/web/app/health/page.tsx
'use client';
import { trpc } from "@/trpc/trpc"
export default function Health(){
const health = trpc.health.useQuery();
console.log(health.data);
return (
<div>
<h1>Healthy</h1>
</div>
)
}
Final commad at root to run allpnpm devpnpm build


