Building a Portfolio Monorepo with Turborepo
How this site is structured as a Turborepo monorepo: shared Zod contracts, a NestJS API, two Next.js apps, and the tooling that keeps it all coherent.
Building a Portfolio Monorepo with Turborepo
On this page
Building a portfolio sounds like a weekend project until you decide it should also be a place to practice production engineering. This post walks through how this site is structured as a Turborepo monorepo and why that structure has paid for itself many times over.
The shape of the repo
The repository hosts three deployables — a public Next.js site, an admin dashboard, and a NestJS API — plus a handful of shared packages. The most important one is the contracts package: a single source of Zod schemas that both the API and the frontends import. When the API changes a response shape, the frontend fails to compile instead of failing in production.
Sharing contracts with Zod
Every DTO in the API extends a Zod schema from the contracts package, and every fetch on the frontend parses responses against the same schema. The compiler enforces the boundary in both directions.
export const postSchema = z.object({
slug: z.string(),
title: z.string(),
publishedAt: z.string().datetime().nullable(),
});
export type Post = z.infer<typeof postSchema>;What Turborepo actually buys you
The headline feature is caching: unchanged packages are never rebuilt, locally or in CI. But the quieter win is task orchestration — type-checking the API automatically type-checks the contracts package first, in the right order, every time.
Tooling choices
A few decisions that kept the repo pleasant to work in:
- Biome for formatting and linting — one fast tool instead of three slow ones
- bun as the package manager and script runner
- A single root .env so every service reads the same configuration
- TypeORM migrations checked in next to the entities they evolve
Where it hurts
Monorepos are not free. Dependency hoisting occasionally produces surprising versions, and CI needs careful cache keys to stay fast. The trade was still worth it: one clone, one install, one mental model.
What's next
The next posts in this series cover Supabase JWT verification inside NestJS guards and the scheduled-publishing pipeline that published this very post.
Comments
Loading comments…