Recommended resources
The freely available resources this lesson will direct you to.
- Guarding your Module Boundaries with Lint Ruleswww.youtube.com
- Build a Monorepo using PNPM workspace, React, Vue, Node, Eslint, Prettier and Typescriptwww.youtube.com
- The Right Way to Structure a Node.js Monorepo in 2026 | Stack Dev Lifewww.stackdevlife.com
- Using Turborepo to Build Your First Monorepo - Earthly Blogearthly.dev
Welcome to the next lesson in our journey to build an agentic engineering assistant. In our previous session, we created the architectural blueprints for our system using C4 diagrams, defining the key containers: a Vue client, a TypeScript API, a background worker, and a database. Now, it's time to lay the foundation and translate that architectural vision into a concrete, organized codebase.
This lesson focuses on creating the project's skeleton. We will set up a TypeScript monorepo to house all our code. You'll learn how to structure the project, create separate packages for the frontend, backend, and shared code, and most importantly, establish explicit rules that govern how these pieces can interact. This structure is essential for maintaining a clean, scalable, and maintainable codebase as the project grows.
By the end of this lesson, you will have a fully configured monorepo ready for development, with clear boundaries that prevent architectural decay over time.
Why a Monorepo?
For a system with distinct but tightly-coupled parts like ours—a frontend, a backend API, and a background worker that all need to agree on data structures—a monorepo (a single repository containing multiple distinct projects) offers significant advantages over managing separate repositories (polyrepo).
The key benefits for our project are:
- Simplified Code Sharing: Our
api,webclient, andworkerall need to use the same data types and validation schemas. In a monorepo, we can create ashared-contractspackage and import it directly, without the overhead of publishing and versioning a private npm package. - Atomic Commits: A change to a shared data structure can be updated across the API, worker, and frontend in a single, atomic commit. This eliminates the risk of temporary inconsistencies between services.
- Unified Tooling: We can have a single, top-level command to run tests, lint, or type-check across the entire project, ensuring consistency everywhere.
To manage our monorepo, we will use two powerful tools:
- pnpm: A fast and disk-space-efficient package manager that has excellent support for monorepos through a feature called workspaces.
- Turborepo: A high-performance build system designed for monorepos. It intelligently orchestrates tasks, caches outputs, and understands the dependency graph between your packages.
The image below illustrates a common monorepo architecture, similar to what we will build, where a backend API serves multiple frontend applications, all managed within a single Turborepo setup.

