<?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: Alex KH</title>
    <description>The latest articles on DEV Community by Alex KH (@klerick).</description>
    <link>https://dev.to/klerick</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%2F2633698%2Fad9f19e7-6583-4dbf-a4fe-205836682c17.jpg</url>
      <title>DEV Community: Alex KH</title>
      <link>https://dev.to/klerick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/klerick"/>
    <language>en</language>
    <item>
      <title>Dart Looked Like a Killer on the Backend. On a Leash, It's a Paper Tiger.</title>
      <dc:creator>Alex KH</dc:creator>
      <pubDate>Mon, 22 Jun 2026 10:53:36 +0000</pubDate>
      <link>https://dev.to/klerick/dart-on-the-backend-the-illusion-of-cloud-ready-performance-16o2</link>
      <guid>https://dev.to/klerick/dart-on-the-backend-the-illusion-of-cloud-ready-performance-16o2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I first published this in Russian on &lt;a href="https://habr.com/ru/articles/1022790/" rel="noopener noreferrer"&gt;Habr&lt;/a&gt;; this is my own translation, lightly reworked for an English audience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Goal:&lt;/strong&gt; Cut the 80 MB idle memory footprint of our Node.js microservices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Plan:&lt;/strong&gt; Let Claude Code migrate an OAuth2 service to Dart, drawn in by AOT compilation and "cloud-ready" marketing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Reality:&lt;/strong&gt; The migration itself was flawless and nearly effortless. The &lt;em&gt;runtime&lt;/em&gt; was the problem: Dart's VM holds memory after peak loads &lt;em&gt;by design&lt;/em&gt; (tuned for Flutter's 60 fps, not K8s): it returned 6% of its peak allocation versus Node's 81%, and posted the lowest throughput of any raw runtime I tested (~1.8× lower RPS than Node.js, a third of Go's per-core efficiency).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Lesson:&lt;/strong&gt; AI makes mechanical migration free. The cost of an &lt;strong&gt;unverified runtime hypothesis&lt;/strong&gt; stays exactly the same, and the agent will never flag it for you. I ended up back on Go.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;To be clear up front: this isn't a language flame. Dart the language is genuinely pleasant - clean, predictable, a joy to write. My problem is strictly the VM's runtime behavior under Kubernetes, not the syntax.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the trap I walked into, and I think it's the defining trap of building with AI agents in 2026: when an agent can rewrite an entire service in a couple of hours, the &lt;em&gt;writing&lt;/em&gt; of code stops being the expensive part. So you stop treating it as a decision. You skip the two-hour test that would have killed the idea, because skipping it no longer feels like it saves you anything: the code is already free.&lt;/p&gt;

&lt;p&gt;It isn't free. The hypothesis underneath the code is exactly as expensive as it always was. This is the story of how an AI agent did everything I asked, perfectly, and that was precisely the problem.&lt;/p&gt;

&lt;p&gt;If you're looking at Dart as a backend alternative to Node.js, learn from my mistake instead. Complete benchmark results (Go, Node.js, Dart, Bun, Deno, and .NET), with methodology, configs, and raw numbers, are on &lt;a href="https://github.com/klerick/go-vs-dart" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; The biggest mistake wasn't choosing Dart: it was the &lt;em&gt;order of steps&lt;/em&gt;. Instead of running a raw benchmark on a production-like scenario on day one, I blindly trusted AOT compilation, static typing, and "ready for cloud" marketing. This isn't really about Dart; it's an expensive lesson about validating a runtime hypothesis before building architecture on top of it. To avoid micro-optimization flame wars, all manifests, CPU profiles, and source are isolated in the &lt;a href="https://github.com/klerick/go-vs-dart" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. I'm a regular JS/TS developer, not a Go or .NET guru; if you spot a flaw, PRs are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Act I: The Marketing Honeymoon&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I work on a SaaS product powered by Node.js. For the most part, it does the job well: solid performance, fast development cycles, and a unified language across the stack. Sure, under heavy load certain services get greedy. Our OAuth2 service, for instance, would peak at 500 MB of RAM. But tokens were issued smoothly, memory was eventually reclaimed, and performance degradation was strictly bottlenecked by CPU-heavy cryptography.&lt;/p&gt;

&lt;p&gt;However, when you run a SaaS with hundreds of identical microservices, an idle footprint of 80 MB per instance scales into a noticeable infrastructure bill.&lt;/p&gt;

&lt;p&gt;We started exploring alternatives. Go is the obvious candidate, but our team is JavaScript to the bone. Go didn't spark much enthusiasm: the endless &lt;code&gt;err != nil&lt;/code&gt; boilerplate on every line, and passing &lt;code&gt;context.Context&lt;/code&gt; as the first argument felt like a relic, a vivid reminder of Node's old callback hell where &lt;code&gt;err&lt;/code&gt; was always the first argument.&lt;/p&gt;

&lt;p&gt;Then we stumbled onto Dart. On paper it ticked every box: AOT compilation, static typing, familiar syntax. The official &lt;a href="https://dart.dev/server/google-cloud" rel="noopener noreferrer"&gt;dart.dev&lt;/a&gt; docs explicitly state: &lt;em&gt;"Creating scalable, high performance APIs and event-driven apps are good use cases for Cloud Run."&lt;/em&gt; Straight from the source, so I bought in.&lt;/p&gt;

&lt;p&gt;You know that honeymoon phase with a new technology? The landing page is pristine, the syntax is elegant, and you think: &lt;em&gt;"This is the silver bullet! Why is everyone else still suffering with legacy tools?"&lt;/em&gt; You dive in headfirst.&lt;/p&gt;

&lt;p&gt;We knew we wouldn't hit Go-level performance. But dropping from 80 MB to 10 MB idle? Our AI assistant, having scraped the same marketing blogs, promised exactly that. To be fair, a barebones HTTP server compiled to AOT genuinely consumed peanuts. I ran it, verified it, got inspired. Static typing, AOT, an 8× memory reduction, and wiping out the black hole of &lt;code&gt;node_modules&lt;/code&gt;: it sounded flawless.&lt;/p&gt;

&lt;p&gt;Keyword: &lt;em&gt;sounded&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Act II: The Reality of the Codebase&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Problem 1: The Backend Ecosystem Ghost Town&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To test the waters we decided to rewrite our auth service. The Node.js version relied on NestJS, a custom Redis ORM, and &lt;code&gt;ts-oauth2-server&lt;/code&gt;. Backed by Claude Code, porting a clean TS codebase with structured architecture seemed trivial. Move the structure, translate the logic, swap the runtime. What could go wrong?&lt;/p&gt;

&lt;p&gt;Turns out Dart is Flutter's world. Outside of Flutter, the backend ecosystem is a ghost town. There's no NestJS equivalent, Redis clients can be counted on one hand, and something resembling &lt;code&gt;ioredis&lt;/code&gt; simply doesn't exist. Sure, there are &lt;code&gt;Shelf&lt;/code&gt; and &lt;code&gt;Serverpod&lt;/code&gt;. But we didn't need a monolithic full-stack framework designed to bundle mobile apps (Serverpod), nor did we want to micro-manage routing like it's Express.js in 2015 (Shelf). We needed an enterprise-grade backend architecture.&lt;/p&gt;

&lt;p&gt;I'd wanted to build a lightweight alternative to NestJS to bypass its classic pain points: bulky dependency trees, excessive runtime reflection magic. The thought of &lt;em&gt;"since Dart lacks NestJS, I'll just build my own perfect mini-framework"&lt;/em&gt; felt incredibly empowering at the time. I was already imagining the GitHub stars. Combined with an uncapped AI agent token budget and a habit of reinventing wheels, the plan felt as precise as a Swiss watch.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Problem 2: Language Constraints &amp;amp; Code Generation Hell&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I remember the early-2010s hype when Google pushed Dart as a JavaScript replacement. After writing actual production code in it, I finally understood why that push failed.&lt;/p&gt;

