Nuxt with Turborepo 💨
There are only good things about this setup. Your Nuxt.js app will be definitely grateful! Try it out now, no regrets.
Hey! Before you walk away, wondering why you’d ever need a monorepo for your small and not-so-complicated Nuxt application, stick with me for a moment. I promise to keep it light and only share practical, no-nonsense reasons that might just change your mind. I promise, only legit arguments.
As for now - to me - a natural way of setting up a new application environments is to put them into the monorepo surroundings. What for you’d ask?! Let me describe you a couple of potentialities where it can be super beneficial for you and your future development process.
Multiple Applications
Let’s say you start with a single, small Nuxt app, happily living in its own repository. But as time goes on, the app grows. New features, dependencies, and complexities start piling up. At some point, you realize that to properly document everything—its structure, API, and best practices - you need dedicated documentation.
Now, mixing that documentation directly within your main app’s codebase? Not the best idea. Instead, you decide to create a separate Nuxt-powered documentation site using the Nuxt Content module, generating a static site for all your docs.
But here’s the catch: your original app is dynamic - it’s not meant to be static, and you don’t want to introduce unnecessary complexity by juggling different rendering strategies in the same project.
And that’s where a monorepo makes perfect sense. You keep both applications close but separate, maintaining clear boundaries while still benefiting from shared dependencies, streamlined development, and a unified workflow. Sounds like a solid reason to go monorepo, right? Let’s go further.
Shared Configuration
Now, imagine both apps living together in a single repository. While they serve different purposes, they share a lot in terms of structure and configuration - things like ESLint, Prettier, TypeScript, and even Tailwind. Duplicating these configurations across both apps? Not ideal.
This is where a monorepo shines. Instead of maintaining separate, nearly identical setups, you can create shared packages for common configurations and utilities, making them easily reusable across both apps. This not only reduces duplication but also keeps everything consistent and easier to maintain.
With a monorepo, you gain efficiency, better organization, and a cleaner development workflow - all while keeping your apps close, but neatly separated.
Nuxt Extensibility
Finally, let’s talk about Nuxt’s extensibility system. For years, Modules have been the go-to way to extend Nuxt applications with additional functionality. Traditionally, we could place them directly inside the modules/
directory of a project, but in most cases, they’ve been distributed as external npm packages, maintained by the community.
Now, with the latest updates, the Modules API is explicitly designed for building external packages rather than keeping them inside the core application code. In fact, even the Nuxt team recommends placing them as packages within a monorepo.
The same thing goes for Layers - Nuxt’s approach to micro frontends. These "mini-apps" extend your main Nuxt application and are inherently structured to be packaged separately. Their architecture naturally fits within a monorepo, and they can even be published as standalone npm packages if needed.
By embracing a monorepo setup, you’re not just organizing your project better - you’re aligning with Nuxt’s best practices for scalability and maintainability.
Not convinced yet? Let me introduce you some of the great Turborepo capabilities, as it might be the best choice out there to handle monorepos.
But first - why Turborepo? After all, there are other solid alternatives, right? Well… not quite. Take Lerna. It’s been around for a long time, well-established in the dev community. But over the years, as technology evolved, its performance didn’t keep up. Lerna was primarily designed for managing package-based monorepos, rather than a mix of packages and real applications. Eventually, it was abandoned by its author and later acquired by Nx, rewritten with its engine involved. Now, if you check its GitHub repository, you'll notice that development activity has significantly slowed down - a bit of a bummer.
Then there's Nx - a powerful, feature-packed tool. It’s huge, advanced, and highly capable, but it's more geared toward enterprises rather than lean startup projects or smaller teams. Not ideal. Finally, there are some tools from MS, and/or Amazon, it’s hard to think about them seriously since they’re an internal enterprise solutions. So, where does Turborepo fit in? Let’s see what it brings to the table in comparison.
Why you should pick Turborepo?
Caching. It has great (quite advanced) caching system that is capable to speed-up development process significantly. Once a task is executed (like building, linting, or testing), the results are cached, meaning future runs can be near-instant if nothing has changed. So imagine that you have co-existing packages powering your application (Nuxt module), you don’t need to build them every time when you’re building your main app. Turborepo will provide cached already-built packages for your final execution. What’s more interesting you can even have remote caching, thanks to which packages can be shared with your team. ✨
Parallel Execution. You don’t have to wait for a single process to finish - you can run multiple processes simultaneously. And to avoid getting caught in dependency traps, you have pipelines to manage execution efficiently.
Pipelines. You can design pipelines for any part of your development process. Whether it's building, running apps, or managing packages, you have full control over execution order. You decide which tasks should run first to provide the necessary resources for other parts of your monorepo.
Polymorphic Nature. Unlike Lerna or Bazel, Turborepo isn’t just built to manage packages - it’s designed to deliver and integrate those packages directly into your applications, making them the primary source and backbone of your system.
Historical Outline
The project was initiated by Jared Palmer, who identified limitations in existing monorepo ecosystem like Lerna (daa), especially concerning performance and scalability. He aimed to create a tool that could handle complex monorepos with advanced features. Initially, Turborepo was developed using Go. However, to leverage performance benefits, the team transitioned to Rust, resulting in significant improvements in speed and efficiency. In December 2021, shortly after its release, Vercel acquired Turborepo, integrating it into their suite of developer tools to enhance performance and scalability for frontend applications.
OK. If you’re still not convinced, maybe you’ll just give it a quick try? I can promise you that the setup and workspace organization will be fast and easy. Additionally, on top of our starting guide, you’ll be ready to go, building with your new and shiny environment on top of Turborepo. Deal? Let’s go!
Firstly, let’s make a small rearrangement within your root directory. Create there two new folders: packages/
and apps/
. Inside apps/
create a new one called web/
- here will be the best place for your main Nuxt app, so please move it there.
In the root please leave the .gitignore
file, you can add to it turbo-specific folders that will held the cache and configs.
# turborepo specific
.turbo
.history
# pnpm specific
~
~/*
OK, let’s initialize the main package.json
file. Use pnpm for that.
pnpm init
pnpm
will also need to have a workspaces.yaml
file to inform Turborepo where you’re actually storing your apps and packages. Create it inside the root folder and place there this code.
packages:
- 'apps/**'
- 'packages/**'
Then you need to install the thing. Run this command.
pnpm add -D turbo
Now, when the turbo is already installed, it needs a configuration file. Please, inside your root directory create a new file turbo.json
. Insert there this content.

What we have here? $schema
is the internal Turborepo set of configs so you can leave it as it is. Then you have tasks
object. This is where you’re defining your processes and pipelines. Name of tasks should be corresponding with the name of your scripts, so if you’re using script dev
to run your development process define it like that, same for building and/or linting. On top of this Turborepo will scan all of your apps and packages for package.json
files and will run the dev scripts from them, parallelly. Each task can have its own pipeline defined to be processed in the desired way along with the other components of your monorepo. Here you can read more about defining it/them step by step.
By defining outputs
you’re telling Turborepo that should store this exact distribution folder and reuse it within the next execution. You can always force the rebuild without/leaving cached resources.
Now, we need to add these tasks to our main packege.json file.
One more thing that you need to do is to move the lint-staged
configuration from the Nuxt folder to the main/root one. Now you’ll be linting all of the apps and packages within your repository so you have to define how scan them all together. Do it like this.
Alright, I believe you can test it now. In your root directory run this command.
pnpm run dev
What you’ll see it will be a Nuxt app process running straight from the main scope.
yourmonorepo/web:dev: Nuxt 3.15.4 with Nitro 2.10.4
Awesome! You now have yourmonorepo
(scope name, the name of your main package, please change it) with Turborepo ready and going. Along the way, while we will develop our codebase adding a new apps and packages we will be extending Turbo configuration and how it behaves. If you don’t want to miss any of the future publications please subscribe to stay up to date.
Last but not least what we can do here - it can be a great reference for the further improvements - will be creation of a global config for ESLint. Interested?
Inside your packages/
folder create a new one: config/
, and inside of it add a new one named eslint/
. Now we need to initialize a new package configuration. Run the pnpm init
command. New package.json
file will be created. Inside you can define something like this.
OK, as you can see we’re referring here to the index.base.mjs
file. We don’t have it so let’s create one. Inside newly created please add this content.
You’ll notice that it’s almost the same config that we’ve had for our Nuxt application. That’s right, we’re globalizing it so common parts were moved.
Additionally, from the Nuxt package please move the dependencies defined within the devDependencies
from the package.json
file that you see above. From now one the eslint-config
package will be installing them.
Great, we have a new, global package with ESLint config that we can now share across the whole repository. So let’s proceed and install it for our Nuxt application. Inside the apps/web/package.json
file add this notation to your devDependencies
object.
"@yourmonorepo/eslint-config": "1.0.0",
You may also add workspace:*
instead of the version number, but while publishing the package to the remote catalog you will get issues since this will be an internal (workspace) package.
Now, we need some small edits around the Nuxt’s app ESLint config.
As you can see almost everything remains the same, the only one difference is that now we’re importing the eslintBaseConfig
from our global ESLint config package, finally extending it with our Nuxt-specific rules. Imagine that for every future app created inside our repository we will be able to do the same thing, never struggling with different ESLint rules. Perfect!
Two more things. Change this inside your main package.json
file.
{
"lint-staged": {
"*.*": "turbo lint --"
},
}
Because we’re defining locally per project which kind of files we went to scan it’s not needed anymore to determinate it along for the lint-staged
proces. I stumble upon the issue of analyzing files as a tasks by Turborepo. To avoid this problem please add —-
to your turbo lint
command, this should resolve the issue.
That’s it! Now you can proceed with the other configs like Prettier or future-added Tailwind, or any other that you’ll be sharing across the repository.
What’s next? Docs, we need docs! Why? Check out the next post to learn more. And to not miss any please subscribe to this publication. Thanks and enjoy, Lukas.
Please let me know how you find this article. What I can improve?