Week 31: Webpack, Custom Builders & Module Federation

Week 2 introduced Angular's default Vite/esbuild application builder — fast, and the right choice for most new apps. This week covers why some teams still reach for Webpack underneath Angular anyway, and the one thing esbuild genuinely can't do yet: Module Federation.

Phase 7 of 8 Week 31 of 33 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Extend Angular's build with @angular-builders/custom-webpack without ejecting
  • Set up Module Federation to expose and consume components across independently-deployed apps
  • Weigh esbuild's speed against Webpack's flexibility for a specific project's actual needs

1. esbuild Today vs. Why Webpack Persists

Week 2 covered the modern Angular application builder — esbuild for dependency bundling, Vite for the dev server — and for the large majority of apps, it's simply the better default: faster cold starts, faster rebuilds, less configuration surface.

Teams still reach for Webpack underneath Angular for a narrower set of real reasons:

  • A build customization esbuild's plugin API can't express yet — a specific loader, a legacy asset pipeline, or output shaping a project depends on.
  • Module Federation (Section 4) — runtime composition of independently-built and independently-deployed bundles, which is a Webpack-specific capability with no first-class esbuild equivalent as of this course.
  • A large legacy codebase already deeply invested in Webpack config that migrating off isn't currently worth the disruption.

None of these apply to most new Angular apps — treat Webpack as something you reach for when a specific requirement demands it, not a default to restore out of habit from older Angular versions.

2. Extending the Build Without Ejecting

@angular-builders/custom-webpack swaps Angular CLI's builder while keeping everything else — angular.json, schematics, the dev server — intact. It merges your own Webpack config into Angular's generated one rather than replacing it wholesale.

angular.json — builder swap
{
  "build": {
    "builder": "@angular-builders/custom-webpack:browser",
    "options": {
      "customWebpackConfig": { "path": "./webpack.extra.js" }
    }
  }
}
webpack.extra.js
module.exports = {
  module: {
    rules: [
      { test: /\.gql$/, use: 'raw-loader' },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      __BUILD_TIME__: JSON.stringify(new Date().toISOString()),
    }),
  ],
};

This is deliberately narrow — you're not hand-rolling Angular's entire Webpack configuration (entry points, output, the Angular compiler plugin), only adding to it. "Ejecting" in the create-react-app sense — copying out the full generated config to edit directly — is rarely necessary and gives up the CLI's ability to keep your build current across Angular upgrades.

3. What You'd Actually Use It For

