Vue Monorepo Guide: Turborepo vs Nx Setup

Introduction to Monorepo Fundamentals

monorepo (monolithic repository) represents a strategic approach to software development where multiple projects, applications, and libraries are stored within a single repository rather than being scattered across multiple version-controlled repositories. This approach has gained significant traction in modern web development, particularly in the Vue.js ecosystem, as it enables teams to maintain consistency across projects while streamlining code sharing and dependency management. Industry leaders like Google and Shadcn’s UI have successfully leveraged monorepos to manage dozens or even hundreds of interconnected projects efficiently, avoiding what’s commonly known as “dependency hell” while ensuring consistent tooling across the entire codebase.

The monorepo approach offers several compelling advantages for Vue.js development. First, it simplifies full deployments as there are fewer moving parts to orchestrate compared to managing multiple repositories. Second, dependency management becomes more straightforward since components within the same monorepo typically share the same dependency versions. Third, code sharing becomes significantly easier, encouraging the creation of reusable UI components, composables, and utilities. Finally, standardization across projects becomes more achievable as teams can enforce consistent coding patterns, tooling configurations, and development practices throughout the entire codebase.

However, monorepos also introduce specific challenges that need careful consideration. Without proper tooling, build times can increase significantly as the entire codebase grows. There’s also a tendency for developers to introduce tight coupling between components that should remain independent. Additionally, publishing individual components or features can become complex when they’re part of a larger repository structure. Teams must also navigate the potential coordination overhead when multiple developers work within the same repository, though this can be mitigated with proper tooling and processes.

Monorepo Tooling Landscape: Turborepo vs Nx

When embarking on a monorepo journey, selecting the right tooling is crucial for success. Two of the most prominent tools in this space are Turborepo and Nx, each offering distinct advantages for Vue.js projects.

Turborepo, now maintained by Vercel, is described as a “smart build system” that focuses on high-performance task execution. Its core strength lies in intelligent caching mechanisms and dependency graph awareness that allows it to run tasks in parallel while skipping work that has already been completed. Turborepo shines in its simplicity and seamless integration with existing codebases, requiring minimal configuration to get started. It works alongside your existing package manager (npm, yarn, or pnpm) rather than replacing it, making adoption relatively straightforward. The tool is particularly well-suited for JavaScript-focused monorepos and offers excellent support for Vue.js applications when configured properly.

Nx, on the other hand, is a more comprehensive monorepo management framework that extends beyond simple task running. It provides advanced features like code generationdependency graph visualization, and sophisticated change detection. Nx automatically analyzes your workspace to build a project graph, understanding how your apps and libraries depend on each other. This allows you to build or test only what has changed and, furthermore, what is directly influenced by those changes based on comparisons with previous commits. As noted in the Nx documentation, “Nx caches build and test results, so repeated commands are lightning fast. It can even share cache results across CI runs and between developers”.

The following table compares the key characteristics of both tools:

FeatureTurborepoNx
Learning CurveGentle, minimal configurationSteeper, more concepts to learn
CachingAdvanced content-aware cachingSophisticated caching with remote capabilities
Code GenerationLimited, relies on communityRich generator ecosystem with plugins
Vue SupportWorks well with proper configurationOfficial @nx/vue plugin available
Project GraphBasic dependency awarenessAdvanced visualization and analysis
CI/CD IntegrationStrong with parallel executionExcellent with affected projects support
ConfigurationPrimarily through turbo.jsonSpread across nx.json, project.json files

For teams already heavily invested in the Vue ecosystem, both tools can be viable options. Turborepo might be preferable for smaller to medium-sized teams looking for a lightweight solution that focuses primarily on build performance. Nx often makes more sense for larger organizations needing enterprise-grade features, extensive code generation capabilities, and more sophisticated project orchestration.

Project Structure and Organization

Establishing a logical and scalable project structure is fundamental to a successful Vue.js monorepo. A well-organized repository ensures that as your codebase grows, developers can easily locate code, understand dependencies, and maintain clear boundaries between different parts of the system.

The most common monorepo structure separates applications from shared packages:

monorepo-root/
├── apps/
│   ├── admin-web/          # Vue admin application
│   ├── marketing-site/     # Next.js documentation site
│   └── customer-portal/    # Another Vue application
├── packages/
│   ├── ui/                 # Shared UI component library
│   ├── utils/              # Utility functions
│   ├── composables/        # Vue composables
│   └── types/              # TypeScript type definitions
├── package.json
├── turbo.json              # Turborepo configuration
├── nx.json                 # Nx configuration (if using Nx)
└── pnpm-workspace.yaml     # Defines workspace packages

