<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cengiz Dönmez</title>
    <description>The latest articles on DEV Community by Cengiz Dönmez (@cengizdonmez).</description>
    <link>https://dev.to/cengizdonmez</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3904948%2F63207e07-9d9e-429c-bffb-61f5e6f648fe.jpg</url>
      <title>DEV Community: Cengiz Dönmez</title>
      <link>https://dev.to/cengizdonmez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cengizdonmez"/>
    <language>en</language>
    <item>
      <title>Share Everything Except the Route Tree: A Multi-Customer Monorepo with TanStack Router</title>
      <dc:creator>Cengiz Dönmez</dc:creator>
      <pubDate>Thu, 02 Jul 2026 00:56:35 +0000</pubDate>
      <link>https://dev.to/cengizdonmez/share-everything-except-the-route-tree-a-multi-customer-monorepo-with-tanstack-router-2o5d</link>
      <guid>https://dev.to/cengizdonmez/share-everything-except-the-route-tree-a-multi-customer-monorepo-with-tanstack-router-2o5d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How we stopped copy-pasting a whole admin panel to every customer.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We build a white-label admin/CMS panel. Every customer gets the same core: users, roles, permissions, content management, multi-language support. On top of that, each customer gets a handful of features only they need. Say, a real-estate portal wants a listings module, or a logistics company wants shipment tracking. (I'll use these two fictional customers as the running example throughout.)&lt;/p&gt;

&lt;p&gt;For years, our "architecture" for this was &lt;code&gt;git clone&lt;/code&gt;. Every new customer meant a fresh fork of the whole panel. It works great, right up until you have ten forks and you fix a bug in the permission system. Now you get to port that fix ten times, by hand, into ten codebases that have quietly drifted apart. We were spending more time porting than building.&lt;/p&gt;

&lt;p&gt;This is the story of how we collapsed those forks into one pnpm monorepo (one shared core, one thin app per customer), and specifically how we made &lt;strong&gt;TanStack Router&lt;/strong&gt; work in a setup that its own GitHub issues suggest it can't handle. Spoiler: it can, as long as you respect one rule. It's in the title.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints that shaped everything
&lt;/h2&gt;

&lt;p&gt;Before drawing boxes, we wrote down what was actually fixed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each customer has their own backend, own database, own domain.&lt;/strong&gt; So one app = one backend = one &lt;code&gt;VITE_API_URL&lt;/code&gt;. There is no shared tenancy to exploit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branding, panel language, and permissions already come from the backend at runtime&lt;/strong&gt; (a settings endpoint, an i18n endpoint, JWT claims). The frontend shell is genuinely thin: in practice, one env variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bespoke features are real code, not config.&lt;/strong&gt; The listings module is a full feature with its own types, services, hooks and pages. And it must &lt;em&gt;never&lt;/em&gt; appear in another customer's bundle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last constraint killed the obvious alternative: a single deployed SaaS with feature flags. Feature flags hide UI; they don't keep a customer's bespoke code out of everyone else's JavaScript. With per-customer backends anyway, per-customer builds were the honest model. What we needed to fix was the &lt;em&gt;duplication&lt;/em&gt;, not the separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target shape
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backoffice/                       ← one git repo
├── packages/
│   └── core/                     ← @bo/core, ALL shared panel code
│       └── src/
│           ├── features/         ← users, roles, content, i18n, ...
│           ├── shared/           ← ui, lib, guards, hooks
│           ├── app/              ← layouts, providers, http, config
│           └── platform/         ← the seams: appConfig registry, bootstrap
├── apps/
│   ├── customer-a/               ← thin shell + bespoke `listings`
│   ├── customer-b/               ← thin shell + bespoke `shipments`
│   └── _template/                ← scaffold for the next customer
└── scripts/                      ← new-app.mjs, sync-core-routes.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes an app &lt;em&gt;an app&lt;/em&gt; is exactly three things: which backend (&lt;code&gt;VITE_API_URL&lt;/code&gt;), which modules are enabled, and which bespoke code it carries. Everything else is core.&lt;/p&gt;