&lt;p&gt;Want to enumerate an arbitrary object's fields dynamically, the way you'd casually &lt;code&gt;for...in&lt;/code&gt; over a plain JS object? Without reflection, forget it. Want to invoke a static method dynamically via a reference to a &lt;code&gt;Type&lt;/code&gt;? No chance. The &lt;code&gt;Type&lt;/code&gt; primitive is severely limited, and invoking anything on it in AOT mode is blocked by design. Runtime reflection is stripped out for AOT. So how do you build a proper DI container without reflection?&lt;/p&gt;

&lt;p&gt;Okay, I thought: time to accept the Dart gospel and stop writing TypeScript in Dart. I looked at how the ecosystem handles this and hit the ultimate developer nightmare: heavy &lt;strong&gt;code generation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dart's approach is highly intrusive: every file needs a manual &lt;code&gt;part 'file.g.dart'&lt;/code&gt; declaration. Your clean source file is tightly coupled to a file that doesn't exist yet. You're left with two options: commit thousands of lines of auto-generated clutter into your repo, or run sluggish build runners on every single pipeline stage.&lt;/p&gt;

&lt;p&gt;To minimize the mess I used an experimental flag (&lt;code&gt;--enable-experiment=enhanced-parts&lt;/code&gt;) and wrote a custom CLI tool. The workflow: spin up the entry point in JIT mode, introspect annotations and types via &lt;code&gt;dart:mirrors&lt;/code&gt;, convert &lt;code&gt;ClassMirror&lt;/code&gt; to descriptors, trigger annotations, generate the files needed for AOT. Something that takes three lines of reflection in C#, Java, or TypeScript required building an entire secondary toolchain in Dart.&lt;/p&gt;

&lt;p&gt;Paradoxically, the core language is pleasant. Writing Dart feels smooth, logical, predictable; most errors are caught early at compile time. Claude Code ported &lt;code&gt;ioredis&lt;/code&gt; in a few hours: &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/dart-service/ioredis/README.md#stats" rel="noopener noreferrer"&gt;4,500 lines of Dart against 23,500 lines&lt;/a&gt; of the original TypeScript. The syntax is clean enough that even an AI writes it elegantly. But the moment you venture beyond basic business logic, you hit a wall. And because of that friction, nobody builds tooling, which leaves the ecosystem stagnant.&lt;/p&gt;

&lt;p&gt;Over two weeks we engineered a NestJS-like framework with hierarchical DI (similar to Angular), transport-layer isolation, request-scoping via &lt;code&gt;Zone&lt;/code&gt;, and a code-gen CLI. Claude flawlessly ported our Redis ORM. On the surface, everything looked spectacular. Yet an unsettling feeling remained. On day 14, I finally ran a clean, isolated load test.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Act III: The Production Reality Check&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Problem 3: Performance Under Throttling&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I set up a straightforward benchmark: three endpoints, Postgres, Redis, identical resource constraints for every runtime. No frameworks, no ORMs, just raw HTTP servers, to test the raw runtimes, not the frameworks.&lt;/p&gt;

&lt;p&gt;I expected &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/dart-service-redis310/bin/server.dart" rel="noopener noreferrer"&gt;Dart&lt;/a&gt; to land slower than &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/go-service/main.go" rel="noopener noreferrer"&gt;Go&lt;/a&gt; but comfortably ahead of &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/node-service/server.mjs" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;. I named the benchmark repo &lt;code&gt;go-vs-dart&lt;/code&gt;. The numbers quickly proved that was the wrong title.&lt;/p&gt;

&lt;p&gt;At rest, Dart looked unbeatable: instant boot, 3 Mi RSS versus Node's 18 Mi, a tiny 5 MB Docker image. Then traffic hit, and the illusion collapsed. Here's the head-to-head at 500 VUS, shown at &lt;em&gt;both&lt;/em&gt; the generous profile (&lt;code&gt;1000m&lt;/code&gt; = a full dedicated core) and the throttled one (&lt;code&gt;100m&lt;/code&gt;), median across 3 runs, 256 Mi memory cap:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;RSS idle&lt;/th&gt;
&lt;th&gt;RSS peak&lt;/th&gt;
&lt;th&gt;Returned&lt;/th&gt;
&lt;th&gt;p95 @1000m&lt;/th&gt;
&lt;th&gt;p95 @100m&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 Mi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29 Mi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;75%*&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;18 Mi&lt;/td&gt;
&lt;td&gt;39 Mi&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3 Mi&lt;/td&gt;
&lt;td&gt;39 Mi&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.9 s&lt;/td&gt;
&lt;td&gt;9.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Returned&lt;/strong&gt; = share of above-idle memory released back to the OS within 5 min after the run. &lt;strong&gt;* Go&lt;/strong&gt;: scavenger releases gradually (&lt;code&gt;29→24→16→8 Mi&lt;/code&gt;), trending to ~1 Mi given longer. Full field (Bun, Deno, .NET, NestJS, Rust) in the &lt;a href="https://github.com/klerick/go-vs-dart" rel="noopener noreferrer"&gt;repo&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three findings stand out, and not one of them improves when you give Dart more CPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput.&lt;/strong&gt; At a full core, Dart handles 741 RPS, compared to Go's 1,656 and Node's 1,321. Throttle all three down to a tenth of a core and the order doesn't move: Dart 67, Go 209, Node 128. The clearest signal is normalized throughput (RPS per 100m of CPU), which barely shifts between profiles: Dart sits at ~74 RPS per 100m of core no matter what, a third of Go's 256 and about half of Node's 135. So this isn't a throttling artifact. Hand Dart a whole idle core and it's still last; the inefficiency is in the runtime, not the CPU budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; This is the real dealbreaker. After the load test, Go hands 75% of its peak back to the OS and Node 81%; Dart returns 6%. It peaks at 39 Mi and is still holding 37 Mi five minutes later. And it gets &lt;em&gt;worse&lt;/em&gt; under pressure: throttle the CPU and Dart's peak climbs to 47–48 Mi, exactly backwards for a runtime you're squeezing to save money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; At a full core, everyone's p95 is sub-second. Drop to 100m and Dart's blows out to 9.5 s: not "slow," but cascading timeouts and an immediate outage. Node and Go degrade too (5.0 s and 2.9 s), but stay survivable. That same 100m profile, for what it's worth, is where Bun-native and NestJS get killed outright by the liveness probe; Go, Node, and Dart at least stay up.&lt;/p&gt;

&lt;p&gt;I tried every GC flag I could find: &lt;code&gt;--dontneed_on_sweep&lt;/code&gt;, &lt;code&gt;--use_compactor&lt;/code&gt;, &lt;code&gt;--force_evacuation&lt;/code&gt;, &lt;code&gt;--mark_when_idle&lt;/code&gt;, &lt;code&gt;--old-gen-heap-size&lt;/code&gt;. At some point even the &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/dart-service/Dockerfile.gc" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt; had started to read like a ritual of hope rather than a configuration. Nothing moved the needle. Eventually the Dart VM team confirmed it: &lt;a href="https://github.com/dart-lang/sdk/issues/51126" rel="noopener noreferrer"&gt;this is by design&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Dart VM is carefully tuned for Flutter. Its priority is minimizing GC pause spikes to guarantee seamless 60 fps UI rendering on mobile. Returning RSS to the host OS is an afterthought. Brilliant for client-side mobile; catastrophic for Kubernetes containers.&lt;/p&gt;

