Managing Large TypeScript Projects: Monorepos and Module Federation
When developing large-scale TypeScript projects, especially those involving multiple teams or numerous interconnected modules, traditional codebase management strategies can become inefficient. This article explores two advanced architectural strategies — Monorepos and Module Federation — that can significantly enhance the scalability, maintainability, and development efficiency of large TypeScript projects.
Monorepos
A monorepo (monolithic repository) is an architectural concept where multiple projects are stored in a single repository. This strategy contrasts with multi-repo setups where each project has its own repository. Monorepos facilitate code sharing and collaboration across teams, ensuring consistency and reducing redundancy.
Benefits of Monorepos
- Unified Versioning: A single repository means you only need one set of dependencies, making version management simpler and more consistent.
- Shared Tooling: Build tools, test frameworks, and other configurations can be unified across projects, reducing setup time and ensuring consistent environments.
- Cross-project Changes: Refactoring or updating shared dependencies is easier as changes can be atomic across all projects that depend on them.
- Simplified Dependency Management: With tools like
Lerna
andnpm workspaces
, managing dependencies and linking packages within a monorepo becomes automated and efficient.
Implementing a Monorepo in TypeScript
Tools like Lerna
are popular for managing JavaScript and TypeScript monorepos. Here’s how to set up a monorepo using npm workspaces
and Lerna
for a hypothetical project (Sparta), previously split across four separate repositories:
Initialize a New Monorepo
mkdir my-monorepo
cd my-monorepo
npm init -y
npm install --save-dev lerna
Configure Lerna
Add to lerna.json
:
{
"packages": ["packages/*"],
"version": "0.1.0",
"npmClient": "npm",
"useWorkspaces": true
}
Set Up npm Workspaces
Add to package.json
:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
Create Package Directories
Organize the existing projects into the packages
directory. Each project becomes a package within the monorepo:
my-monorepo/
├── lib/ # Shared library functions
├── ui-components/ # Shared UI components
├── sparta-ui/ # React.js front-end application
├── sparta-api/ # Backend application (Express.js or Flask)
├── lerna.json
├── package.json
Set Up Interdependencies
Modify each package's package.json
to reflect interdependencies:
// /ui-components/package.json
{
"name": "ui-components",
"version": "1.0.0",
"dependencies": {
"lib": "1.0.0"
}
}
// /sparta-ui/package.json
{
"name": "sparta-ui",
"version": "1.0.0",
"dependencies": {
"lib": "1.0.0",
"ui-components": "1.0.0"
}
}
// /sparta-api/package.json
{
"name": "sparta-api",
"version": "1.0.0",
"dependencies": {
"lib": "1.0.0"
}
}
Link Local Packages
Run lerna bootstrap
to link local packages together based on their dependencies specified in their package.json
files.
Challenges and Considerations
While monorepos provide many benefits, they also introduce complexity, particularly in build times and tooling configuration. Proper setup of CI/CD pipelines and build optimization is crucial to managing this complexity.
In a future article we’ll review several tools besides Lerna
to set up monorepos. We will set up CI/CD pipelines using build tools such Github Workflows
, and Jenkins
. We will also review various aspects of running unit tests locally, and within the deployment pipeline.
Module Federation
Module Federation is a feature provided by Webpack 5
that allows a JavaScript application to dynamically load code from another application at runtime. This is especially useful for microfrontend architectures
where different parts of a frontend app are developed by different teams.
Benefits of Module Federation
- Decoupled Deployment: Teams can deploy updates independently for parts of the application they own.
- Shared Libraries: Common dependencies can be loaded once and shared between various applications, reducing load times and duplication.
- Scalability: Applications can scale more efficiently by loading only the necessary modules when required.
Example Directory Structure for Microfrontend Architecture
For a project using Module Federation with microfrontends, the directory structure might look like this:
my-microfrontend-project/
├── host/ # Host application
│ ├── src/
│ ├── webpack.config.js
│ └── package.json
├── remoteApp1/ # Remote application 1
│ ├── src/
│ ├── webpack.config.js
│ └── package.json
├── remoteApp2/ # Remote application 2
│ ├── src/
│ ├── webpack.config.js
│ └── package.json
Implementing Module Federation in a TypeScript Project
To set up Module Federation, configure the Webpack of the host and remote applications:
Host Configuration (webpack.config.js
)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
app1: "app1@http://localhost:3001/remoteEntry.js",
},
}),
],
};
Explanation:
The host configuration uses the ModuleFederationPlugin
to define a remote. This tells the host application where to find the code for app1
. When needed, the host will load app1
's code from the provided URL.
Remote Configuration (webpack.config.js
)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js",
exposes: {
'./Component': './src/Component',
},
}),
],
};
Explanation:
The remote configuration exposes specific modules (in this case, Component
) which can be dynamically loaded by the host or other applications. remoteEntry.js
acts as the interface for this exposure, allowing the host to integrate the remote code seamlessly.
Challenges and Considerations
While powerful, Module Federation requires careful consideration of version compatibility and security, especially when loading code from external sources.
We will explore a fully fleshed out microfrontend application in a future article series. We’ll explore various deployment strategies, and security concerns.
Conclusion
For large TypeScript projects, both monorepos and module federation offer compelling benefits that can streamline development and enhance collaboration. By understanding and implementing these strategies, teams can significantly improve their development practices and project scalability. For a deeper dive into these topics, the Webpack documentation and Lerna documentation provide extensive information and guides.