This structure clearly distinguishes between runnable applications (located in the apps/ directory) and reusable library code (located in the packages/ directory). Each application and package should be self-contained with its own package.json file, dependencies, and build scripts while leveraging the monorepo tooling to manage interdependencies.

Within the packages directory, consider further categorizing your libraries based on their purpose and domain:

  • UI components: Reusable Vue components with their styles and tests
  • Composables: Vue composition API functions for stateful logic
  • Utilities: Pure JavaScript/TypeScript functions without framework dependencies
  • Configurations: Shared ESLint, Prettier, TypeScript, and Tailwind configurations
  • Types: Shared TypeScript interfaces and type definitions

A critical aspect of maintaining this structure is properly configuring your package manager workspace. For pnpm, this involves creating a pnpm-workspace.yaml file that defines the locations of your packages:

packages:
  - "apps/*"
  - "packages/*"

This configuration ensures that when you run commands from the root, the package manager recognizes all your applications and packages as part of the same workspace, enabling efficient dependency hoisting and cross-package linking.

Implementation with Turborepo and Vue

Initial Setup and Configuration

Setting up a Vue.js monorepo with Turborepo begins with establishing the foundational workspace structure. First, create your root directory and initialize the project:

mkdir vue-monorepo
cd vue-monorepo
pnpm init

Next, create a pnpm-workspace.yaml file to define your workspace structure:

packages:
  - "apps/*"
  - "packages/*"

Now, install Turborepo as a development dependency:

pnpm add -D turbo

Create a turbo.json configuration file to define your pipeline—the heart of Turborepo’s task orchestration:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["^test"]
    }
  }
}

This configuration defines four key tasks: builddevlint, and test. The dependsOn field with the ^ prefix specifies that each task depends on the same task being completed in all dependencies first, ensuring proper build order.

Creating Vue Applications with Vite

With the workspace configured, create your Vue applications using Vite. From the root directory, execute:

cd apps
pnpm create vue@latest admin-app
# Select the features you need: TypeScript, Router, Pinia, etc.
cd admin-app
pnpm install

Repeat this process for additional applications you need in your monorepo. For each Vue application, you’ll need to configure Vite to properly handle monorepo imports. Update each app’s vite.config.ts:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@repo/ui': path.resolve(__dirname, '../../packages/ui/src'),
    },
  },
})

This configuration creates aliases that point to your shared packages, ensuring that imports from within your applications correctly resolve to the source code in your packages directory.

Building Shared UI Libraries

A key benefit of monorepos is the ability to share UI components across multiple applications. Create a shared UI library in the packages directory:

cd packages
mkdir ui
cd ui
pnpm init

Update the generated package.json to define proper exports:

{
  "name": "@repo/ui",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": "./src/index.ts",
    "./components/*": "./src/components/*",
    "./composables/*": "./src/composables/*"
  },
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  },
  "dependencies": {
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "vite": "^6.0.0",
    "@vitejs/plugin-vue": "^5.0.0"
  }
}

