Building a Modular Portal with Webpack Module Federation

In this post, we explore how Webpack Module Federation helped us build a scalable, modular portal. Learn how dynamic code sharing enabled independent development, how we managed shared dependencies, centralized services like authentication, and maintained design consistency with a UI-kit. We also cover performance optimizations that ensured a seamless user experience.

photo of Kadir Caner Erguen
Kadir Caner Erguen

Senior Software Engineer

Posted on Oct 17, 2024

Introduction

Context and Purpose

Our team is part of the Transport teams within the Logistics department, where we build and manage software for internal users, including finance teams, warehouses, and, in the future, our third-party partners. The portal software is designed to streamline operations across these teams, providing tools and features that improve workflow efficiency and collaboration.

We decided to use Webpack Module Federation while building a modular portal to address the challenges of scalability and development autonomy. Modularity was key to enabling independent feature development and deployment across teams. In this blog post, we’ll share the reasoning behind this choice, the process, and the lessons we learned along the way.

Overview

Webpack Module Federation allows for sharing code dynamically between applications at runtime without needing to rebuild the host app or statically link all modules, enabling micro frontends. This flexibility played a crucial role in the architecture of our portal project.

1. The Challenge

Project Scope

Our portal project involved collaboration between five different teams, with each team responsible for different applications. Some of these applications were brand new, while others had legacy code that required continued support. For example, certain applications were still being used within iframes in other portals, adding complexity to the integration. The critical aspect of modularity was to ensure that each team could independently develop and deploy their applications without affecting the main portal. This independence was key to maintaining flexibility, especially as teams needed to update their applications without touching the portal itself.

Limitations of Traditional Approaches

In previous approaches, such as monolithic applications or using static builds, deployments were cumbersome and tightly coupled. Every time an update was required, teams would need to coordinate to release changes together, which often led to delays and bottlenecks. These traditional models also made it difficult to integrate new applications alongside legacy systems, as maintaining compatibility across different codebases was a significant challenge. Additionally, relying on methods like iframes was not scalable, and it lacked the modern functionality we needed to future-proof the system.

2. Why Webpack Module Federation?

Dynamic Code Sharing

Webpack Module Federation stands out because it allows us to dynamically share modules between applications without the need for static linking or managing shared npm packages across teams. This ability to share code at runtime enabled us to avoid the traditional pitfalls of managing dependencies between different teams’ applications. Each team could expose specific components or utilities from their micro frontend, and other teams could consume them directly, without rebuilding or redeploying shared libraries. For example in our case host and remote applications dynamically sharing React and React-DOM. This not only reduced overhead but also ensured that each application could be updated independently.

Autonomy and Scalability

One of the biggest advantages of Webpack Module Federation is that it provides teams with the ability to develop and release features in isolation. Each team could work on their micro frontend independently, making their own technology decisions and deploying updates on their schedule. This autonomy allowed for much quicker development cycles and reduced the complexity of managing interdependencies between applications.

3. What about backend services? How did we handle authentication and authorisation?

Handling Authentication and Authorisation

On the backend side, we implemented a centralised backend proxy within the portal to handle both authentication and authorisation. This proxy acts as a gatekeeper for all requests coming from the frontend, ensuring that no direct access to backend services occurs without proper authentication and authorisation checks.

Centralised Authentication

The backend proxy is responsible for authenticating users. It uses a single authentication mechanism, ensuring that every frontend application integrates within the portal and does not have to manage individual authentication flows. Once authenticated, the user’s credentials and permissions are handled by the proxy.

Authorisation and Request Forwarding

Once authentication was completed, the proxy verifies user permissions for specific services. The proxy forwards the requests to the appropriate microservices. This approach allows each microservice to focus on its core functionality without worrying about authorisation logic. The proxy ensures that only authorised requests reach the relevant service.

Unified Entry Point

By creating this backend proxy, we established a single entry point for both the frontend and backend, streamlining security and reducing the complexity of managing multiple microservices. This architecture ensures consistent handling of security concerns while giving us the flexibility to scale backend services independently.

4. Flow from the user’s perspective

From the user’s perspective, interacting with the portal is seamless and intuitive. Here’s how the flow works: User Flow Diagram

Initial Request

When a user opens the portal, the first action is to call the portal proxy’s applications endpoint. This endpoint serves as a gatekeeper, checking the user’s permissions and determining which applications they have access to. The response from this endpoint contains a list of applications the user is authorised to view. Each application comes with its ID, name, configuration path, and the specific URL (activePath) where the application will start loading into the portal. Additionally, it contains detailed permission scopes (e.g., read and write permissions) for specific actions within the application.

Here’s an example of the response the user’s browser would receive:

{
    "applications":
    [
        {
            "appId": "example-application",
            "name": "Example Application",
            "configPath": "application.manifest.json path",
            "activePath": "/example-application",
            "opaScope": {
                "scope1": {
                    "read": true,
                    "write": true
                },
                "scope2": {
                    "read": true,
                    "write": true
                }
            }
        }
    ]
}

Fetching Application Configurations

Once the portal receives the list of applications, it proceeds to load each application’s manifest.json file. This file contains essential details about the application, such as its menu items (more on below), required permissions, and the path to the bundled application. The manifest allows the portal to integrate each application seamlessly, displaying menu options and enabling users to navigate to the correct sections based on their permissions.

Example Manifest Data

