Highlights Front-end developers often start projects using one Single Page Application framework (monolithic repository). Using a monolithic repository over time makes it difficult to modify and update the codebase as the application becomes larger and more complex. There are solutions to these challenges that still use a monolithic repo, but they have significant downsides in […]

When FE developers discover the Single SPA framework

How to break up with your monolithic repository for front-end developers

Highlights

  • Front-end developers often start projects using one Single Page Application framework (monolithic repository).
  • Using a monolithic repository over time makes it difficult to modify and update the codebase as the application becomes larger and more complex.
  • There are solutions to these challenges that still use a monolithic repo, but they have significant downsides in terms of development time and costs. 
  • What we have found works for our tech team at Insignia Ventures is to instead use a Single SPA framework that offers modularity for more complex applications.

Having a headache with your monolithic repository? 

As a developer, I often use a Single Page Application framework like ReactJs, AngularJs, VueJs to implement the front-end side of an application. 

But the consequence of using a monolithic repository over time is that as the application grows in size and complexity, it becomes more difficult to modify the code without breaking other features. 

Newer components and technological best practices that come into play like a new library, new UI, new way of structuring your codebase, or handling data or logic, also makes things more difficult. 

Three ways to deal with a monolithic repo over time

Three ways to deal with monolithic repo headaches while still using a monolithic repo

Three ways to deal with monolithic repo headaches while still using a monolithic repo

To overcome these challenges and still use a monolithic repository, we usually consider three options: 

(1) Build a new front-end app from scratch. 

Developers have room to apply new tech and clean up the code, which we would love to do. But for company management, this would likely not be the favorite option as it consumes too much time. 

(2) Improve the existing codebase along the way. 

Developers can both add new features and update the code. This option saves on time relative to the first option, but it comes at a cost. We are faced with increased complexity in their code and they have to make sure that the improvements made do not affect the other portions of the code. We would also have to update all the places in the codebase to keep up with new tech. 

(3) Keep the codebase the same. 

As management requests for more features, developers simply keep making the app bigger and bigger. This in turn makes it more expensive to implement new features as time goes on. 

So now it seems like all options considered have significant downsides. This makes the potential benefits for all three options seem not so worth it for both the developers and management.

Big Brain on how to deal with FE monolithic repo headaches

Big Brain on how to deal with FE monolithic repo headaches

The Fourth Way: Single SPA framework

At Insignia, we have gone down a fourth path: implementing front-end microservices using the Single SPA framework

The key difference of using a Single SPA framework is that it helps to combine multiple SPA frameworks into one SPA app. Instead of working with one, monolithic SPA framework that becomes increasingly complex as changes are introduced and bugs are fixed over time, the application is split between “child-apps.” We can use multiple frameworks (e.g. ReactJs, VueJs, AngularJs, etc.) on the same page without page refreshing.

For example, we turned our existing code base to child-app-1. Then added another new ReactJS framework as child-app-2. To update existing features or fix bugs, we would modify child-app-1. To add new features or tech, we use child-app-2. 

Single SPA framework Pros v Cons

The pros of using the single SPA framework all stem from its modularity. 

We no longer need to rewrite our existing app (the main con of the first option) as new features and tech updates could go into a new SPA framework. This also means that we no longer have to worry about a bug in one child-app affecting the whole application (the main con of the second option). 

Modularity also allows us to develop features in parallel, create multiple access levels to the code base for different developers (e.g. you’ve got an intern developer working on a single feature), and reduce time in general when it comes to the entire process from development to testing to deployment. 

On the other hand, it inevitably increases the overall complexity of the application. 

But it is important to note this complexity is more manageable and less fragile than the complexity of using only one SPA framework for the entire application. As it is also an advanced type of architecture, developers have to change existing paradigms and their understanding of underlying tools (e.g. Webpack, SystemJs, in-browser / build-time modules, import map, continuous integration, module federation, etc.). 

Migration is also challenging when it comes to documentation as we would need to deal with our navbar, shared dependencies, router, shared utilities or components, authentication flow, among other issues. 

Because of these challenges, for developers starting off with a simpler concept for the application or have a small project or team, sticking to the monolithic repo is still more practical until such a time when scaling with that single SPA becomes too challenging for the reasons previously mentioned.

Single SPA still saves the day

Thanks to its modularity, the Single SPA framework ultimately addresses the key issue of fragility and complexity faced by using only one SPA framework as the application grows. 

It also doesn’t have to resort to measures (options one through three) that either lengthen development time, double down on the complexity, or increasing development costs.