The exports field allows consumers to import directly from subpaths, providing a clean API for your library. Create a vite.config.ts for your UI library specifically tailored for library builds:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'UiLibrary',
      fileName: (format) => `ui-library.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

Create an src/index.ts file that exports all public components and utilities:

export { default as Button } from './components/Button/Button.vue'
export { default as Card } from './components/Card/Card.vue'
export { default as Modal } from './components/Modal/Modal.vue'
// Export composables and other utilities
export { useToast } from './composables/useToast'
export { useLocalStorage } from './composables/useLocalStorage'

Connecting Dependencies and TypeScript

To ensure proper type checking and IntelliSense across your monorepo, configure TypeScript path mapping. Create a tsconfig.json in each application that extends from a shared base configuration:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@repo/ui": ["../../packages/ui/src"],
      "@repo/ui/*": ["../../packages/ui/src/*"],
      "@repo/utils": ["../../packages/utils/src"],
      "@repo/utils/*": ["../../packages/utils/src/*"]
    }
  },
  "include": ["src/**/*", "../../packages/ui/src/**/*"]
}

A base tsconfig.base.json at the root can define shared compiler options:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

Finally, in each application, add your shared packages as dependencies in their package.json files using the workspace protocol:

{
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}

The workspace:* protocol ensures that packages are always symlinked to the latest version within your monorepo, facilitating rapid development and testing.

Implementation with Nx and Vue

Setting Up Nx Workspace

Implementing a Vue.js monorepo with Nx offers a more structured approach with advanced tooling capabilities. To start, create a new Nx workspace:

pnpx create-nx-workspace@latest vue-monorepo

During the setup process, you’ll be prompted to choose a preset. Select the “integrated” monorepo style for full Nx features, or “package-based” for a lighter setup. For Vue.js development, the integrated approach is generally recommended as it provides more powerful capabilities.

If you’re adding Nx to an existing monorepo, you can initialize it with:

pnpm add -D nx
nx init

This command will set up the necessary configuration files while preserving your existing structure. Nx will automatically detect your existing applications and packages.

Configuring Vue Support in Nx

After setting up the workspace, add official Vue.js support by installing the Nx Vue plugin:

pnpm add -D @nx/vue

With the plugin installed, you can generate new Vue applications using Nx’s code generators:

nx g @nx/vue:app admin-app

This command scaffolds a complete Vue application with Vite, TypeScript, and testing already configured. The generator creates the application structure, sets up the necessary configuration files, and ensures the new application is properly integrated into the Nx project graph.

Similarly, generate library projects for shared code:

nx g @nx/vue:lib ui
nx g @nx/vue:lib utils
nx g @nx/vue:lib composables

Each library comes pre-configured with the appropriate build settings, TypeScript configuration, and testing setup. The Nx Vue plugin automatically configures Vite for both applications and libraries, ensuring optimal build performance and proper dependency handling.

Vite Configuration for Nx Monorepos

When using Nx with Vite, special configuration is needed to ensure proper monorepo support. For Vue applications, your vite.config.ts should look like this:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'

export default defineConfig({
  root: __dirname,
  plugins: [vue(), nxViteTsPaths()],
  build: {
    outDir: '../../dist/apps/admin-app',
    reportCompressedSize: true,
    commonjsOptions: {
      transformMixedEsModules: true,
    },
  },
})

The nxViteTsPaths() plugin is crucial—it ensures that TypeScript path mappings from your tsconfig files are properly resolved by Vite. This allows for seamless imports between packages within your monorepo without needing relative paths.

For library builds, the configuration differs slightly:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'
import dts from 'vite-plugin-dts'
import path from 'path'

export default defineConfig({
  root: __dirname,
  plugins: [
    vue(),
    nxViteTsPaths(),
    dts({
      entryRoot: 'src',
      tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
      skipDiagnostics: true,
    }),
  ],
  build: {
    outDir: '../../dist/packages/ui',
    lib: {
      entry: 'src/index.ts',
      name: 'ui',
      fileName: 'index',
      formats: ['es', 'cjs'],
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue',
        },
      },
    },
  },
})

The vite-plugin-dts plugin generates TypeScript declaration files (.d.ts) for your library, essential for providing type safety to consumers. The skipDiagnostics: true option can speed up builds by skipping type checking during the build process, though you may set this to false for more thorough type validation.

Leveraging Nx Project Graph and Affected Commands

One of Nx’s most powerful features is its project graph, which automatically analyzes and visualizes dependencies between all applications and libraries in your monorepo. You can view the graph with:

nx graph

This command opens a browser-based visualization showing how all projects interconnect, helping you understand dependency relationships and identify potential issues.

Even more valuable is Nx’s affected commands system, which allows you to run tasks only on projects impacted by recent changes:

# Build only projects affected by changes since the main branch
nx affected --target=build

# Test affected projects
nx affected --target=test

# Run linting on affected projects
nx affected --target=lint

You can specify different base commits for comparison:

# Run lint for packages changed between current state and 5 commits ago
nx affected --target=lint --base=HEAD~5 --head=HEAD

This capability significantly reduces CI times by avoiding unnecessary builds and tests of unchanged code. For larger teams, Nx Cloud offers remote caching, allowing build results to be shared across your organization’s CI system and between developers.

Advanced Monorepo Considerations

Optimizing Caching and Build Performance

Both Turborepo and Nx excel at optimizing build performance through sophisticated caching mechanisms. In Turborepo, you can enhance your caching strategy by carefully defining inputs and outputs for each task:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*", "vite.config.ts", "tsconfig.json"],
      "outputs": ["dist/**"],
      "env": ["NODE_ENV"]
    }
  }
}

The inputs key specifies which files should be considered when determining whether a task needs to be rerun, while outputs defines where the task produces artifacts. Environment variables listed in the env array are also included in the cache key.

For Nx, similar caching configuration can be defined in nx.json or in individual project.json files:

{
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
    "production": ["!{projectRoot}/**/*.test.ts"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "outputs": ["{projectRoot}/dist"]
    }
  }
}

Nx’s inputs configuration allows for even more granular control, enabling you to define different input sets for development vs. production builds.

Microfrontends with Monorepos

Monorepos provide an excellent foundation for implementing microfrontend architecture. With Vue and Vite, you can develop multiple independent applications that share common dependencies and can be deployed separately.

When configuring microfrontends in a monorepo, pay special attention to the Vite base property for child applications:

// apps/microfrontend/vite.config.ts
export default defineConfig({
  base: process.env.NODE_ENV === 'production' 
    ? 'https://cdn.yourdomain.com/microfrontend/' 
    : '/',
  // ... other config
})

This ensures that assets like images and CSS are loaded from the correct location when the application is deployed. For microfrontend communication, consider implementing a lightweight event bus or leveraging the native browser module federation when using Webpack.

Component-Driven Development Approach

Adopting a component-driven development (CDD) approach within your Vue monorepo can significantly enhance reusability and team collaboration. As noted in the Bit documentation, “CDD is a way of working in which you structure your software into independent components, where each component addresses a specific concern and exposes a well-defined interface”.

Tools like Bit can help implement CDD within your monorepo by providing:

  • Independent versioning for each component
  • Isolated development and testing environments
  • Automated dependency management between components
  • Streamlined publishing of individual components

To create Vue components with Bit:

bit create vue components/nav-bar
bit create vue components/data-table
bit create vue components/search-filters

Each component is developed, built, and versioned independently while remaining within your monorepo structure. This approach combines the coordination benefits of a monorepo with the isolation advantages of a polyrepo setup.

Troubleshooting Common Issues

Even with proper setup, monorepo development can present challenges. Here are solutions to common issues:

Path Resolution Problems: One of the most frequent issues in monorepos is incorrect path resolution, particularly with Vite builds. If you encounter “Module not found” errors for monorepo imports, ensure your Vite configuration includes the proper plugins. For Nx, the nxViteTsPaths() plugin is essential. For Turborepo setups, manually configure aliases in your Vite configuration:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: [
      { 
        find: '@repo/ui',
        replacement: path.resolve(__dirname, '../../packages/ui/src')
      },
    ],
  },
})

TypeScript Path Mapping: TypeScript might not recognize path mappings from outside the current project. Ensure each application’s tsconfig.json properly extends your base configuration and includes path mappings that correctly resolve to package source directories:

{
  "compilerOptions": {
    "paths": {
      "@repo/ui": ["../../packages/ui/src"],
      "@repo/ui/*": ["../../packages/ui/src/*"]
    }
  }
}

Dependency Version Conflicts: Monorepos can sometimes experience version conflicts for shared dependencies. Use your package manager’s dependency hoisting capabilities and ensure consistent versions across packages. Both pnpm and yarn support hoisting dependencies to the root node_modules directory, reducing duplication and ensuring singleton packages work correctly.

Circular Dependencies: These can be particularly problematic in monorepos. Use Nx’s project graph (nx graph) or tools like madge to detect circular dependencies between packages. Refactor shared code into separate packages or reconsider package boundaries to resolve these issues.

Build Performance Degradation: As your monorepo grows, you might experience slower builds. Both Turborepo and Nx offer remote caching solutions that can share cache artifacts across CI runs and between team members, significantly reducing build times. Consider implementing these solutions for larger teams.

By addressing these common issues proactively, you can maintain a smooth development experience and maximize the benefits of your Vue.js monorepo setup.

Conclusion

Implementing a monorepo setup with Vue.js, Vite, and Turborepo or Nx represents a powerful approach to managing complex frontend architectures. This comprehensive guide has demonstrated how to establish a scalable project structure, configure build tooling, share code effectively, and optimize performance through intelligent caching.

The choice between Turborepo and Nx ultimately depends on your team’s specific needs—Turborepo offers a lightweight, performance-focused approach, while Nx provides a more comprehensive monorepo management system with advanced features like code generation and project visualization.

As you embark on your monorepo journey, remember that successful adoption requires not just technical implementation but also organizational buy-in and process adjustments. Start with a clear understanding of your project requirements, establish conventions early, and gradually migrate existing codebases to avoid disruption.

With the strategies outlined in this guide, your team can leverage the full potential of monorepos to accelerate development, improve code quality, and build more maintainable Vue.js applications.

References

  1. The Ultimate Guide to Building a Monorepo in 2025: Sharing Code Like the Pros – Comprehensive guide to monorepo setup with modern tooling.
  2. Start with an example – Turborepo – Official Turborepo examples and templates.
  3. Creating a scalable Monorepo for Vue – Libs vs Apps – Practical guidance on structuring Vue monorepos.
  4. Vite – Turborepo – Official guide for integrating Vite with Turborepo.
  5. Configure Vite on your Nx workspace – Nx documentation for Vite configuration.
  6. Build Ultra-Fast Dev Loops with Vite & Monorepos – Performance optimization strategies.
  7. Vite + Turborepo Issue – Common troubleshooting scenarios for Vite in monorepos.
  8. Complete Vue3 monorepo Discussion – Community discussion on Vue monorepo setup.
  9. Building a Vue.js Monorepo in 2023 – Component-driven development approaches with Bit.
  10. Creating a scalable Monorepo for Vue – Nx – Nx-specific implementation guide for Vue monorepos.