Seamless Code Sharing in Node.js Microservices with Module Federation
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
In the evolving landscape of modern software development, microservices architecture has emerged as a predominant pattern, offering numerous benefits such as independent deployability, scalability, and technological diversity. However, with the proliferation of distinct services, a new challenge frequently arises: how to effectively share common code, utilities, or components across these services without resorting to traditional package management strategies that often lead to versioning headaches or bloated dependencies. This is particularly pertinent in Node.js environments, where a shared codebase can significantly reduce boilerplate, improve consistency, and accelerate development cycles. Addressing this, Module Federation, initially popularized in the frontend world, presents an innovative and compelling solution for enabling dynamic code sharing even in the backend. This article delves into the practical application of Module Federation within Node.js microservices, illustrating how it can redefine inter-service code collaboration.
Understanding Module Federation for Node.js Microservices
Before diving into the implementation details, let's clarify some core concepts surrounding Module Federation and its relevance in a Node.js context.
Core Concepts
-
Module Federation: A webpack feature that allows a JavaScript application to dynamically load code from another application, treating it as a dependency. Unlike traditional dependency management, Federated Modules are not bundled ahead of time within consuming applications. Instead, they are exposed and consumed at runtime. This dynamic linking capability is key to its power.
-
Host (Container): An application that consumes modules exposed by other applications. In a microservices setup, a service that needs to use functionalities from another service acts as a host.
-
Remote (Exposed Module): An application that exposes some of its modules for other applications to consume. A microservice that offers shared utilities or logic would be a remote.
-
Shared Modules: Dependencies (like
lodash
,express
,uuid
) that are common across multiple federated applications. Module Federation'sshared
configuration ensures that these dependencies are loaded only once, improving performance and consistency by avoiding multiple instances of the same library. This is crucial for Node.js services to manage dependencies effectively.
Principle of Operation
At its heart, Module Federation works by generating a special bundle called a "remoteEntry.js" file for each remote application. This file acts as a manifest, containing metadata about the exposed modules and a bootstrap script to load them. When a host needs to consume a remote module, it dynamically fetches this remoteEntry.js
file from the remote service (often via HTTP in Node.js setups) and then executes its bootstrap logic to load the requested module. Webpack's runtime glue then integrates this module into the host application's module graph, making it available as if it were a locally installed dependency.
For Node.js microservices, this means a service can expose a set of utility functions, API clients, or even entire middleware stacks, and other services can consume these directly at runtime. No need for publishing private npm packages, no complex monorepo setups just for shared code—simply a direct runtime relationship.
Implementation and Application
Let's illustrate this with a practical example. Imagine we have two Node.js microservices: a User Service
and an Auth Service
. The Auth Service
handles user authentication and generates JWT tokens. The User Service
needs to validate these tokens for authenticated requests. Instead of duplicating the token validation logic or packaging it into an internal npm library, we can use Module Federation.
Auth Service (Remote)
The Auth Service
will expose a utility function validateJwtToken
.
First, ensure you have webpack 5 installed and configured for Node.js.
npm install webpack webpack-cli webpack-node-externals @module-federation/node@next
webpack.config.js
for Auth Service:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', // or 'production' output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', // Important for Node.js }, externals: [nodeExternals()], // Exclude node_modules from the bundle plugins: [ new ModuleFederationPlugin({ name: 'authServiceRemote', filename: 'remoteEntry.js', // This file will be fetched by hosts exposes: { './tokenValidator': './src/utils/tokenValidator.js', // Exposing a utility }, shared: { // Shared dependencies to avoid duplication. // Webpack will ensure only one instance is loaded. jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, }, }), ], };
src/utils/tokenValidator.js
in Auth Service:
const jwt = require('jsonwebtoken'); require('dotenv').config(); const JWT_SECRET = process.env.JWT_SECRET || 'supersecretkey'; function validateJwtToken(token) { try { const decoded = jwt.verify(token, JWT_SECRET); return { isValid: true, user: decoded }; } catch (error) { return { isValid: false, error: error.message }; } } module.exports = { validateJwtToken };
The Auth Service
would then start, typically exposing its remoteEntry.js
file at a known endpoint (e.g., http://localhost:3001/remoteEntry.js
). This usually means serving the dist
folder directly, or integrating it into an existing Express app.
User Service (Host)
The User Service
will consume the validateJwtToken
function from the Auth Service
.
webpack.config.js
for User Service:
const { ModuleFederationPlugin } = require('@module-federation/node'); const path = require('path'); const nodeExternals = require('webpack-node-externals'); module.exports = { entry: './src/index.js', target: 'node', mode: 'development', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js', libraryTarget: 'commonjs-static', }, externals: [nodeExternals()], plugins: [ new ModuleFederationPlugin({ name: 'userServiceHost', remotes: { // Point to the Auth Service's remoteEntry.js authServiceRemote: 'authServiceRemote@http://localhost:3001/remoteEntry.js', }, shared: { jsonwebtoken: { singleton: true, requiredVersion: '^8.5.1', }, dotenv: { singleton: true, requiredVersion: '^16.0.0', }, express: { singleton: true, requiredVersion: '^4.17.1', }, }, }), ], };
src/index.js
in User Service:
const express = require('express'); const { validateJwtToken } = require('authServiceRemote/tokenValidator'); // Consuming the remote module const app = express(); app.use(express.json()); // Middleware to protect routes using the federated token validator app.use(async (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).send('No authorization header'); } const token = authHeader.split(' ')[1]; if (!token) { return res.status(401).send('Token not provided'); } // Use the federated function! const validationResult = await validateJwtToken(token); if (validationResult.isValid) { req.user = validationResult.user; // Attach user info to request next(); } else { res.status(403).send(`Invalid token: ${validationResult.error}`); } }); app.get('/profile', (req, res) => { res.json({ message: `Welcome ${req.user.username}! This is your profile.`, user: req.user }); }); const PORT = 3000; app.listen(PORT, () => { console.log(`User Service running on port ${PORT}`); });
When the User Service
starts, it will attempt to dynamically load authServiceRemote/tokenValidator
from http://localhost:3001/remoteEntry.js
. This allows the User Service
to leverage the Auth Service
's logic directly without bundling jsonwebtoken
or dotenv
twice, nor needing a pre-published token-validator
package.
Benefits and Use Cases
- Reduced Duplication: Centralize common logic (e.g., validation schemas, specific algorithms, API clients) and share it across services.
- Improved Consistency: Ensure all services use the same version of shared components or utilities, reducing "drift" and potential bugs arising from different implementations.
- Faster Development: Developers build a utility once and expose it; other teams can consume it immediately without waiting for package publishing or complex sync-ups.
- Independent Deployability: Services can update their exposed modules independently. As long as the API contract remains stable, consumers don't need to redeploy.
- Dynamic Updating: Potentially, a host could update its remote configuration to point to a new version of a remote service without a full redeploy of the host itself, though this requires careful management of versioning and breaking changes.
- Monorepo Alternative: Offers a way to share code across services without being tied to a monorepo setup, allowing services to reside in separate repositories.
Considerations and Challenges
- Runtime Dependency: Introduces a runtime dependency on the remote server's availability and responsiveness. If the remote service (or its
remoteEntry.js
) is down, the host might fail to load its federated modules. - Versioning and Compatibility: While
shared
modules help, managing breaking changes in exposed remote modules still requires discipline. Semantic versioning of remote modules is crucial. - Build Complexity: Adds webpack configuration overhead to each service.
- Debugging: Debugging issues across federated modules can be more complex than with traditional local dependencies.
- Security: Ensure the
remoteEntry.js
files are served securely and consumed from trusted sources to prevent malicious code injection.
Conclusion
Module Federation offers a powerful and elegant solution for efficient code sharing in Node.js microservices. By enabling dynamic, runtime loading of modules across distinct services, it significantly enhances maintainability, reduces code duplication, and accelerates development. While it introduces new considerations around runtime dependencies and versioning, the benefits in terms of flexibility and cross-service collaboration make it a compelling architectural pattern worth exploring for modern Node.js backends. In essence, Module Federation empowers microservices to truly behave like a cohesive, yet independently deployable, ecosystem.