Concrete, common reasons a real project reaches for this builder:

  • A non-standard file type — importing .graphql documents as raw strings (feeding Week 26's codegen pipeline), or bundling a proprietary asset format a design tool exports.
  • Build-time constants — injecting a build timestamp, commit hash, or feature-flag defaults via DefinePlugin, values that don't belong in a runtime environment file.
  • Output shaping for a specific legacy host — a hosting environment with unusual requirements about chunk naming or asset paths that the standard builder doesn't expose configuration for.

Each of these is a genuine one-off customization, not a reason to migrate a whole app's build off esbuild — the custom-webpack builder is meant for surgical additions, and reaching for it should stay the exception on a well-run Angular project.

4. Module Federation for Micro-Frontends

Week 24 covered micro-frontend architecture with Nx's build-time integration. Module Federation is a genuinely different mechanism: independently-built, independently deployed bundles that load each other's code at runtime, over the network, with no shared build step at all.

webpack.config.js — the remote (exposing a component)
const { withModuleFederationPlugin } = require('@angular-architects/module-federation/webpack');

module.exports = withModuleFederationPlugin({
  name: 'catalog',
  exposes: {
    './ProductList': './src/app/product-list/product-list.component.ts',
  },
  shared: { /* Section 5 */ },
});
webpack.config.js — the host (consuming it)
module.exports = withModuleFederationPlugin({
  remotes: {
    catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
  },
  shared: { /* Section 5 */ },
});
app.routes.ts — loading the remote lazily
{
  path: 'products',
  loadComponent: () =>
    loadRemoteModule({
      type: 'module',
      remoteEntry: 'https://catalog.example.com/remoteEntry.js',
      exposedModule: './ProductList',
    }).then((m) => m.ProductListComponent),
},

The host application never bundles the catalog team's code at build time — it fetches remoteEntry.js at runtime and loads the exposed component on demand, through the exact same loadComponent lazy-loading mechanism from Week 11. This is what lets the catalog team deploy independently, on their own schedule, without the host application rebuilding at all.

5. Shared Singleton Dependencies

Without explicit configuration, each federated app would ship its own copy of Angular itself — multiple Angular instances on one page, which breaks dependency injection and change detection across the federation boundary.

shared config, in both host and remote
shared: {
  '@angular/core': { singleton: true, strictVersion: true },
  '@angular/common': { singleton: true, strictVersion: true },
  '@angular/router': { singleton: true, strictVersion: true },
},

singleton: true ensures only one copy of Angular loads across host and remote combined; strictVersion: true makes a version mismatch a hard error at load time rather than a silent, hard-to-diagnose runtime bug. This is the single most common source of "Module Federation doesn't work" reports — get the shared config wrong and things fail in ways that look unrelated to federation at all.

6. Module Federation vs. Nx's Build-Time Integration

Week 24 built micro-frontends inside an Nx monorepo, composed at build time. Both solve "split a large app into independently-owned pieces" — the real question is which boundary actually matters for your org.

  • Nx monorepo (Week 24): one repo, one build pipeline, apps composed at build time. Simpler mental model, easier to keep consistent, but every app still ships together — a change anywhere can require rebuilding and redeploying the whole workspace.
  • Module Federation: genuinely independent repos, builds and deployments. The catalog team ships without coordinating with the checkout team's release. Worth the added operational complexity specifically when teams need to deploy on separate schedules — not merely because the codebase is large.

A large codebase that still deploys as one unit is usually better served by Nx's build-time composition; genuinely independent deployment cadence across teams is the actual justification for Module Federation's extra complexity.

7. Hands-on Exercise

Hands-on

Add a custom Webpack step, then federate a component

Two parts: a small custom-webpack customization, then a genuine two-app Module Federation setup.

Part 1 — custom-webpack:

  1. Install @angular-builders/custom-webpack and swap the builder in angular.json (Section 2).
  2. Add one real customization — a DefinePlugin injecting a build timestamp constant, displayed somewhere in the app's footer.
  3. Confirm the app still builds and serves normally through the standard ng build/ng serve commands — the point of this builder is that nothing else about your workflow changes.

Part 2 — Module Federation:

  1. Create two separate Angular apps (a "host" and a "catalog" remote), each with its own webpack.config.js via @angular-architects/module-federation.
  2. Expose one standalone component from the remote (Section 4) and lazy-load it into a route in the host via loadRemoteModule.
  3. Configure shared correctly (Section 5) and confirm — via a breakpoint or a log statement — that only one instance of Angular's core is actually running on the page once both apps are loaded together.
  4. Run both apps on different ports and confirm you can rebuild and redeploy the remote alone (restart just that dev server) without touching the host — the actual capability this whole mechanism exists to provide.
Hint

If the remote component renders but injected services behave strangely (a shared state service appears to have two different values depending on which app reads it), that's almost always a missed or misconfigured entry in shared — check every Angular package the two apps have in common, not just @angular/core.

8. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Given esbuild's speed advantage, what's the actual justification for reaching for Webpack on a new Angular project?

A specific capability esbuild doesn't offer — most commonly Module Federation, or a build customization (loader, plugin) esbuild's plugin API can't express. It shouldn't be a default choice; for most new apps esbuild is genuinely the better fit, and Webpack is a targeted exception.

Q2

What does @angular-builders/custom-webpack actually replace, and what does it leave alone?

It swaps the build tool the Angular CLI delegates to, merging your custom Webpack config into the CLI's generated one. It leaves angular.json, schematics, and the rest of the CLI workflow (ng build, ng serve) intact — you're extending the build, not replacing the CLI's control of it.

Q3

What actually goes wrong if @angular/core isn't marked singleton: true in a Module Federation setup?

Each federated app loads its own copy of Angular, resulting in multiple separate Angular instances running on the same page. That breaks dependency injection and change detection across the boundary between host and remote — services can appear to have inconsistent state depending on which app's Angular instance is reading them.

Q4

What's the real deciding factor between Nx's build-time micro-frontend composition (Week 24) and Module Federation?

Whether teams genuinely need to deploy on independent schedules. Codebase size alone doesn't justify Module Federation's added operational complexity — a large app that still ships as one deployable unit is usually better served by Nx's simpler, build-time composed monorepo approach; true independent deployment cadence is what Module Federation actually buys you.