Streamlining Full-Stack TypeScript Development with Monorepos
Ethan Miller
Product Engineer · Leapcell

Introduction
In the vibrant landscape of modern web development, building robust full-stack applications often involves a complex interplay between front-end frameworks and back-end services. As projects scale, managing disparate codebases for React applications and NestJS APIs can quickly become a significant overhead. Fragmentation leads to duplicated configurations, inconsistent dependencies, and inefficient build processes. This is where the monorepo approach shines, offering a unified development experience that addresses these challenges head-on. By consolidating related projects within a single repository, we unlock powerful advantages in code sharing, build optimization, and overall developer productivity. This article delves into how tools like Nx and Turborepo specifically enable this streamlined workflow for full-stack (React + NestJS) TypeScript projects.
Understanding the Monorepo Advantage
Before diving into the specifics of Nx and Turborepo, let's establish a foundational understanding of the core concepts that underpin their power in a monorepo context.
Monorepo: A development strategy where multiple distinct projects (e.g., front-end, back-end, shared libraries) are stored within a single version-controlled repository. This contrasts with a "polyrepo" approach, where each project resides in its own separate repository.
Workspace: In the context of Nx and Turborepo, a workspace refers to the root directory of your monorepo, encompassing all your individual applications and libraries.
Application (App): A deployable unit within the monorepo, such as your React front-end or your NestJS back-end API.
Library (Lib): Reusable blocks of code that can be shared across multiple applications or other libraries within the monorepo. This is a cornerstone for enforcing consistency and reducing redundancy. Examples include shared UI components, data models, utility functions, or API interfaces.
Task Runner / Build System: Tools like Nx and Turborepo act as intelligent task runners. They understand the dependencies between projects in your monorepo and can optimize various development tasks like building, testing, linting, and serving.
Dependency Graph: A crucial concept where these tools represent the relationships between your projects (apps and libraries). This graph allows them to determine which projects need to be rebuilt after a change and to execute tasks in the correct order.
Distributed Caching: To further accelerate development, these tools leverage caching. If a dependency hasn't changed, its build outputs can be reused from a cache (local or remote), significantly speeding up subsequent builds and tests.
Now, let's explore how Nx and Turborepo implement these concepts to streamline a full-stack React + NestJS TypeScript project.
Nx for Full-Stack TypeScript Development
Nx is a powerful, extensible build system that embraces the monorepo philosophy. It provides a comprehensive set of features for managing large-scale, enterprise-grade applications. It offers strong opinions and code generation capabilities, making it excellent for jump-starting projects and maintaining consistency.
Setting up an Nx Workspace
Let's begin by creating a new Nx workspace and adding our React and NestJS applications.
npx create-nx-workspace@latest fullstack-monorepo --preset=react-standalone --appName=frontend --style=css
This command creates a new Nx workspace named fullstack-monorepo
with a React application called frontend
. Now, let's add a NestJS application (often referred to as an API).
cd fullstack-monorepo npm install -D @nx/nest nx g @nx/nest:application backend --dryRun=false
Now your workspace structure will look something like this:
fullstack-monorepo/
├── apps/
│ ├── backend/ # NestJS application
│ └── frontend/ # React application
├── libs/ # Shared libraries
├── nx.json # Nx configuration
├── package.json
└── tsconfig.base.json
Creating and Sharing Libraries
The true power of a monorepo emerges when you start sharing code. Let's create a shared library for our data models or API types.
nx g @nx/js:library shared-data --dryRun=false
This creates libs/shared-data
. In libs/shared-data/src/lib/shared-data.ts
, we might define an interface:
// libs/shared-data/src/lib/shared-data.ts export interface User { id: string; name: string; email: string; } export function getUserGreeting(user: User): string { return `Hello, ${user.name}!`; }
Now, both our frontend
and backend
can consume this library.
In apps/backend/src/app/app.service.ts
:
// apps/backend/src/app/app.service.ts import { Injectable } from '@nestjs/common'; import { User, getUserGreeting } from '@fullstack-monorepo/shared-data'; // Import from the library @Injectable() export class AppService { getData(): { message: string } { const user: User = { id: '1', name: 'Alice', email: 'alice@example.com' }; return { message: getUserGreeting(user) }; } }
And in apps/frontend/src/app/app.tsx
:
// apps/frontend/src/app/app.tsx import React, { useEffect, useState } from 'react'; import { User, getUserGreeting } from '@fullstack-monorepo/shared-data'; // Import from the library export function App() { const [greeting, setGreeting] = useState(''); useEffect(() => { // Example usage in frontend const user: User = { id: '2', name: 'Bob', email: 'bob@example.com' }; setGreeting(getUserGreeting(user)); // Example: Fetching data from NestJS backend fetch('/api') .then((res) => res.json()) .then((data) => console.log('Backend response:', data.message)); }, []); return ( <> <h1>Welcome, Frontend!</h1> <p>{greeting}</p> </> ); } export default App;
Notice the import paths using the @fullstack-monorepo/shared-data
alias. Nx automatically configures these TypeScript path mappings in tsconfig.base.json
for seamless imports.
Running Tasks with Nx
Nx provides powerful commands for running tasks across your monorepo.
- Serve both applications concurrently:
nx run-many --target=serve --projects=frontend,backend --parallel
- Build all affected projects:
This command intelligently detects changes and only rebuilds projects that are directly or indirectly affected.nx affected:build
- Run tests:
nx test backend
Nx's dependency graph analysis ensures that tasks are executed efficiently, and its caching mechanisms (local and remote) dramatically reduce build times.
Turborepo for Speed and Simplicity
Turborepo, acquired by Vercel, distinguishes itself with its laser focus on speed and ease of use. It's essentially a high-performance build system for JavaScript and TypeScript monorepos, prioritizing fast builds, smart caching, and efficient task orchestration. While Nx offers a more opinionated, full-suite experience with generators and plugins, Turborepo is more agnostic and focuses purely on accelerated builds.
Setting up a Turborepo Workspace
Let's start fresh to demonstrate Turborepo.
npx create-turbo-app
Follow the prompts, and choose a minimal
setup for now. This will create a basic workspace structure.
turbo-monorepo/
├── apps/
├── packages/ # Similar to Nx's libs
├── turborepo.json
├── package.json
Manually create our React and NestJS applications.
For React:
In apps/frontend
, create a standard React project (e.g., using Vite or Create React App).
package.json
for frontend
:
{ "name": "frontend", "version": "1.0.0", "private": true, "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", "typescript": "^5.0.2", "vite": "^4.4.5" } }
For NestJS:
In apps/backend
, create a NestJS project.
package.json
for backend
:
{ "name": "backend", "version": "1.0.0", "private": true, "scripts": { "build": "nest build", "start": "nest start", "start:dev": "nest start --watch" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "jest": "^29.5.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" } }
Shared Packages (Libraries)
In Turborepo, shared code lives in the packages
directory. Let's create packages/shared-data
.
packages/shared-data/package.json
:
{ "name": "shared-data", "version": "1.0.0", "main": "./src/index.ts", "types": "./src/index.ts", "private": true, "scripts": { "build": "echo \"No build needed for shared-data, direct TS usage.\"" } }
packages/shared-data/src/index.ts
:
// packages/shared-data/src/index.ts export interface User { id: string; name: string; email: string; } export function getUserGreeting(user: User): string { return `Hello, ${user.name}!`; }
To use this in our applications, we add shared-data
as a dependency in their respective package.json
files.
In apps/backend/package.json
:
{ // ... other fields "dependencies": { "@nestjs/common": "^10.0.0", // ... "shared-data": "workspace:*" // This tells npm/yarn/pnpm to link to the local package } }
In apps/frontend/package.json
:
{ // ... other fields "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "shared-data": "workspace:*" } }
Then run npm install
at the root of the monorepo to link these packages.
Configuring Turborepo Tasks
The heart of Turborepo is the turbo.json
file at the root of your workspace. It defines the tasks (build
, dev
, test
) for each package and their dependencies.
turbo.json
:
{ "$schema": "https://turbo.build/schema.json", "pipe": { "build": { "cache": true, "dependsOn": ["^build"], "outputs": ["dist/**"] }, "dev": { "cache": false, "persistent": true }, "test": { "cache": true, "outputs": ["coverage/**"] } } }
cache: true
: Enables caching for thebuild
andtest
tasks.dependsOn: ["^build"]
: The^
indicates a dependency on thebuild
task of all dependent packages. So,apps/frontend
'sbuild
task will depend onpackages/shared-data
'sbuild
task (if it had one, or simply ensuringshared-data
is ready).outputs
: Specifies which files to cache and restore from cache.persistent: true
: Tasks likedev
that run continuously should be marked as persistent.
Running Tasks with Turborepo
Now, you can execute tasks for your entire monorepo with simple commands.
- Build all projects:
turbo build
- Run all
dev
tasks concurrently:
This will start both your React frontend and your NestJS backend in development mode.turbo dev
- Run tests for a specific project:
turbo run test --filter=backend
Turborepo automatically parallelizes tasks and leverages its cache to only run operations that truly need to be executed.
Choosing Between Nx and Turborepo
Both Nx and Turborepo are excellent choices for managing monorepos, but they cater to slightly different needs:
- Nx: Offers a more opinionated, full-featured experience. It provides code generators, plugins for various frameworks (React, Angular, NestJS, etc.), and built-in project graph visualization. It's often preferred for larger teams and enterprises that benefit from structured approaches and standardized project setups. If you want a complete "monorepo operating system," Nx is a strong contender.
- Turborepo: Focuses on being a fast, flexible, and unopinionated build system. It excels at accelerating existing monorepos or new ones where developers prefer more control over project configurations. If your primary concern is speed and getting out of the way of your existing tooling, Turborepo is an excellent choice.
For a full-stack React + NestJS TypeScript project, both tools can deliver significant benefits. Nx's generators can get you up and running faster with best practices baked in, while Turborepo might appeal if you prefer to integrate your existing build scripts with minimal overhead but maximum speed.
Conclusion
Managing a full-stack React and NestJS TypeScript project within a monorepo structure, powered by tools like Nx or Turborepo, represents a significant leap forward in development efficiency and maintainability. By enabling effortless code sharing through libraries, optimizing build processes with intelligent caching and parallelization, and providing a unified development experience, these tools transform the way developers approach complex applications. Embracing a monorepo strategy with Nx or Turborepo is not just about organizing code; it's about building faster, more consistently, and with greater confidence in your rapidly evolving full-stack projects.