&lt;p&gt;Three mechanisms make this work. Two were easy. One ate a week of research.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 1: core is a source package, no build step
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@bo/core&lt;/code&gt; never gets compiled on its own. Its &lt;code&gt;package.json&lt;/code&gt; exports raw &lt;code&gt;.tsx&lt;/code&gt; source, and each app's Vite compiles it as if it were the app's own code. Turborepo calls this an &lt;a href="https://turborepo.dev/docs/core-concepts/internal-packages" rel="noopener noreferrer"&gt;"internal" or just-in-time package&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@bo/core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"./*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src/*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apps depend on it with &lt;code&gt;"@bo/core": "workspace:*"&lt;/code&gt;, which pnpm resolves to a symlink. The payoff is enormous for a small team: no versioning, no publish step, no stale builds. Edit a core component and it hot-reloads instantly inside whichever customer app you're running.&lt;/p&gt;

&lt;p&gt;The accepted tradeoff: a type error in core surfaces in the &lt;em&gt;app's&lt;/em&gt; typecheck. We consider that a feature. It fails at the first &lt;code&gt;pnpm build&lt;/code&gt; instead of at publish time.&lt;/p&gt;

&lt;p&gt;Two Vite gotchas cost us an afternoon each, so here they are for free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/*/vite.config.ts&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;searchForWorkspaceRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;())]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// dev server may serve ../../packages/core&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="c1"&gt;// React Compiler runs via Babel. Its include must match core's source too,&lt;/span&gt;
&lt;span class="c1"&gt;// or core components silently skip compilation:&lt;/span&gt;
&lt;span class="nf"&gt;babel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;src&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;\.[&lt;/span&gt;&lt;span class="sr"&gt;jt&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;sx&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;reactCompilerPreset&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mechanism 2: core never knows who it's running for
&lt;/h2&gt;

&lt;p&gt;Core must stay pure: no customer names, no hardcoded module lists. But core's layout needs the navigation, and core's guards need to know which modules are enabled. Classic dependency inversion. The app &lt;em&gt;tells&lt;/em&gt; core who it is, once, at bootstrap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/core/src/platform/appConfig.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;enabledModules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NavItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setAppConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getEnabledModules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* throws if not set */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/customer-a/src/app.config.ts, the customer's entire identity&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;enabledModules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;roles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;coreNavigation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;listingsNav&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A disabled module 404s at the route level (&lt;code&gt;requireModule&lt;/code&gt; in &lt;code&gt;beforeLoad&lt;/code&gt;) and disappears from the sidebar. There's a free future hiding in this seam, too: if the backend ever starts serving &lt;code&gt;enabledModules&lt;/code&gt;, only the registry's source changes. A one-file migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism 3: the route tree, where it got hard
&lt;/h2&gt;

&lt;p&gt;Here's what we refused to give up from TanStack Router: file-based routing, automatic code splitting, &lt;code&gt;beforeLoad&lt;/code&gt; guard chains, and above all &lt;strong&gt;type-safe search params&lt;/strong&gt;. Our entire URL-state pattern (table filters, pagination, everything) rests on &lt;code&gt;validateSearch&lt;/code&gt; + &lt;code&gt;Route.useSearch()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And here's the problem: TanStack Router generates a &lt;code&gt;routeTree.gen.ts&lt;/code&gt; per project, and that generated tree &lt;strong&gt;does not survive a package boundary&lt;/strong&gt;. If you go looking, you find a trail of pain: &lt;a href="https://github.com/TanStack/router/issues/1203" rel="noopener noreferrer"&gt;#1203&lt;/a&gt; (sharing route trees across packages) and &lt;a href="https://github.com/TanStack/router/issues/4984" rel="noopener noreferrer"&gt;#4984&lt;/a&gt; (virtual routes can't resolve package aliases in a monorepo; still open, unanswered, and its author eventually gave up and switched to React Router). There's a whole genre of blog posts titled some variation of "why TanStack Router may not be the best choice for monorepos".&lt;/p&gt;

