Lesson 57 of 81
Running a Next.js app alongside shared packages in one Turborepo — what task caching actually speeds up, and the two ways a Next.js build inside that setup quietly serves stale code.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A monorepo puts multiple packages — your Next.js app, a shared ui component library, a shared config package — in one repository, wired together with your package manager's workspace protocol ("@acme/ui": "workspace:*"). Turborepo sits on top of that and does one job: it figures out which packages' build/lint/test tasks actually need to re-run based on what changed, and caches the output of the ones that don't, locally and optionally on a remote cache shared by the whole team.
For a Next.js app specifically, this means two things layer together: the workspace protocol handles resolving @acme/ui to the local package folder, and Turborepo's task graph handles not re-running next build when nothing that build depends on has changed. Get the task graph's dependency declarations wrong and you get the worst of both worlds — a build that looks fast because it hit cache, serving code that's actually stale.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
"dependsOn": ["^build"] is the load-bearing line: the ^ means "build every package this one depends on first." Without it, Turborepo might build the Next.js app before the @acme/ui package it imports from has been built, and the app either fails to resolve the import or, worse, resolves against a stale build sitting in @acme/ui/dist from an earlier run.
The Next.js app also has to opt in to treating the workspace package as first-party source, not an opaque pre-built dependency:
// apps/web/next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
transpilePackages: ["@acme/ui"],
};
transpilePackages tells Next.js's own compiler to process that package's source directly rather than assuming it already ships browser-ready, pre-transpiled JavaScript — which matters because most internal workspace packages ship raw TypeScript, not a built dist folder, to avoid every consumer needing its own build step.
outputs or the file hash Turborepo keys on doesn't actually cover everything the build depends on, you can get a fast, green, wrong build — the danger isn't slowness, it's silent staleness.NEXT_PUBLIC_* value gets inlined into the client bundle at build time; if it differs between environments but the task's cache key doesn't account for it, a teammate can pull a cached build baked with someone else's environment's values.transpilePackages isn't automatic for every workspace dependency. Forgetting to list a package that ships raw TypeScript produces a real module-resolution or syntax error at build time that has nothing to do with the code you actually changed — it reads like a mystery bug in an unrelated file.Turborepo doesn't replace anything about how Next.js itself compiles a route or builds a page — it wraps the same next build this track's build-system lesson already covers and decides when to skip re-running it. Understanding what actually triggers a rebuild inside a single Next.js app is what makes the monorepo-level caching layer legible instead of magic.