&lt;p&gt;Kubernetes has no idea your process is just "caching memory for later." It reads active RSS. If an HPA scales your service to 10 pods during a surge, those Dart pods retain peak memory long after traffic subsides. The K8s scheduler can't bin-pack effectively, and cluster autoscalers can't downscale nodes. Node.js pods shrink back down and free cluster resources; Dart makes you pay for peak usage indefinitely.&lt;/p&gt;

&lt;p&gt;And this is what gets called "ready for cloud," on a runtime whose own docs pitch it for "scalable, high performance APIs." Run it on Cloud Run (long-lived container instances, not ephemeral functions), where you pay for &lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;memory × time&lt;/a&gt;. A burst of 100 lands and memory jumps; Node spikes and gives it back, Dart grabs it and keeps it. A burst of 200 follows: Node climbs from idle again and settles back, Dart climbs from a floor it never lowered. Node lets you size the instance limit tight and trust it to fit; Dart makes you provision for the all-time peak, forever. So Dart on Cloud Run literally costs more than Node.js: slower to serve each request, and sitting on memory long after the peak. Then again, Google knows best what "high performance" means.&lt;/p&gt;

&lt;p&gt;One last indicator of ecosystem neglect: our &lt;code&gt;ioredis&lt;/code&gt; port, mechanically spit out by an AI agent, posted &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/results/comparisons/dart-redis310_vs_dart-ioredis.md" rel="noopener noreferrer"&gt;5% higher RPS&lt;/a&gt; than the most popular, community-vetted Redis client on pub.dev. That's not praise for the AI; it's an indictment of the ecosystem. Nobody is building or optimizing heavy server infrastructure in Dart.&lt;/p&gt;

&lt;p&gt;The marketing facade had completely crumbled.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Act IV: A Sober Retrospective&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After two weeks of heavy lifting (authoring a DI engine, porting Redis clients, running load profiles), the verdict is absolute: for backend cloud workloads, Dart is a paper tiger. Its true home is primarily Flutter. The team traded reflection for small mobile binaries, stalled on macro language features for years, and tuned the VM exclusively for client-side interaction.&lt;/p&gt;

&lt;p&gt;It's a shame, because Dart was one decision short of being a serious contender, and potentially a genuine Node.js killer for cloud-native. It starts at 3 Mi, with a memory peak no worse than Node's. If it just handed that peak back within a reasonable window after load dropped, the lifecycle would already be beautiful: an HPA brings up a second pod in a second, traffic spreads across both, load falls, memory frees, Kubernetes packs the nodes tighter. You'd fit two lean Dart pods in the memory one 40 Mi Node pod occupies, and two Dart pods comfortably out-serve that single Node pod, and the per-pod RPS gap would no longer look like a death sentence. Instead, the pod grabbed 40 Mi and never gave it back. One architectural call in the GC, made for 60 fps, and the whole scenario falls apart. Had memory behaved predictably, a backend use case would have emerged, a community would have formed behind it, the I/O core would have caught up, and Dart could have been a real Node.js replacement for cloud-native.&lt;/p&gt;

&lt;p&gt;In the end, my search for a silver bullet led me right back to Go. Yes, it can feel repetitive, verbose, and demanding with its explicit &lt;code&gt;err != nil&lt;/code&gt; handling. But in a real production environment (throttled by CPU ceilings, juggling database network hops, needing nimble memory management), Go simply scales. It wasn't the raw-RPS champion (Bun's native APIs edged it out), but it had the best &lt;strong&gt;CPU efficiency in the field by a wide margin&lt;/strong&gt; (~256 RPS per 100m of core versus Node's 135 and Dart's 74), at the smallest peak footprint (29 Mi); it never died under any throttling profile, and its scavenger unwinds memory back toward ~1 Mi idle. Gradually but fully, which is exactly what Dart refuses to do.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Side note: Bun native actually posted the &lt;a href="https://github.com/klerick/go-vs-dart/blob/main/results/comparisons/go_vs_bun-native.md" rel="noopener noreferrer"&gt;highest raw RPS&lt;/a&gt; of the whole field (edging out Go), but at ~3× Go's peak memory (85 Mi vs 29 Mi), and under severe 100m CPU throttling it got killed outright by the liveness probe. For hardened K8s setups, not production-ready yet. And there's a deeper catch I'll save for its own post: on a large codebase, JSC's resident JIT machine code and allocator regions inflate RSS no matter how clean your heap is, which makes Bun a brilliant toolchain, but not a runtime I'd hand a heavy backend in production. (&lt;a href="https://github.com/klerick/go-vs-dart/blob/main/results/comparisons/bun-native_vs_bun-npm_vs_node.md" rel="noopener noreferrer"&gt;Native vs npm vs Node numbers here.&lt;/a&gt;))&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I'm opening the Go tour and starting from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Real Lesson&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Commenters will rightly point out: &lt;em&gt;"A two-hour k6 script on day one would have saved you two weeks."&lt;/em&gt; They're completely correct.&lt;/p&gt;

&lt;p&gt;But there's a subtle trap I only understood at the end. Tools like Claude Code make mechanical migration incredibly cheap, essentially free. That's the paradox. When the cost of writing code drops to zero, it becomes dangerously easy to forget that &lt;strong&gt;the cost of an unverified runtime hypothesis stays identical&lt;/strong&gt;. An AI agent won't challenge you and ask: &lt;em&gt;"Are you sure this execution engine fits your infrastructure model?"&lt;/em&gt; It will simply build what you asked for, beautifully, swiftly, without hesitation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Validate the runtime hypothesis first. &lt;em&gt;Then&lt;/em&gt; let the AI raise the walls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The benchmark is open and built to be extended: same workload, same Postgres and Redis, same K8s manifests, three CPU profiles. Adding a runtime is one service directory plus one manifest, and the harness does the rest. Benchmarks, configs, and raw numbers: &lt;a href="https://github.com/klerick/go-vs-dart" rel="noopener noreferrer"&gt;github.com/klerick/go-vs-dart&lt;/a&gt; - PRs and new runtimes welcome.&lt;/p&gt;

&lt;p&gt;I'd genuinely love to see &lt;strong&gt;PHP 8.x&lt;/strong&gt; in there. My last serious PHP was 5.1, and people keep telling me the modern version is a different animal — so if that's you, send a PR and let the numbers settle it. Same for any runtime you think I treated unfairly: the rules are in the README, and the harness doesn't play favorites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; This was supposed to be a quick post on preliminary findings. Instead, validating the numbers dragged me down a benchmark rabbit hole for another two weeks: warming up containers, tuning throttling quotas, evaluating .NET, Bun, and Deno.&lt;/p&gt;

&lt;p&gt;Validate your hypothesis. Validate your benchmarks. &lt;em&gt;Then&lt;/em&gt; write the article. One month of work instead of a two-hour test.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dart</category>
      <category>kubernetes</category>
      <category>node</category>
    </item>
    <item>
      <title>Custom builder for Angular: My way</title>
      <dc:creator>Alex KH</dc:creator>
      <pubDate>Wed, 15 Jan 2025 08:58:23 +0000</pubDate>
      <link>https://dev.to/klerick/custom-builder-for-angular-my-way-12hd</link>
      <guid>https://dev.to/klerick/custom-builder-for-angular-my-way-12hd</guid>
      <description>&lt;h2&gt;
  
  
  Disclamer
&lt;/h2&gt;