&lt;p&gt;We evaluated three ways to share routes and eliminated all three. One of them &lt;em&gt;after&lt;/em&gt; building it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Virtual File Routes.&lt;/strong&gt; The cleanest on paper: routes declared once, in core. In practice, &lt;a href="https://github.com/TanStack/router/issues/4984" rel="noopener noreferrer"&gt;#4984&lt;/a&gt; is exactly our setup: package aliases in virtual route declarations resolve relative to the app directory and break. Unusable today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route-config stubs.&lt;/strong&gt; Core exports &lt;code&gt;{ validateSearch, beforeLoad }&lt;/code&gt; objects, app route files spread them in. We &lt;em&gt;built&lt;/em&gt; this. It worked, builds were green, and we reverted it the same day. Every route became an exercise in indirection: to understand one screen you now read two files in two packages. Not worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code-based routing.&lt;/strong&gt; The officially blessed monorepo pattern (a shared &lt;code&gt;router&lt;/code&gt; package, a &lt;code&gt;routerMap&lt;/code&gt; of components per app). It works, but it means abandoning file-based DX and automatic code splitting wholesale. Too much, too early.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The model that works: don't share the tree at all
&lt;/h3&gt;

&lt;p&gt;The realization that unlocked everything: &lt;strong&gt;the route &lt;em&gt;tree&lt;/em&gt; is the only thing that can't cross the package boundary, so let it be the one thing that doesn't.&lt;/strong&gt; Share the pages, the guards, the search validators, the layouts. Duplicate the thin wiring.&lt;/p&gt;

&lt;p&gt;Every app owns its physical &lt;code&gt;src/routes/&lt;/code&gt; directory and generates its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;routeTree.gen.ts&lt;/code&gt;. A route file is deliberately boring, pure wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/customer-a/src/routes/_authenticated/users/index.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createFileRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requirePermission&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@bo/core/shared/lib/permission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requireModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@bo/core/app/config/modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UsersPage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@bo/core/features/users/components/UsersPage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_authenticated/users/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;beforeLoad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;requireModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;requirePermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users.List&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;validateSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicated wiring needs a source of truth, so ours lives in &lt;code&gt;apps/_template&lt;/code&gt;: a complete app shell with every core route file. A ~30-line script (&lt;code&gt;cpSync&lt;/code&gt;, essentially) propagates template routes to every app. It never touches bespoke routes, because they don't exist in the template. New customer = copy the template, set one env var, edit &lt;code&gt;enabledModules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two supporting tricks keep the model honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core pages never import app route files.&lt;/strong&gt; That would be a reverse dependency; core can't know app paths. Instead, a core page binds to its route with &lt;code&gt;getRouteApi('/_authenticated/users/')&lt;/code&gt; at module level. File-based DX preserved, dependency arrow pointing the right way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config files that also live per-app&lt;/strong&gt; (&lt;code&gt;vite.config.ts&lt;/code&gt;) ride along in the same sync script. Anything duplicated needs a propagation story, or it &lt;em&gt;will&lt;/em&gt; drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Proving it before merging
&lt;/h2&gt;

&lt;p&gt;Claims are cheap, so before this hit &lt;code&gt;main&lt;/code&gt; we verified the three promises the architecture makes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation.&lt;/strong&gt; Bespoke code must not leak. After building everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"api/Listing"&lt;/span&gt;  apps/customer-a/dist   &lt;span class="c"&gt;# ✓ found&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"api/Listing"&lt;/span&gt;  apps/customer-b/dist   &lt;span class="c"&gt;# ✗ nothing&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"api/Shipment"&lt;/span&gt; apps/customer-b/dist   &lt;span class="c"&gt;# ✓ found&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"api/Shipment"&lt;/span&gt; apps/customer-a/dist   &lt;span class="c"&gt;# ✗ nothing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Propagation.&lt;/strong&gt; A change in &lt;code&gt;packages/core/src/shared/ui&lt;/code&gt; shows up in every app's next build with zero manual steps. It's a symlink; there is nothing to forget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No drift.&lt;/strong&gt; &lt;code&gt;diff -rq&lt;/code&gt; between the template's routes and each app's routes returns only the bespoke directories. The sync model holds.&lt;/p&gt;