Setting Up the Skeleton
The standard convention for a monorepo is to separate deployable applications from shareable packages.
apps/: This directory will contain our runnable applications: the Vue frontend (web), the backendapi, and theworker.packages/: This will house our shared code, such as UI components, utility functions, and most importantly, ourshared-contracts.
The following article from Earthly provides a hands-on walkthrough for setting up a monorepo with pnpm and Turborepo. We'll follow its guidance to create our initial structure.
Using Turborepo to Build Your First Monorepo - Earthly Blog
This guide provides a clear, step-by-step process for initializing a monorepo. We will use the concepts it introduces to create the skeleton for our project.
Please read the sections titled Create the Monorepo. As you read, focus on understanding these key steps: Initializing pnpm. Creating the apps and packages directory structure. Setting up the pnpm-workspace.yaml file to tell pnpm where to find the projects. You can skip the parts about creating specific Vue or React apps for now; we're just focused on the directory structure and workspace configuration.
Following those steps, your project's root directory should now have a pnpm-workspace.yaml file and two empty directories: apps and packages. This is the basic skeleton of our monorepo.
Centralized TypeScript Configuration
A common pitfall in monorepos is duplicating configuration files, leading to inconsistencies. To avoid this, we'll use a single base tsconfig.json at the root of our project and have each package extend it. This ensures all our code adheres to the same TypeScript compiler rules.
The article "The Right Way to Structure a Node.js Monorepo" explains this inheritance pattern perfectly.
The Right Way to Structure a Node.js Monorepo in 2026 | Stack Dev Life
This section explains a robust pattern for managing TypeScript configuration across a monorepo, which we will adopt for our project.
Please read the section get the inheritance right. Pay close attention to the distinction between the base tsconfig.base.json (which contains only compilerOptions) and the package-specific tsconfig.json (which extends the base and adds local paths).
Let's put this into practice.
- Create a file named
tsconfig.base.jsonin the root of your monorepo with the strictcompilerOptionssuggested in the article. - Inside each application and package we create (e.g.,
apps/api), we will add atsconfig.jsonfile that looks like this:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
This simple but powerful pattern ensures consistency while giving each package control over its own source files.
The Shared Contract Package
The most critical shared element in our architecture is the "contract" between the backend and its clients. This includes data types, API endpoint definitions, and validation logic. By placing this in a shared package, we can achieve end-to-end type safety.
We will create a package at packages/contracts. The Stack Dev Life article also demonstrates a modern technique for exposing a package's source code for a seamless development experience, using the exports field in package.json.
The Right Way to Structure a Node.js Monorepo in 2026 | Stack Dev Life
This reading will show you how to configure a shared package so other parts of the monorepo can consume it without needing a separate build step during development.
Read the section how to share code without pain. Focus on how the exports field in package.json is configured to point directly to the TypeScript source files (./src/index.ts).
By using this pattern, when you're developing the api, it can import directly from the TypeScript source of the contracts package. Your IDE will provide instant feedback and type-checking, and you won't need to constantly rebuild the contracts package after every change. For production builds, Turborepo will ensure everything is compiled correctly.
Enforcing Architectural Boundaries
We've established a directory structure, but nothing technically prevents a developer from making an incorrect import, violating our architecture. For example, the api should never import code directly from the web frontend. Manually policing this is tedious and error-prone.
A senior engineering practice is to automate the enforcement of these architectural rules. We can do this using linting. The following video explains how to define and enforce module boundaries using ESLint rules. Although the video uses a tool called Nx, the underlying principle of tagging packages and defining dependency rules is universal and can be applied with the @typescript-eslint/enforce-module-boundaries rule.
Guarding your Module Boundaries with Lint Rules
This video demonstrates a powerful technique for enforcing architectural rules automatically. We will apply this concept to our monorepo to maintain its structure.
Please watch the following segments: The problem: Understand why accidental imports are an issue in monorepos. Classifying projects: Learn the strategy of using "tags" to categorize each package (e.g., by type and scope). Defining rules: See how to write rules in your ESLint configuration that specify which tags can depend on which other tags. Seeing it in action: Observe how the lint rule provides immediate feedback in the editor when a rule is violated.
To apply this to our project, we would:
- Tag each package: In each
package.json, we can add a custom field like"tags": ["type:app", "scope:api"]for the backend, or"tags": ["type:lib", "scope:shared"]for our contracts package. - Configure ESLint: In our root
.eslintrc.jsfile, we would configure the@typescript-eslint/enforce-module-boundariesrule to define the valid dependency graph. For example:- A package with
type:appcan depend on a package withtype:lib. - A package with
type:libcannot depend on a package withtype:app. - A package with
scope:apicannot depend on a package withscope:web.
- A package with
This turns your architectural diagram into a checkable specification that runs continuously in your IDE and in your CI/CD pipeline, preventing costly mistakes.
Conclusion
In this lesson, you have constructed the foundational structure of our application. We've moved beyond diagrams and created a tangible, well-organized TypeScript monorepo. This skeleton isn't just a set of empty folders; it's a system designed for scalability and maintainability.
Key Takeaways:
- A monorepo is an effective strategy for managing projects with multiple, interconnected parts like our agentic assistant.
- The
apps/andpackages/directory structure provides a clean separation between deployable applications and shared libraries. - pnpm workspaces link the packages together, and Turborepo helps manage the build process efficiently.
- Using a shared
tsconfig.base.jsonensures consistent compiler settings across the project. - Automated boundary enforcement via ESLint rules is a crucial practice for preventing architectural decay and maintaining clean dependencies.
We've made a significant architectural choice today by opting for a monorepo. In our next lesson, we will formalize this decision. You will learn how to write an Architecture Decision Record (ADR), a short document that captures a key architectural decision, its context, and its consequences. This is a vital skill for communicating design rationale within a team.