&lt;p&gt;Hello everyone, I'd like to share by experience of building custom builder for  &lt;a href="https://angular.dev/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;TL;DR Here is the &lt;a href="https://github.com/klerick/nx-angular-mf" rel="noopener noreferrer"&gt;result&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once upon a time, it started with Angular 18 where everything went like clockwork, but the release of Angular 19 changed many things. Many approaches had to be revised, and this process inspired me to write this article.&lt;br&gt;
These steps are like travel notes: why certain decisions were made, what problems arose, and how they were solved. The links to the key commits will follow you through the process.&lt;br&gt;
Sometimes the decisions came from intuition, sometimes - from common sense, and sometimes - just "because." I hope this experience will be useful if you decide to go down this path.&lt;/p&gt;
&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Micro-frontend has always aroused my curiosity: I wanted to understand how they work, how to build them, what their pros and cons are. In 2018, inspired by this topic, I tried to build something similar to &lt;a href="https://single-spa.js.org/" rel="noopener noreferrer"&gt;&lt;code&gt;single-spa&lt;/code&gt;&lt;/a&gt; in one of the pet projects. At that time, there was no &lt;a href="https://webpack.js.org/concepts/module-federation/" rel="noopener noreferrer"&gt;Webpack Module Federation (WMF)&lt;/a&gt;, and &lt;a href="https://webpack.js.org/" rel="noopener noreferrer"&gt;Webpack&lt;/a&gt; itself seemed inconvenient. The choice fell on &lt;a href="https://esbuild.github.io/" rel="noopener noreferrer"&gt;ESBuild&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap" rel="noopener noreferrer"&gt;importmap&lt;/a&gt;.&lt;br&gt;
Browser support for &lt;code&gt;importmap&lt;/code&gt; at the time was mostly on paper or with special flags in browsers. For this reason, I used a &lt;a href="https://github.com/guybedford/es-module-shims" rel="noopener noreferrer"&gt;polyfill&lt;/a&gt;. But, surprisingly, everything worked and even in several projects.&lt;/p&gt;
&lt;h3&gt;
  
  
  Transition to Native Federation
&lt;/h3&gt;

&lt;p&gt;When Angular started moving away from Webpack towards ESBuild, and WMF was replaced by &lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/main/libs/native-federation/README.md" rel="noopener noreferrer"&gt;Native Federation (NF)&lt;/a&gt;, it was nice to see that the ideas of five years ago were not so crazy. NF was used in recent projects, and everything seemed to be going well.&lt;br&gt;
With the release of Angular 18, &lt;a href="https://angular.dev/guide/hydration#" rel="noopener noreferrer"&gt;Hydration&lt;/a&gt; support also appeared. I wanted to try this functionality, but it turned out that NF does not support SSR. &lt;br&gt;
The &lt;a href="https://github.com/angular-architects/module-federation-plugin/issues/466#issuecomment-1951058406" rel="noopener noreferrer"&gt;solution&lt;/a&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; proposed by the author of NF didn't seem like a reliable. It called for a wrapper that, instead of a module, made an HTTP request to get the HTML, then parsed it and inserted it into the component. That approach created compatibility issues with &lt;a href="https://angular.dev/guide/hydration#" rel="noopener noreferrer"&gt;Hydration&lt;/a&gt; and in my opinion significantly complicated the architecture, since it required running a separate SSR server for each mini-SPA.&lt;br&gt;
In turn, NF already had everything needed to load mini-SPA modules via dynamic import.&lt;br&gt;
Therefore I decided to give it a try:&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4201/remoteUrl.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it didn't go that smoothly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'http:'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shame on me,  I didn't know that &lt;code&gt;Node.js&lt;/code&gt; can't load modules via HTTP. So I had to find a workaround. &lt;code&gt;Node.js&lt;/code&gt; supports &lt;a href="https://nodejs.org/api/module.html#moduleregisterspecifier-parenturl-options" rel="noopener noreferrer"&gt;hooks&lt;/a&gt; for loading modules, and this is already at the release candidate stage. Angular 19 even uses this method to generate the manifest file.&lt;br&gt;
Wrote an quick and dirty code which worked. Created an &lt;a href="https://github.com/angular-architects/module-federation-plugin/issues/622" rel="noopener noreferrer"&gt;issue&lt;/a&gt;, suggested a &lt;a href="https://github.com/angular/angular-cli/issues/29092" rel="noopener noreferrer"&gt;pull request with POC&lt;/a&gt;, but there was no response. What's left? Make your own solution.&lt;/p&gt;
&lt;h3&gt;
  
  
  Goals
&lt;/h3&gt;

&lt;p&gt;Any project starts with setting goals so as not to lose focus during the process.&lt;br&gt;
What I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a tool for developing SPA-applications on Angular with micro-frontend architecture without a lot of refactoring for my current projects;&lt;/li&gt;
&lt;li&gt;a plugin for &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;nx.dev&lt;/a&gt;, because this platform is actively used in my own projects.&lt;/li&gt;
&lt;li&gt;easy support and testing, so that in the future it would be possible to update and fix bugs without problems;&lt;/li&gt;
&lt;li&gt;
&lt;del&gt;test coverage&lt;/del&gt; Who am I kidding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first goal is divided into two stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dev environment:&lt;/strong&gt; create a convenient tool for development and testing via &lt;code&gt;nx run serve app-name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build of the application:&lt;/strong&gt; set up the build process via &lt;code&gt;nx run build app-name&lt;/code&gt; so that the result is ready for production.
The first step is to create a project that will materialize these ideas.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 1: Initialization
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Preparing the work environment
&lt;/h3&gt;

&lt;p&gt;An efficient work environment is the key to rapid development and testing. Many of us have heard stories about how it takes days or even weeks to set up a work environment at a new job or project. I am not exception! To avoid such situations, I decided to think through the structure and configuration in advance. The main idea was to make everything reproducible and easy to use. Since the goal was to develop a plugin for &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;nx.dev&lt;/a&gt;, &lt;a href="https://github.com/klerick/nx-angular-mf/commit/381c2490a17e1cff93e360c9e824f92ccd64a46f" rel="noopener noreferrer"&gt;I started&lt;/a&gt; by creating a new workspace via &lt;code&gt;create-nx-workspace&lt;/code&gt;. I used the test application to experiment with SSR, and therefore created a plugin template using &lt;code&gt;@nx/plugin:plugin&lt;/code&gt;. Additionally, I generated two applications and one library via NX generators.&lt;br&gt;
As a result, the project structure &lt;a href="https://github.com/klerick/nx-angular-mf/commit/fc0ffd567ab07cebd21090099ff95ec6ec6b08fe" rel="noopener noreferrer"&gt;looked&lt;/a&gt; like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;plugin&lt;/strong&gt; with two tasks: &lt;code&gt;serve&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;host application&lt;/strong&gt; - the main entry point;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;two SPA-applications&lt;/strong&gt; that simulate micro-frontend;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shared library&lt;/strong&gt; to store code used by all applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This set covered the main case and allowed me immediately test the micro-frontend architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  First steps
&lt;/h3&gt;

&lt;p&gt;After generating the plugin, the first thing was to &lt;a href="https://github.com/klerick/nx-angular-mf/commit/d694a7982cfc8b99b2d9d445d1c156efcf9e9b37" rel="noopener noreferrer"&gt;check&lt;/a&gt; that it worked. Of course, there were some problems. The first run gave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Builder is not a builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem was that &lt;code&gt;@nx/plugin:plugin&lt;/code&gt; didn't generate correct &lt;code&gt;builders&lt;/code&gt; for Angular. I had to manually &lt;a href="https://github.com/klerick/nx-angular-mf/commit/455802903ef00bcf905b9153c1c4d91e5683cb61" rel="noopener noreferrer"&gt;add&lt;/a&gt; &lt;code&gt;executors&lt;/code&gt; and write how they interact with &lt;code&gt;builders&lt;/code&gt;. When I fixed this, I encountered another error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;no schema with key or ref 'https://json-schema.org/schema'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solution turned out to be &lt;a href="https://github.com/klerick/nx-angular-mf/commit/12a80be489f51037712a319765cb2f16760df2b6" rel="noopener noreferrer"&gt;simple&lt;/a&gt; enough: update the link to the current scheme. After that, the command was successfully executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; nx run host-application:serve-test
Run serve mf
 NX   Successfully ran target serve-test for project host-application (480ms)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Improvements in build process
