When configuring ModuleFederationPlugin in Webpack for a remote and a host, what do the 'exposes' and 'remotes' options each declare, and how does the 'filename' option relate to the generated container file?
answer
- exposes = remote's menu
- remotes = host's address book
- name@url syntax
- remoteEntry.js is just a convention
- public name != file path
basics
~20 s'exposes' is a remote saying 'here's what I'm sharing and its nickname'; 'remotes' is a host saying 'here's a friend's address I want to fetch code from.' 'filename' just names the small file that makes the connection work.
solid answer
~40 sexposes (set on the app acting as remote) maps a public module name to a local file, e.g. {'./ProductList': './src/ProductList'}; Webpack builds a separate chunk for each entry and registers it under that public name in the remote's container. remotes (set on the app acting as host) maps a local alias to a resolution string like 'catalog@https://cdn/remoteEntry.js', combining the remote's registered federation name with the URL of its container. The host can then import('catalog/ProductList') and Webpack's runtime fetches the container, calls its get/init API, and resolves the module. filename controls what Webpack names the emitted container bundle — conventionally remoteEntry.js — and every host's remotes URL must point at exactly that filename. Any app can set both options at once, acting as host and remote simultaneously.
go deeper
Should be able to say that a remote 'shares' code and a host 'uses' shared code, and recognize exposes/remotes as the two config options involved, even without reciting exact syntax.
Should correctly describe the mapping direction of both options, know that filename names the container bundle, and be able to write a basic working exposes/remotes pair from memory.
Should explain the name@url resolution string, why the public exposed name is decoupled from file path, and reason about what breaks, and when, if names drift out of sync between a remote and its hosts.
Should discuss how larger orgs govern this contract at scale — versioned or type-checked exposed APIs, coordinating filename/URL stability across dozens of host-remote pairs, and the org-design implications of a remote's exposes map effectively being a cross-team public API.
## The two sides of one config The `ModuleFederationPlugin` is configured differently depending on which role a Webpack 5 build plays, and two options — `exposes` and `remotes` — are the primary vocabulary for declaring that role, with `filename` controlling how the resulting container is named and found. | Option | Set on the app acting as | What it declares | |---|---|---| | `exposes` | a remote | a public name to a local file path | | `remotes` | a host | a local alias to a resolution string | | `filename` | a remote | the container bundle Webpack emits | ## exposes — the remote's public contract `exposes` is used on the side acting as a remote — the app offering modules for others to consume. It is a map from a public 'friend name' to a local file path, for example { './ProductList': './src/components/ProductList' }. - For every entry, Webpack builds a separate async chunk containing that module and its exclusive dependencies, and registers it under the given public name inside the container it builds for this app. - The public names are deliberately decoupled from the real file paths and directory structure — a consumer of this remote never needs to know that ProductList actually lives at `src/components/ProductList.tsx`; it will import it as `catalog/ProductList`, where catalog is this app's federation name, a value set alongside `exposes` in the same plugin config. This indirection is what lets the remote refactor its internal file layout freely without breaking any host that consumes it — the exposed name is the stable public contract, comparable to a package's public API surface versus its internal module structure. ## remotes — the host's address book `remotes` is used on the side acting as a host — the app that wants to consume another app's exposed modules. It is a map from a local alias to a resolution string that tells Webpack's runtime where to find that remote's container at runtime, for example { catalog: 'catalog@https://cdn.example.com/catalog/remoteEntry.js' }, or in the newer object form { catalog: { external: 'catalog@https://cdn.example.com/catalog/remoteEntry.js' } }. The `@` in that string separates the federation name the remote registers itself under (used to look it up in the browser's global federation registry once loaded) from the actual URL of its container script. Given this entry, any code in the host can write `import('catalog/ProductList')`, and Webpack's runtime resolves catalog to the registered URL, fetches it if not already loaded, and resolves ProductList against that remote's `exposes` map. ## filename — naming the container `filename` controls the name Webpack gives to the container bundle it emits for a remote — most commonly set to `remoteEntry.js`, which is why that name shows up everywhere in Module Federation documentation, but it is just a convention, not a hard requirement; a build could name it `container.js` or version it as `remoteEntry.2024-06-01.js`, as long as every host's `remotes` URL for that app points at the exact same filename that was actually emitted. This matters operationally: the container filename and its hosting URL together form the contract a host is coupled to, so teams commonly either - version the filename itself, forcing a coordinated update on the host side for breaking changes, or, - more commonly, keep the filename stable and instead let the container's internal get/init protocol and shared-dependency version negotiation absorb compatibility, deploying new remote code under the same stable URL so hosts pick it up automatically on next load. ## Why the model is two-sided The reason this two-sided configuration model exists is to keep the coupling between apps as thin and declarative as possible: a remote's `exposes` config is a complete description of its public contract, and a host's `remotes` config is a complete description of what external contracts it depends on and where to find them — neither side needs visibility into the other's build, source layout, or internal module graph. This mirrors, at the level of separately deployed applications, what a package.json dependency and its package's exported entry points do for code shipped through npm, except the resolution happens over HTTP at runtime in the browser instead of at install time on disk. ## The main failure mode The main failure mode teams hit here is **naming drift and typos**. Three independently typed strings must line up exactly, with no build-time type checking across the boundary by default: 1. the `exposes` names, 2. the federation name, 3. the `remotes` alias. Because of that, a remote renaming an exposed module or its federation name silently breaks every host still using the old string, usually surfacing as a runtime 'Module not found' or similar error rather than a build failure. Larger organizations mitigate this by generating and publishing TypeScript declaration files for each remote's `exposes` map so hosts get IDE-level and CI-level type checking against a remote's real exposed API, closer to what a monorepo would give for free.
- Can a single Webpack app both expose modules and consume remotes at the same time?Yes — exposes and remotes are independent options on the same ModuleFederationPlugin config, so an app is commonly both a remote to its parent shell and a host to smaller widgets it itself composes. This is how deeply nested federation trees are built.
- What happens if a host's remotes URL points to a container filename that no longer exists after a remote redeploy?The host's runtime import() call rejects with a network/load error, typically a 404, because Webpack resolves that string literally at runtime; nothing catches this at build time. This is why teams either keep the filename stable across deploys or coordinate URL changes with every host consuming that remote.
- Does the exposed module's public name have to match its file path?No, and that's intentional — the public name in exposes is a stable contract, while the file path is an internal implementation detail the remote team can refactor freely without breaking any host, as long as the public name and what it resolves to stays behaviorally compatible.
Exposes is like a restaurant's public menu (dish names, not kitchen recipes); remotes is like a customer's address book entry for that restaurant (name plus delivery address); remoteEntry.js is the printed menu itself that gets delivered on request.
saying these in an interview costs you the question
- thinks exposes and remotes are the same option used both ways automatically
- believes remoteEntry.js is a hardcoded requirement
- assumes the public exposed name must equal the source file path
- doesn't realize an app can be both host and remote at once
- thinks a naming mismatch is caught at build time