&lt;p&gt;Plus the boring-but-essential checks: one React instance across the workspace (pnpm hoisting plus aligned versions; the "invalid hook call" error is the monorepo rite of passage), a green &lt;code&gt;tsc&lt;/code&gt;, zero-warning lint under a single root ESLint config, and the unit/schema test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest ledger
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Costs we accepted:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core route wiring exists N times (once per app), managed by a sync script and one team rule: &lt;em&gt;route changes happen in the template, never in a single app.&lt;/em&gt; This is a process guarantee, not a compiler guarantee. We're fine with it at our scale; we'd want a CI drift check before we're fine with it at 20 apps.&lt;/li&gt;
&lt;li&gt;A new app's first build has a chicken-and-egg problem: &lt;code&gt;tsc&lt;/code&gt; runs before Vite has ever generated &lt;code&gt;routeTree.gen.ts&lt;/code&gt;. One documented &lt;code&gt;vite build&lt;/code&gt; bootstraps it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deferred on purpose:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Turborepo.&lt;/strong&gt; Two apps build in seconds; task caching solves a problem we don't have yet. The repo is already shaped for it (&lt;code&gt;apps/&lt;/code&gt; + &lt;code&gt;packages/&lt;/code&gt;) when we do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript project references.&lt;/strong&gt; Core currently re-typechecks inside every app build. Annoying in principle, invisible in practice at this size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular package exports.&lt;/strong&gt; &lt;code&gt;"./*": "./src/*"&lt;/code&gt; offers zero encapsulation. Acceptable now, revisit when the team grows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Watching:&lt;/strong&gt; &lt;a href="https://github.com/TanStack/router/pull/7196" rel="noopener noreferrer"&gt;PR #7196&lt;/a&gt;, external directories in &lt;code&gt;physical()&lt;/code&gt; virtual route mounts. If that lands, core route files could live &lt;em&gt;once&lt;/em&gt; in &lt;code&gt;packages/core&lt;/code&gt; and be mounted per app, and our sync script retires. The architecture doesn't change; one mechanism gets an upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you do this?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This model fits if:&lt;/strong&gt; you ship one product to many customers who need separate builds (bespoke code, separate backends, white-labeling), you want core changes to reach everyone without porting, and you care about TanStack Router's type-safe DX enough to keep it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't fit if:&lt;/strong&gt; you have one app and just want shared libraries (that's the standard Turborepo starter, simpler than this), or your teams need to compose routes from many packages into &lt;em&gt;one&lt;/em&gt; runtime app. That second case is a micro-frontend problem; the people unhappy with TanStack Router in monorepos are usually solving that one, and honestly, React Router may serve them better.&lt;/p&gt;

&lt;p&gt;The rule that made it all work fits in one line, so here it is one last time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a monorepo, share your pages, your guards, your layouts, your validators. Share everything except the route tree.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Where the Real Difference Between Developers Emerges in the AI Age</title>
      <dc:creator>Cengiz Dönmez</dc:creator>
      <pubDate>Wed, 29 Apr 2026 19:36:10 +0000</pubDate>
      <link>https://dev.to/cengizdonmez/where-the-real-difference-between-developers-emerges-in-the-ai-age-2mmk</link>
      <guid>https://dev.to/cengizdonmez/where-the-real-difference-between-developers-emerges-in-the-ai-age-2mmk</guid>
      <description>&lt;p&gt;&lt;em&gt;Writing code is now a common ground. The real differentiation happens elsewhere.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same tools, same AI, same speed.&lt;br&gt;
But one creates value, the other waits for tickets.&lt;/p&gt;

&lt;p&gt;There is no real technical difference.&lt;br&gt;
So where does the difference come from?&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Code No Longer Differentiates
&lt;/h2&gt;

&lt;p&gt;With AI, generating boilerplate, writing CRUD operations, and even setting up mid-level architecture have become accessible.&lt;/p&gt;

&lt;p&gt;So writing code faster is no longer a differentiator.&lt;br&gt;
Knowing more technologies is not as impactful as it used to be.&lt;/p&gt;

&lt;p&gt;Because in these areas, AI already brings you to the "average."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI disrupted the comfort of being average. Producing at an 'average' speed is now easy, and average no longer has value."&lt;br&gt;
Codewarts Bulletin, 2025&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And average is no longer enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Initiative
&lt;/h2&gt;

&lt;p&gt;AI solves the problem it is given.&lt;br&gt;
But it does not notice the problem.&lt;/p&gt;

&lt;p&gt;This is where the real difference begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One approach:&lt;/strong&gt;&lt;br&gt;
A problem is noticed in the logs. There is no ticket, so nothing is done. It surfaces in production over the weekend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another approach:&lt;/strong&gt;&lt;br&gt;
The same issue is identified, quickly fixed, and communicated to the team. It is resolved before the sprint ends.&lt;/p&gt;