&lt;/h3&gt;

&lt;p&gt;At the moment, the plugin was built "on the fly" on each run. This is not the most reliable approach, since the final build may work differently. I decided to &lt;a href="https://github.com/klerick/nx-angular-mf/commit/966b2ac11608d3542d339d4c677ef10a186d538a" rel="noopener noreferrer"&gt;make&lt;/a&gt; the process more predictable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;before running the &lt;code&gt;serve-test&lt;/code&gt; command, the plugin is built;&lt;/li&gt;
&lt;li&gt;the application is launched from the &lt;code&gt;dist&lt;/code&gt; directory.
Those steps allowed me to have more control over the build process and avoid surprises.
### Compatibility with Angular DevKit
I wanted the plugin configuration to be similar for the standard &lt;code&gt;@angular-devkit/build-angular:application&lt;/code&gt; and &lt;code&gt;@angular-devkit/build-angular:dev-server&lt;/code&gt;. That would provide consistent behavior and a minimal entry threshold for the new developers.
To do this, I wrote a script that automatically &lt;a href="https://github.com/klerick/nx-angular-mf/commit/30f76300f315d3f9e7f1678ebd8c6def1bde5fe2" rel="noopener noreferrer"&gt;extends&lt;/a&gt; the Angular DevKit schema with my own. This script runs before the build so that the final schema is always up-to-date.
Then I &lt;a href="https://github.com/klerick/nx-angular-mf/commit/6db714ce6f8b378a1946a5c09901cfd07a703ca3" rel="noopener noreferrer"&gt;updated&lt;/a&gt; &lt;code&gt;project.json&lt;/code&gt;, replacing the default &lt;code&gt;executor&lt;/code&gt; for &lt;code&gt;serve&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt; with my own and &lt;a href="https://github.com/klerick/nx-angular-mf/commit/62c04cb9e4f9e005c657d305e55f1c53970023f7" rel="noopener noreferrer"&gt;did&lt;/a&gt; the same for two other apps.
As a result, the plugin was integrated into the NX ecosystem as a native tool, and applications were ready to run in a micro-frontend architecture.
If all of this sounds obvious now, it will get more interesting soon: the next steps was reveal the details of integration and solutions to the remaining problems.
## Step 2: Dev-server
I really didn't like the fact that in NF the dependency build was separated from the main project build. And I caught very strange behavior which could be fixed only by restarting the dev-server. Also, I didn't understand how to work with environments: I couldn't start the dev-server so that dependencies were built as for the prod environment. For this reason, I wanted to revisit it with the way that is more practical for me :)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Dev-server without SSR
&lt;/h3&gt;

&lt;p&gt;The main idea of ​​NF and WMF was to move external dependencies out of the main build and load them when it's needed. Of course, that led to many small requests on the first load, but the browser cache significantly speeded up work in the future. Because dependencies change pretty rare, this approach is acceptable. For the first load, SSR with &lt;a href="https://angular.dev/guide/incremental-hydration" rel="noopener noreferrer"&gt;Incremental Hydration&lt;/a&gt; was used, which also reduced the response time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Rules to extract dependencies
&lt;/h4&gt;

&lt;p&gt;To extract dependencies, first I had to figure out what exactly to extract. NF suggests using dependencies from &lt;code&gt;package.json&lt;/code&gt; which sounds logical. However, in the case of monorepos containing backend applications, errors occurred. &lt;code&gt;esbuild&lt;/code&gt; tried to add &lt;code&gt;Node.js&lt;/code&gt; modules to the build for browser. To avoid this, a filter was implemented that creates two lists: dependencies that need to be extracted, and those that will remain in the build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dependency configuration
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/klerick/nx-angular-mf/commit/4beff951c2ec03bc497e29cd03298f83cb16769a" rel="noopener noreferrer"&gt;I added&lt;/a&gt; two parameters for dependency configuration: if a string is passed, it is a path to a JSON file, if an array is passed, it is a list of dependencies. The next step was a function that makes the configuration looking as expected. Since I needed to specify the configuration for both &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;serve&lt;/code&gt;, I implemented an extension of the &lt;code&gt;serve&lt;/code&gt; config based on &lt;code&gt;build&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Entry points definition
&lt;/h4&gt;

&lt;p&gt;Each dependency can have multiple entry points. For example, &lt;code&gt;@angular/ssr&lt;/code&gt; and &lt;code&gt;@angular/ssr/node&lt;/code&gt; are the same package with different entry points. I grabbed the idea from &lt;a href="https://github.com/angular-architects/module-federation-plugin/blob/main/libs/mf/src/utils/share-utils.ts" rel="noopener noreferrer"&gt;NF&lt;/a&gt;: once I got a list of dependencies excluded from the build, Angular config was extended with &lt;a href="https://github.com/klerick/nx-angular-mf/commit/220691189d1e6ee4c78bb74248f19b1dac6cd674" rel="noopener noreferrer"&gt;parameter&lt;/a&gt; &lt;code&gt;externalDependencies&lt;/code&gt; containing this list.&lt;/p&gt;

&lt;h4&gt;
  
  
  First run
&lt;/h4&gt;

&lt;p&gt;After configuration, the build was performed &lt;code&gt;nx run host-application:build&lt;/code&gt; without any problems. However, &lt;a href="https://github.com/klerick/nx-angular-mf/commit/73ec47e142d70c3a9d49c25c765ee99c7395e5fa" rel="noopener noreferrer"&gt;running&lt;/a&gt; &lt;code&gt;nx run host-application:serve&lt;/code&gt; failed with the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NX Schema validation failed with the following errors:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem was related to the implementation of &lt;code&gt;@angular-devkit/build-angular:dev-server&lt;/code&gt;. After analyzing other builders, such as &lt;a href="https://github.com/just-jeb/angular-builders/tree/master/packages/custom-esbuild" rel="noopener noreferrer"&gt;custom-esbuild&lt;/a&gt;, a solution was found. &lt;a href="https://github.com/klerick/nx-angular-mf/commit/e050413c9d155de1832fceefc28170eae2b11fba" rel="noopener noreferrer"&gt;Using&lt;/a&gt; the approach from &lt;a href="https://github.com/just-jeb/angular-builders/blob/master/packages/custom-esbuild/src/dev-server/patch-builder-context.ts" rel="noopener noreferrer"&gt;this example&lt;/a&gt; helped to successfully complete the build of &lt;code&gt;host-application:serve&lt;/code&gt;.&lt;br&gt;
On &lt;code&gt;http://localhost:4200/&lt;/code&gt; everything looked correct, but only thanks to SSR. &lt;a href="https://github.com/klerick/nx-angular-mf/commit/05374b2e690cf0522e8cbf1be700de778cbf2a19" rel="noopener noreferrer"&gt;With disabled&lt;/a&gt; SSR, there was an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uncaught TypeError: Failed to resolve module specifier "@angular/platform-browser". Relative references must start with either "/", "./", or "../"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error showed that the excluded dependencies were missing from the build. For restoration I needed to include &lt;code&gt;importmap&lt;/code&gt; that specifies where to get dependencies like &lt;code&gt;@angular/platform-browser&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  ESBuild plugins injection
&lt;/h4&gt;