And so far, for us at Insignia, it’s a win-win for us developers and management. 

Management can keep requesting new features, and we are now more comfortable and eager to apply a higher level of coding to our codebase. If you’ve ever run into some of the headaches that come with a monolithic repository over time, then the Single SPA framework is definitely worth considering. 

Coding Example

For the devs reading this, I’ve also prepared a quick example to demonstrate how the Single SPA framework works.

Let’s say we have 5 repositories:
  • root-config: Your root config exists only to start up the single-spa applications.
  • shared: Contain shared components, logic, helpers, utilities,.. of your app
  • navbar: Contain menu for navigation between our app1 and app2
  • app1: Child SPA app
  • app2: Child SPA app
Let’s run through important files of these repositories:
root-config:
  1. The root HTML file that is shared by all single-spa applications.
  2. The JavaScript that calls singleSpa.registerApplication().
file: index.html
<!DOCTYPE html>
<head>
  <title>HELLO WORLD</title>  <!-- THIS IS THE MOST IMPORTANT LINE, WILL TALK ABOUT THIS BELOW -->
  <script type="systemjs-importmap" src="https://you-cdn.url/importmap.json"></script>  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
</head>
<body>
  <template id="single-spa-layout">
        <single-spa-router>
            <nav>
                <application name="@org-name/navbar"></application>
            </nav>
            <main>
                <route path="app1">
                    <application name="@org-name/app1"></application>
                </route>
                <route path="app2">
                    <application name="@org-name/app2"></application>
                </route>
                <route default>
                    <application name="@org-name/app2"></application>
                </route>
            </main>
        </single-spa-router>
    </template>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
file: entrypoint.js
// single-spa-config.js
import { registerApplication, start } from 'single-spa';// Simple usage
registerApplication(
  'app2',
  () => import('src/app2/main.js'),
  (location) => location.pathname.startsWith('/app2'),
  { some: 'value' }
);// Config with more expressive API
registerApplication({
  name: 'app1',
  app: () => import('src/app1/main.js'),
  activeWhen: '/app1',
  customProps: {
    some: 'value',
  }
});start();
shared
file: helper.js
export function getMyCompanyName() {
  return 'Insignia VP';
}
file: entrypoint.js
export * as helper from './helpers'
app1/somefile.js
import { helper } from '@org-name/shared'function foo() {
   ...
   const myCompanyName = getMyCompanyName();
   ...
}
To make all of these repositories work, you must ensure that your webpack is properly configured as in SPA documentation.
How can we link all of these repositories together?
Notice this line in root-config/index.html?
  <script type="systemjs-importmap" src="https://you-cdn.url/importmap.json"></script>
Here is the content of importmap.json
{
      "imports": {
        "@org-name/root-config": "https://you-cdn.url/org-name-root-config.js",
        "@org-name/shared": "https://you-cdn.url/org-name-shared.js",
        "@org-name/navbar": "https://you-cdn.url/org-name-navbar.js",
        "@org-name/app1": "https://you-cdn.url/org-name-app1.js",
        "@org-name/app2": "https://you-cdn.url/org-name-app2.js",        
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.8.3/lib/system/single-spa.min.js",
        "react": "https://cdn.jsdelivr.net/npm/react@16.12.0/umd/react.development.js",
        "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.12.0/umd/react-dom.development.min.js",
        "lodash": "https://cdn.jsdelivr.net/npm/lodash@4.17.20/lodash.min.js"
      }
    }
The first part which has @org-name/... is the map of module name to the link of its actual built file (All these modules are marked as external in webpack)
  • For each repo, we run yarn build to generate a built file then upload to a CDN
The second part is the map of large libraries. For performance, it is crucial that your web app loads large JavaScript libraries only once. Your framework of choice (React, Vue, Angular, etc) should only be loaded on the page a single time. For large libraries like react, momentjs, rxjs, etc, you may consider to put it here so that all of our child app can only load it once
  • Because we already included the url to our importmap.json in index.html file. When user loads our page, it also loads our modules.
  • The single-SPA framework helps to manage our child app lifecycle
  • Inside our code, when we try to import sth from @org-name/shared for example. Since we alr marked our modules as external, webpack will not look at our node_modules but leave to SystemJs looking inside our importmap.json file

Hope you picked up something from this and it helps with your next project or managing your company’s front-end needs.

Website | + posts

Phuoc is a senior fullstack developer at Insignia Ventures, with prior experience building applications for tech companies in Vietnam, Singapore, and the US.

***