Monorepo Architecture with Shared Types
A TypeScript monorepo houses the frontend, backend, and shared code in one repository. The `packages/shared` package contains types and Zod schemas used on both client and server — eliminating duplication and guaranteeing that both sides always agree on the API contract. Tools: npm workspaces (or pnpm workspaces), TypeScript project references.
Monorepo Setup — Shared Types Package
# ── Project structure ───────────────────────────────────────────
# task-manager/
# ├── package.json ← workspace root
# ├── tsconfig.base.json ← shared strict config
# ├── packages/
# │ ├── shared/ ← @task-manager/shared
# │ │ ├── package.json
# │ │ ├── tsconfig.json
# │ │ └── src/
# │ │ ├── types.ts ← Task, User, Priority types
# │ │ ├── schemas.ts ← Zod schemas (isomorphic — no Node/DOM)
# │ │ └── index.ts ← barrel
# │ ├── server/ ← @task-manager/server (Node/Express)
# │ │ ├── package.json ← depends on @task-manager/shared
# │ │ └── src/
# │ └── client/ ← @task-manager/client (React/Vite)
# │ ├── package.json ← depends on @task-manager/shared
# │ └── src/
# root package.json (npm workspaces):
# {
# "name": "task-manager",
# "private": true,
# "workspaces": ["packages/*"],
# "scripts": {
# "build": "npm run build --workspaces",
# "test": "npm run test --workspaces",
# "type-check": "npm run type-check --workspaces",
# "qa": "npm run type-check && npm run lint && npm run test"
# }
# }
// packages/shared/src/types.ts — pure types, no runtime deps ──
export type Priority = "low" | "medium" | "high" | "critical";
export type TaskStatus = "todo" | "in-progress" | "done" | "archived";
export interface Task {
readonly id: number;
title: string;
description: string | null;
status: TaskStatus;
priority: Priority;
assigneeId: number | null;
dueDate: string | null; // ISO string — safe for JSON
readonly createdAt: string; // ISO string
updatedAt: string; // ISO string
tags: readonly string[];
}
// packages/shared/src/schemas.ts — Zod schemas (works everywhere)
import { z } from "zod";
export const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(["low","medium","high","critical"]).default("medium"),
description: z.string().max(5000).optional(),
dueDate: z.string().datetime().optional(),
tags: z.array(z.string()).max(10).default([]),
});
export type CreateTaskDTO = z.infer<typeof CreateTaskSchema>;
// Server uses it to validate incoming requests.
// Client uses it to validate the form before sending.
// Both sides share the EXACT same schema — zero drift.Common Mistakes
- Putting Node.js-specific code in shared package — `@task-manager/shared` must work in both Node and browser. Never import `fs`, `path`, `http`, or `process` directly. Keep it pure TypeScript + Zod only.
- Not using TypeScript project references in the monorepo — without project references, changes to the shared package aren't picked up by the server/client until you manually rebuild. Set up `composite: true` and `references` in tsconfigs for incremental builds.
- Importing implementation from shared, not just types — the shared package exports types and schemas. Business logic belongs in server or client, not shared.
Tip
Tip
Practice Monorepo Architecture with Shared Types in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Always strict mode. Use .d.ts for ambient types.
Practice Task
Note
Practice Task — (1) Write a working example of Monorepo Architecture with Shared Types from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Monorepo Architecture with Shared Types is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready typescript code.
Key Takeaways
- A TypeScript monorepo houses the frontend, backend, and shared code in one repository.
- Putting Node.js-specific code in shared package — `@task-manager/shared` must work in both Node and browser. Never import `fs`, `path`, `http`, or `process` directly. Keep it pure TypeScript + Zod only.
- Not using TypeScript project references in the monorepo — without project references, changes to the shared package aren't picked up by the server/client until you manually rebuild. Set up `composite: true` and `references` in tsconfigs for incremental builds.
- Importing implementation from shared, not just types — the shared package exports types and schemas. Business logic belongs in server or client, not shared.