&lt;p&gt;To handle dependencies in the separate builds, the capabilities of &lt;code&gt;esbuild&lt;/code&gt; was &lt;a href="https://github.com/klerick/nx-angular-mf/commit/e1b7aa92f0f4a75309e69da3be5790a8f752bae5" rel="noopener noreferrer"&gt;extended&lt;/a&gt; via a plugin that adds new &lt;code&gt;entryPoints&lt;/code&gt; given the list of dependencies. Changes were also made  the build settings:&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="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;splitting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initialOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ngServerMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The first line is self-explanatory - I'm disabling code splitting.&lt;/li&gt;
&lt;li&gt;Removing &lt;code&gt;ngServerMode&lt;/code&gt;: This is the more interesting&lt;sup id="fnref2"&gt;2&lt;/sup&gt; part. The &lt;code&gt;ngServerMode&lt;/code&gt; variable was introduced in Angular 19 as a global variable for SSR. It is set to &lt;code&gt;true&lt;/code&gt; for the server environment and &lt;code&gt;false&lt;/code&gt; for the browser. In the code, it is used as:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &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;ngServerMode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ngServerMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;h4&gt;
  
  
  Customization of ESBuild configuration with your own plugins
&lt;/h4&gt;

&lt;p&gt;In my project I also use &lt;a href="https://typeorm.io/" rel="noopener noreferrer"&gt;TypeORM&lt;/a&gt; with &lt;a href="https://github.com/klerick/nestjs-json-api" rel="noopener noreferrer"&gt;plugin&lt;/a&gt; for &lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt;. To do this I needed to add custom options for &lt;code&gt;esbuild&lt;/code&gt;. I &lt;a href="https://github.com/klerick/nx-angular-mf/commit/efcf289e9379277d98b998d630f5ba0a667b6cf6" rel="noopener noreferrer"&gt;implemented&lt;/a&gt; a feature similar to &lt;a href="https://github.com/just-jeb/angular-builders/blob/master/packages/custom-esbuild/src/application/index.ts" rel="noopener noreferrer"&gt;custom-esbuild&lt;/a&gt; to allow users to add their own plugins to the build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Importmap generation
&lt;/h4&gt;

&lt;p&gt;Once all the setup steps were done, it was time to combine them and create a working &lt;code&gt;importmap&lt;/code&gt;. Based on the list of dependencies and their entry points, a JSON was &lt;a href="https://github.com/klerick/nx-angular-mf/commit/a4099cb64162c0f43a6f5af81f2299ddeea025d5" rel="noopener noreferrer"&gt;generated&lt;/a&gt; which became the basis for &lt;code&gt;importmap&lt;/code&gt;. This tool simplified dependency and routing management.&lt;br&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/9d781c6d2f3885fbe9113003113dec5ea1255b5d" rel="noopener noreferrer"&gt;Generation&lt;/a&gt; of &lt;code&gt;importmap&lt;/code&gt; was organized in advance&lt;sup id="fnref3"&gt;3&lt;/sup&gt;, before loading modules. This allowed avoiding of usage of &lt;a href="https://www.npmjs.com/package/es-module-shims" rel="noopener noreferrer"&gt;es-module-shims&lt;/a&gt; if the browser supports &lt;code&gt;importmap&lt;/code&gt;. And at the same time also &lt;a href="https://github.com/klerick/nx-angular-mf/commit/10b85e0143604b6dc30994a4edc84ff7a1104be7" rel="noopener noreferrer"&gt;added&lt;/a&gt; the ability to pass your own function to modify &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Micro-frontend architecture
&lt;/h4&gt;

&lt;p&gt;With the dev server generating &lt;code&gt;importmap&lt;/code&gt;, everything started working as expected. However, micro-frontend requires a special approach: loading mini-SPAs from remote hosts.&lt;br&gt;
For this, the following were used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;remoteEntry&lt;/strong&gt; — defines where the module is loaded from;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exposes&lt;/strong&gt; — describes the modules available for loading.
#### Routing and build configuration
For verification purpose I &lt;a href="https://github.com/klerick/nx-angular-mf/commit/663cf110dbb4b197ea0255e0d1716a53a132ed04" rel="noopener noreferrer"&gt;configured&lt;/a&gt; the following routes:&lt;/li&gt;
&lt;li&gt;in &lt;code&gt;host-application&lt;/code&gt; two routes with different components;&lt;/li&gt;
&lt;li&gt;in &lt;code&gt;mf1-application&lt;/code&gt; updated configuration for building without dependencies.
#### Importmap logic&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;remoteEntry&lt;/code&gt; is specified, its dependencies and &lt;code&gt;exposes&lt;/code&gt; are retrieved.&lt;/li&gt;
&lt;li&gt;The retrieved data is added to &lt;code&gt;importmap&lt;/code&gt; and new &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap#scoped_module_specifier_maps" rel="noopener noreferrer"&gt;scopes&lt;/a&gt; are created.&lt;/li&gt;
&lt;li&gt;If the dependency is not present in the main &lt;code&gt;imports&lt;/code&gt;, it is added to &lt;code&gt;scopes&lt;/code&gt;.
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/c57bfab038602539e22624dbcc3393550d8969d1" rel="noopener noreferrer"&gt;The implementation&lt;/a&gt; is based on the &lt;code&gt;esbuild&lt;/code&gt; plugin, which creates a new &lt;code&gt;entryPoints&lt;/code&gt; named &lt;code&gt;import-map-config&lt;/code&gt;. Since &lt;code&gt;esbuild&lt;/code&gt; does not support JSON directly, I &lt;a href="https://github.com/klerick/nx-angular-mf/commit/c57bfab038602539e22624dbcc3393550d8969d1#diff-f407b8ddc3a7f705c9e75ba18c0f017a91e331bd36842bd252a7b93766da5b2bR34" rel="noopener noreferrer"&gt;used&lt;/a&gt; the alternative approach.
Bottom line here: the dev server provides a URL for configuration, and a separate file(&lt;code&gt;import-map-config.json&lt;/code&gt;) is generated during the build.
#### Refinement of paths
By default, paths in &lt;code&gt;importmap&lt;/code&gt; are relative which can be a problem when using a CDN. Angular config &lt;a href="https://github.com/klerick/nx-angular-mf/commit/596a372c2ef0774ed04c08956511f8512213eba9" rel="noopener noreferrer"&gt;supports&lt;/a&gt; &lt;a href="https://github.com/angular/angular-cli/blob/main/packages/angular_devkit/build_angular/src/builders/browser-esbuild/schema.json#L278" rel="noopener noreferrer"&gt;&lt;code&gt;deployUrl&lt;/code&gt;&lt;/a&gt; parameter, which solves this problem. If the parameter is missing, the dev server host is used.
#### Dynamic import
Dynamic import also works with &lt;code&gt;importmap&lt;/code&gt;. For example:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firstRemote/FirstRemoteRoute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But TypeScript may throw an error about not being able to find the module. To solve this problem, a &lt;a href="https://github.com/klerick/nx-angular-mf/commit/c251f8ea304e66aeda01bf2394f028ebe3b6bb82" rel="noopener noreferrer"&gt;wrapper&lt;/a&gt; &lt;sup id="fnref4"&gt;4&lt;/sup&gt;provided more flexibility was created. Now routes are set like this:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appRoutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;  
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
  &lt;span class="na"&gt;loadChildren&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="nx"&gt;loadModule&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;firstRoutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firstRemote/FirstRemoteRoute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstRoutes&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;h3&gt;
  
  
  SSR on dev-server: problems and the ways to workaround them
&lt;/h3&gt;