{
    "menuItems": [
        {
            "label": "Last Mile",
            "path": "/last-mile",
            "requiredPermissions": [],
            "groupId": [
                "invoice-verification"
            ]
        }
    ],
    "bundlePath": "Bundle path of application. (RemoteEntry.js)"
}

Loading the Application

When a user navigates to one of the defined activePath routes in the configuration (for example, “/last-mile”), the portal dynamically loads the corresponding application bundle from the specified bundlePath. This allows the application to be loaded directly into the portal’s interface without refreshing or reloading the entire page. The user is presented with the appropriate UI and functionality based on the permissions defined in the manifest, ensuring they only see what they are authorised to access.

Dynamic Menu and Permissions

The manifest file also defines the menu items for each application, ensuring that the user can navigate the different features within the application. These menu items are dynamically displayed in the portal’s UI based on the user’s permissions, making the experience personalised and secure.

This entire flow is smooth from the user’s perspective, allowing them to access only the applications and features they have permission to use while the portal handles the complex backend interactions in the background.

5. Challenges and Solutions

Shared Dependencies

Challenge: One of the significant challenges we encountered was managing shared dependencies across multiple federated modules. With several teams working independently, version conflicts were a real concern. For instance, two different micro frontends might rely on different versions of a shared library, which could lead to runtime errors or unexpected behaviour.

Solution: We used Webpack’s shared dependencies feature to specify the common libraries used by multiple micro frontends, ensuring that only a single version of the dependency would be loaded at runtime. By marking key libraries (e.g., React, lodash) as shared, we were able to reduce version conflicts and avoid loading multiple versions of the same package. Additionally, we worked on aligning versions across teams during development to maintain consistency and minimise potential issues.

Communication Between Apps

Challenge: Another challenge was enabling smooth communication between different federated modules, especially since they were independently developed and deployed. Some applications needed to share state or data, and managing this across independently running modules posed a challenge.

Solution: We are passing a prop to each application, which allows interaction with the portal and other applications. This prop serves as an interface for each micro frontend to communicate with the rest of the portal. Through this prop, modules can access shared data, trigger specific actions, or exchange necessary information between the federated applications. This method allows us to maintain the independence of each application while providing the necessary communication channels to ensure smooth functionality across the portal.

You can find examples of shared data, specific actions, and necessary information below;

Shared Data

User Session Info: Each micro frontend might need to access the currently logged-in user’s session details, such as their role or permissions, to ensure the correct data is displayed or actions are allowed.

Global App Settings: Applications could share configuration settings, such as theme preferences (light/dark mode) or language localization, to ensure a consistent experience across the portal.

Specific Actions

Logging Out the User: If a user triggers a logout action from one micro frontend this action can be communicated to other applications through the shared prop, ensuring the entire session is closed and the user is logged out portal-wide.

Necessary Information

Navigation Requests: If one application needs to trigger navigation to another part of the portal it can use the shared prop to request the portal to navigate the user to the appropriate page.

Error Handling: Federated applications can pass errors (e.g., failed API calls) to the portal via the shared prop, and the portal can handle displaying a global error message or logging errors centrally.

Performance Considerations

Challenge: Loading multiple remote modules into the portal can introduce performance issues, particularly in terms of loading time and bundle size. We had to ensure that the portal loaded efficiently, even as more micro frontends were added.

Solution: To tackle this, we implemented lazy loading for federated modules, ensuring that only the necessary modules were loaded when the user navigated to a particular section of the portal. This minimised initial load times and kept the bundle size in check. Additionally, we optimised our Webpack builds by enabling code splitting, caching, and compression techniques, which further improved performance. Preloading critical assets for the user’s next possible interactions also helped speed up the perceived load time.

6. Lessons Learned and Best Practices

Plan for Integration

While modularity is great for flexibility, we realised early planning for how modules would communicate and interact is crucial. Defining clear interfaces and communication methods helped avoid complexity later on.

Centralise Common Services

Some services, like authentication and user state management, were best centralised. This helped maintain consistency across applications while allowing teams to remain autonomous.

Optimise for Performance

We prioritised lazy loading and code splitting to ensure the portal remained fast, even as more modules were added. Early optimisation paid off in maintaining a smooth user experience.

7. Bonus: UI-Kit for Consistent Design

To ensure a unified and consistent design across all applications in the portal, we created a shared UI-kit library. This UI-kit was packaged as an internal npm module and distributed across all teams. It provides a set of reusable components, such as buttons, modals, input fields, and typography, all following the same design language and style guidelines.

By using this shared UI-kit, we maintained design consistency across different micro frontends, regardless of the team or application. It also helped speed up development, as teams didn’t need to recreate common UI elements. Additionally, any design updates could be applied centrally within the UI-kit and propagated across all applications, ensuring the portal always maintained a cohesive and up-to-date look and feel.

8. Conclusion

Building our portal with Webpack Module Federation allowed us to create a highly modular and scalable system where different teams could work independently without compromising on integration or performance. By centralizing key services like authentication, managing shared dependencies, and optimizing loading strategies, we are able to deliver a smooth user experience while maintaining flexibility for future growth. Though there were challenges in managing communication between modules and handling version conflicts, careful planning and adherence to best practices helped us overcome these hurdles.

Currently, our portal consists of 11 different applications being developed by 4 different teams. This structure allows each team to work autonomously while maintaining consistency and integration across the platform. In the end, the result is a robust, efficient portal that meets the needs of multiple teams and applications, offering a strong foundation for future development.


We're hiring! Do you like working in an ever evolving organization such as Zalando? Consider joining our teams as a Frontend Engineer!



Related posts