&lt;p&gt;Both may have the same technical skill level.&lt;br&gt;
But one completes tasks, the other moves the system forward.&lt;/p&gt;

&lt;p&gt;Initiative is not a technical skill.&lt;br&gt;
It is a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ownership "I Built It, I Stand Behind&amp;nbsp;It"
&lt;/h2&gt;

&lt;p&gt;AI can write code.&lt;br&gt;
But reviewing that code critically, thinking through edge cases, and asking whether it will still be understandable a month later is human work.&lt;/p&gt;

&lt;p&gt;Opening a PR is not the job.&lt;br&gt;
Having it deployed and running reliably is.&lt;/p&gt;

&lt;p&gt;Delivering is one thing. Owning is another.&lt;br&gt;
A developer who owns the work puts their name on it and stands behind it.&lt;/p&gt;

&lt;p&gt;In teams working with AI tools, this sense of ownership is becoming rarer, and therefore more valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Finishing the&amp;nbsp;Work
&lt;/h2&gt;

&lt;p&gt;"Almost done."&lt;br&gt;
"It works locally but I couldn't deploy."&lt;br&gt;
"I opened a PR but I'm waiting for review."&lt;/p&gt;

&lt;p&gt;They all mean the same thing. The work is not finished.&lt;/p&gt;

&lt;p&gt;AI makes it easy to get a task to 80%.&lt;br&gt;
The last 20% includes edge cases, error handling, documentation, and production readiness. It still requires human discipline.&lt;/p&gt;

&lt;p&gt;This is where real value is created.&lt;/p&gt;

&lt;p&gt;If you said "it will ship today," it should ship today.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Moving Forward Without Waiting for&amp;nbsp;Clarity
&lt;/h2&gt;

&lt;p&gt;In real projects, requirements are rarely clear.&lt;/p&gt;

&lt;p&gt;"Let's do this, but we're not sure how."&lt;/p&gt;

&lt;p&gt;AI can generate options.&lt;br&gt;
But it cannot make decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for clarity:&lt;/strong&gt;&lt;br&gt;
Weeks pass. By the time requirements arrive, other variables have changed. Work restarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moving in uncertainty:&lt;/strong&gt;&lt;br&gt;
Assumptions are written down, work starts small, and direction evolves through feedback.&lt;br&gt;
By the time things become clear, the work is already 60% complete.&lt;br&gt;
Uncertainty is not a blocker.&lt;br&gt;
It is the working environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Expanding the Problem, Finding the Right&amp;nbsp;Question
&lt;/h2&gt;

&lt;p&gt;The defining trait of high-impact work is this:&lt;br&gt;
Not just solving the given problem, but questioning the problem itself.&lt;br&gt;
"The login page is slow."&lt;br&gt;
Is login actually slow, or is it the redirect?&lt;br&gt;
Is it user perception or real latency?&lt;br&gt;
Are unnecessary requests being made?&lt;br&gt;
AI can provide the right answers.&lt;br&gt;
But it cannot find the right questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Titles still exist.&lt;br&gt;
But those who stand out are remembered for their impact, not their titles.&lt;/p&gt;

&lt;p&gt;AI has given everyone speed.&lt;br&gt;
But speed does not create differentiation. Direction does.&lt;/p&gt;

&lt;p&gt;And direction is still a human responsibility.&lt;/p&gt;

&lt;p&gt;The ability to take initiative, own the outcome, finish the work, operate in uncertainty, and define the right problem. This is the real point of differentiation in the age of AI.&lt;/p&gt;

&lt;p&gt;These skills can be learned.&lt;br&gt;
But they are not easy.&lt;br&gt;
Because all of them require caring.&lt;br&gt;
And caring is not something AI can produce, at least not yet.&lt;/p&gt;

&lt;p&gt;Based on research compiled from GitHub Octoverse 2025, World Economic Forum 2026, and Codewarts Bulletin.&lt;/p&gt;

&lt;p&gt;This article was originally published on Medium:&lt;br&gt;
&lt;a href="https://medium.com/@cengizdonmez/where-the-real-difference-between-developers-emerges-in-the-ai-age-b2191b137f88" rel="noopener noreferrer"&gt;https://medium.com/@cengizdonmez/where-the-real-difference-between-developers-emerges-in-the-ai-age-b2191b137f88&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