&lt;p&gt;When SSR on the dev server side had to be disabled, it seemed like life had become easier. But sooner or later it had to be &lt;a href="https://github.com/klerick/nx-angular-mf/commit/20ffd26d46254f167be033582f43a39a03f435a2" rel="noopener noreferrer"&gt;returned&lt;/a&gt;. I enabled it back and immediately ran into a problem.&lt;/p&gt;

&lt;h4&gt;
  
  
  Vite surprises me again
&lt;/h4&gt;

&lt;p&gt;The error was the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module '@nx-angular-mf/test-shared-library' imported from '/nx-angular-mf/.angular/vite-root/host-application/main.server.mjs'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The irony is that this is not a &lt;code&gt;Node.js&lt;/code&gt; error. This is &lt;code&gt;Vite&lt;/code&gt; deciding that it knows better and trying to load a module marked as an external dependency. I opened the &lt;code&gt;Vite&lt;/code&gt; source code and dived into the stack trace. I saw that the check for external modules looked something like this&lt;sup id="fnref5"&gt;5&lt;/sup&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;externalRE&lt;/span&gt; &lt;span class="o"&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;https&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;/&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;isExternalUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;boolean&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;externalRE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means that if a dependency doesn't start with &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;Vite&lt;/code&gt; ignores it. External module settings? Forget it. The solution is obvious: fake the import by adding &lt;code&gt;http&lt;/code&gt;. But how? The only way is the &lt;code&gt;Vite&lt;/code&gt; plugin. The problem is that &lt;code&gt;Vite&lt;/code&gt; runs inside &lt;code&gt;@angular-devkit/build-angular:dev-server&lt;/code&gt;, which is not so easy to get to.&lt;/p&gt;

&lt;h4&gt;
  
  
  Looking for workarounds
&lt;/h4&gt;

&lt;p&gt;Instead of giving up, I went the other way. If &lt;code&gt;Vite&lt;/code&gt; doesn't want to cooperate, I can intercept its &lt;a href="https://github.com/klerick/nx-angular-mf/commit/58f0681dd7b9593748818f1796f74b9729be9e75" rel="noopener noreferrer"&gt;behavior&lt;/a&gt; via &lt;a href="https://github.com/klerick/nx-angular-mf/commit/a22724f01d06efb54f0d6da93db0bcfbcbc0de55" rel="noopener noreferrer"&gt;hooks&lt;/a&gt; to load modules. Since I was going to use it anyway, I &lt;a href="https://github.com/klerick/nx-angular-mf/commit/6f6291de7569505a3b2c5269cffd85976df715d2" rel="noopener noreferrer"&gt;registered&lt;/a&gt; a custom loader before starting &lt;code&gt;serveWithVite&lt;/code&gt;, limiting work to SSR mode only and &lt;a href="https://github.com/klerick/nx-angular-mf/commit/fddb8133b0d188a99f2af12910eb1417d91f4928" rel="noopener noreferrer"&gt;gave&lt;/a&gt; patched &lt;code&gt;Vite&lt;/code&gt;. Result? New error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'http:'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looked like a step back, but it was exactly the error that was needed to move forward.&lt;/p&gt;

&lt;h4&gt;
  
  
  Intercepting everything at once: working SSR on a dev-server
&lt;/h4&gt;

&lt;p&gt;When I started implementing the loader, it turned out that I had to take into account many details. Here are the main tasks that I faced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;work with external files:&lt;/strong&gt; it's needed to have a list of all used external files to pass them to the loader. If a request comes in for one of these files, pass it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;load mini-SPA modules:&lt;/strong&gt; these modules need to be pulled in via &lt;code&gt;http&lt;/code&gt; and passed correctly;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;use importmap:&lt;/strong&gt; dependencies need to be updated with each change;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;include dependencies with the &lt;code&gt;http&lt;/code&gt; prefix:&lt;/strong&gt; all of this only works on the dev-server, so dependencies need a special approach;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;implement via &lt;code&gt;resolve&lt;/code&gt;:&lt;/strong&gt; apply the solution from &lt;a href="https://github.com/angular/angular-cli/issues/16050" rel="noopener noreferrer"&gt;this issue&lt;/a&gt;.
After &lt;a href="https://github.com/klerick/nx-angular-mf/commit/4cb0d4d90ade242b19cdadf61fe548c26d03ea78" rel="noopener noreferrer"&gt;implementing&lt;/a&gt; all these steps, I managed to achieve working SSR on the dev-server. But there was one problem without which a full-fledged environment was still unachievable.
#### Problems during rebuild
Let's imagine that &lt;code&gt;host-application&lt;/code&gt; is running. If at this point I make changes to &lt;code&gt;test-shared-library&lt;/code&gt;, then rebuilding happens automatically. However, the result of these changes doesn't appear when the page is refreshed. Why? Because the dev-server doesn't take into account the rebuilt dependency. To see the changes, I have to restart the dev-server that is inconvenient.
##### Update dependencies
This behavior is expected. We excluded &lt;code&gt;test-shared-library&lt;/code&gt; from the main build, so &lt;code&gt;Vite&lt;/code&gt; sees no reason to update itself when it changes. Solution? I have a list of dependencies and change events. I just need to manually initiate the update process by calling this &lt;a href="https://github.com/angular/angular-cli/blob/19.0.x/packages/angular/build/src/builders/dev-server/vite-server.ts#L439" rel="noopener noreferrer"&gt;process&lt;/a&gt;
And again the problem: there is no direct access to &lt;code&gt;Vite&lt;/code&gt;. But, since the import of &lt;code&gt;Vite&lt;/code&gt; is already intercepted, this can be bypassed with a small &lt;a href="https://github.com/klerick/nx-angular-mf/commit/58f0681dd7b9593748818f1796f74b9729be9e75#diff-eb1bbe6e3f73c49889a6c6b07278b38664a8c2e0442325bc8824961b485d7a43R34" rel="noopener noreferrer"&gt;trick&lt;/a&gt;, which runs the required &lt;a href="https://github.com/klerick/nx-angular-mf/commit/813aa391c42d0ca552126e1b7838%209e3a126ea766#diff-3e3174939312e3ba98ffe447ce32682deb2400ef885a1262837b17f9d5836528R88" rel="noopener noreferrer"&gt;process&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Problem with remoteEntry
&lt;/h5&gt;

&lt;p&gt;Another case: changes in &lt;code&gt;remoteEntry&lt;/code&gt;. &lt;code&gt;esbuild&lt;/code&gt; is useless here because the changes may happen in another repository. In my case, it's &lt;code&gt;mf1-application&lt;/code&gt;. Does it mean that I need to restart the dev server every time I edit &lt;code&gt;mf1-application&lt;/code&gt;? No way.&lt;br&gt;
If I know there is an update in &lt;code&gt;remoteEntry&lt;/code&gt;, then I can trigger a &lt;code&gt;Vite&lt;/code&gt; restart by myself. &lt;a href="https://github.com/klerick/nx-angular-mf/commit/dd31a44fac1a208ded277ef0aaf769acacaa26b0" rel="noopener noreferrer"&gt;Adding&lt;/a&gt; a button to the page that restarts the server when clicked.&lt;br&gt;
Now everything is ready: SSR works, changed dependencies are processed, and the dev environment covers all my current tasks. We proceed with of the final build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Main build
&lt;/h2&gt;

&lt;p&gt;This is a key step on the way to the final result. There are many nuances, but the result is worth it. I will describe what I had to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/5852256c00fca3b49014b800355c9fa8abc8d504" rel="noopener noreferrer"&gt;&lt;strong&gt;Mandatory &lt;code&gt;deployUrl&lt;/code&gt;&lt;/strong&gt; &lt;/a&gt;:
This is the heart of the &lt;code&gt;importmap&lt;/code&gt; configuration. It defines where to load dependencies from. Get this wrong and the application will simply crash. &lt;code&gt;deployUrl&lt;/code&gt; is also used to specify the path to &lt;code&gt;import-map-config.json&lt;/code&gt; when starting the SSR server, which is then responsible for loading dependencies from the CDN.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/4157431a5c3cb34151acb8d5579476d9c35a67ee" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;deployUrlEnvName&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;:
Added a parameter that contains the name for the environment variable that, in turn,  contains &lt;code&gt;deployUrl&lt;/code&gt;. If it exists and is set, it should be used in priority. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/d8eba55aa411b02b7a7c8b7f7d7f82188fc6e6cb" rel="noopener noreferrer"&gt;&lt;strong&gt;Module loader&lt;/strong&gt;&lt;/a&gt;: 
Its job is to request &lt;code&gt;import-map-config.json&lt;/code&gt; by &lt;code&gt;deployUrl&lt;/code&gt; and build &lt;code&gt;importmap&lt;/code&gt; that will then be used to download dependencies. If there were changes in any mini-SPA, I just need to restart the SSR server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/d6ab6a0471f29b3f3dc012a9bcd975a0339e9b56" rel="noopener noreferrer"&gt;&lt;strong&gt;Loader registration&lt;/strong&gt;&lt;/a&gt;: 
To do this, the &lt;code&gt;esbuild&lt;/code&gt; plugin creates &lt;code&gt;server.ssr.mjs&lt;/code&gt;, which registers the loader and imports &lt;code&gt;server.mjs&lt;/code&gt;. This ensures that the loader is included before the SSR server is started.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/759b13037e0074e3e3328568073c1c55c43a1d05" rel="noopener noreferrer"&gt;&lt;strong&gt;Loader transfer&lt;/strong&gt;&lt;/a&gt;: 
I need to build the loader with all dependencies and move it to the SSR build folder. This ensures access to all components during runtime. Once again, the &lt;code&gt;esbuild&lt;/code&gt; plugin came in handy.&lt;sup id="fnref6"&gt;6&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/klerick/nx-angular-mf/commit/266c829d401f1a7328a5f746352dc9b250933540" rel="noopener noreferrer"&gt;&lt;strong&gt;Applying the changes&lt;/strong&gt;&lt;/a&gt;: All tasks are integrated into the loader.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Yet-another run
&lt;/h3&gt;

&lt;p&gt;I ran the build... and got an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@nx-angular-mf/test-shared-library'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reason:&lt;/strong&gt; The builder didn't see the module. The dev-server was running, but the build failed. The difference was in the new SSR functionality in Angular 19. It turned out that Angular uses its own custom &lt;a href="https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts" rel="noopener noreferrer"&gt;loader&lt;/a&gt;, which simply doesn't see my dependencies.&lt;br&gt;
Found out that there is a parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;partialSSRBuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It disables the route generation during build time. &lt;a href="https://github.com/klerick/nx-angular-mf/commit/9b670a87312c8588632e56cb0bf24a0bf75b1393" rel="noopener noreferrer"&gt;Enabled it&lt;/a&gt; and build started working.&lt;br&gt;
But a new problem appeared: &lt;code&gt;server.mjs&lt;/code&gt; considered &lt;code&gt;@angular/ssr/node&lt;/code&gt; dependency as external, although it couldn't be excluded. Therefore I had to manually &lt;a href="https://github.com/klerick/nx-angular-mf/commit/2b5f23d86e16f1c0293543bc84fec13918c4e8a9" rel="noopener noreferrer"&gt;specify&lt;/a&gt; paths for each dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final run
&lt;/h3&gt;

&lt;p&gt;When everything was ready:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the projects.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;serve-static&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Then start &lt;code&gt;server.ssr.mjs&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/klerick/nx-angular-mf/commit/0eb9d903a10659a6a8b2c866cedbcf6bab440457" rel="noopener noreferrer"&gt;Result&lt;/a&gt; — everything works. Now it is a full-fledged environment for development and build.&lt;br&gt;
At the moment, this solution works in the production environment, and there are no problems.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;At the time of publication, NF started &lt;a href="https://www.angulararchitects.io/en/blog/ssr-and-hydration-with-native-federation-for-angular/" rel="noopener noreferrer"&gt;supporting&lt;/a&gt; for hook-based SSR&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Solve the problem with &lt;strong&gt;ngServerMode&lt;/strong&gt;. After upgrading to Angular 19, I spent the whole evening trying to figure out why SSR wasn't working: &lt;code&gt;dev-server&lt;/code&gt; was working, but the final build wasn't. The problem was in the &lt;code&gt;ngServerMode&lt;/code&gt; variable. I was using browser-based dependencies on the SSR side. After the build, the condition looked like this: &lt;code&gt;if(true){}&lt;/code&gt;. And it worked in this &lt;a href="https://github.com/angular/angular-cli/blob/main/packages/angular/ssr/src/routes/route-config.ts#L214" rel="noopener noreferrer"&gt;place&lt;/a&gt;, which led to several hours of fun debugging :)&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Native Federation and  &lt;a href="https://www.npmjs.com/package/es-module-shims" rel="noopener noreferrer"&gt;ES Module Shims&lt;/a&gt;. The specificity of &lt;code&gt;importmap&lt;/code&gt; is that it must be declared before any module is loaded. NF creates &lt;code&gt;importmap&lt;/code&gt; at runtime when the module has already been loaded. I assume this is the reason why NF always uses a polyfill.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;Looking ahead: When using &lt;code&gt;provideExperimentalZonelessChangeDetection&lt;/code&gt;, parts loaded via &lt;code&gt;loadChildren&lt;/code&gt; or &lt;code&gt;loadComponent&lt;/code&gt; are not hydrated. This is likely related to this issue &lt;a href="https://github.com/angular/angular/issues/53191" rel="noopener noreferrer"&gt;issue&lt;/a&gt; and &lt;a href="https://angular.dev/api/core/PendingTasks#" rel="noopener noreferrer"&gt;PendingTasks&lt;/a&gt;. Having a wrapper would make it easier to make changes if needed.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;In the latest version of Vite, the regular expression &lt;a href="https://github.com/vitejs/vite/blob/main/packages/vite/src/node/utils.ts" rel="noopener noreferrer"&gt;has changed&lt;/a&gt;. But it doesn't change the essence, since the logic of the check hasn't &lt;a href="https://github.com/vitejs/vite/blob/main/packages/vite/src/node/ssr/fetchModule.ts#L39" rel="noopener noreferrer"&gt;changed&lt;/a&gt;. I asked &lt;a href="https://github.com/vitejs/vite/discussions/19101" rel="noopener noreferrer"&gt;a question&lt;/a&gt;, but at the time of writing there was no answer yet.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Strange behavior of &lt;code&gt;esbuild&lt;/code&gt; during loader building. When converting &lt;code&gt;cjs&lt;/code&gt; to &lt;code&gt;esm&lt;/code&gt;, &lt;code&gt;esbuild&lt;/code&gt; declared all &lt;code&gt;exports&lt;/code&gt; as &lt;code&gt;default&lt;/code&gt;, which was similar to &lt;a href="https://github.com/evanw/esbuild/issues/442" rel="noopener noreferrer"&gt;this issue&lt;/a&gt;. Because of this, loaders can't register the required hooks correctly. So I had to &lt;a href="https://github.com/klerick/nx-angular-mf/blob/main/libs/nx-angular-mf/src/builders/es-plugin/move-custom-loader-plugin.ts#L32" rel="noopener noreferrer"&gt;complicate&lt;/a&gt; the process a bit.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>angular</category>
      <category>microfrontend</category>
      <category>ssr</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
