mobileRumblefishLogo
Menu
desktopRumblefishLogo
Services
Products
Case studies
Careers
Resources
About us
Why we choose Lerna monorepo for frontend projects

Why we choose Lerna monorepo for frontend projects

Thu, Dec 19, 201910 min read

Category: Code Stories / Software Development

Why Lerna monorepo?

When we scale applications, sooner or later we reach a point where it makes sense to build reusable components. There components are stored in repositories. Usually, teams create separate repositories for each package.

But can become problematic for a few reasons.

We might end up with dozens of different repositories that repeat the same build, test, and release process. We also run the risk of bundling components that eventually become unnecessary. Finally, it’s challenging to upgrade applications, especially at scale.

Lerna allows teams to build libraries and apps in a single repository called a monorepo. Since we don’t have to publish to NPM until we’re ready to go, it’s faster to iterate locally when building components that depend on each other. Lerna also helps to optimize the management of multiple packages thanks to high-level commands.

So how does Lerna work in practice?

We have used Lerna monorepos for building frontend apps in several projects. This example is relatively simple, but Lerna also came in handy for more complex projects consisting of multiple frontend components, backend APIs, and all the packages they shared (for example, schema descriptions).

We’re going to show you how Lerna helps in building frontend applications on an anonymized case study from our portfolio.

Our client is a leading insurance company offering health savings accounts in the United States. The project concentrated on re-implementing the legacy user interface of the company’s administration panel. Using modern frontend technologies (React, Material UI, Redux, Saga), our team organized the code as a component library. We later reused it for creating multiple frontend applications.

Our team faced this challenge

We started out with a legacy setup that included 2 repositories: components-library and partner-app.

The idea here is making components-library a package shared between all the frontend application in the series with the goal of making them graphically consistent.

Here’s an example workflow of implementing a feature in the components-library:

Note a few things:

1. The process we visualized above requires the creation of multiple components-library versions with incremental fixes:

  • v1.1.X

  • v1.1.(X+1)

  • v.1.1.(X+2)

  • etc.

2. If two developers are working on different features, they will clash on the version number. To prevent that, they will have to coordinate their work and use other versions. While their versions intertwine, they will pull each other’s work-in-progress changes to their local components-library checkout.

3. Each cycle of changes in the components-library requires 15–20 minutes of waiting for the built version to be available in verdaccio. It means that a developer needs to wait before seeing the graphical feedback of their change.

4. The resulting merge request includes only the changes made in the components-library repository. When we submit the merge request, the changes in components-library are already merged. That’s because they had to be made available for the development of components-library. As a result, the component-library code might not get a proper review.

Introducing monorepo

The structure of our monorepo

The monorepo checkout includes the following structure of directories (along with other files):

Every package in our monorepo is an independent package,. As such, it can be pushed to an NPM repository like verdaccio or npmjs.

Lerna takes care of package versioning and symlinking dependencies defined in the package.json of every package. This is all done with a single command: lerna bootstrap. It creates a full development environment for all the packages in our monorepo.

Development flow

Now let’s examine the work we’ve completed on the same feature we showed you previously. But this time, we’re using an integrated repository.

Note a few things here:

  1. In this case, the development process doesn’t rely on an external repository like verdaccio.

  2. That’s why multiple developers can work side by side and don’t clash on common resources like version number of library components.

  3. The resulting merge request includes the diff done in the entire codebase, not just a subset.

  4. It takes only a few seconds to see the graphical feedback of the applied changes. As a result, developers don’t lose their focus on the task at hand due to long wait times.

Deployment strategy for frontend components with monorepo

In this section, we describe a deployment strategy which we used in another project. Our client had existing tooling and procedures, so the process described here would have to be adjusted to fit well into the infrastructure.

Specific challenges for frontend builds

The frontend stack is slightly different than the backend services. The frontend files are built into bundles and delivered through CDN to a web browser. At the point of their creation, the components can’t take in any further configuration.

Typically, the frontend application needs to know the base URL of the backend (or multiple backends) which it cooperates with. We need to inject this URL into the bundle during compilation.

This makes frontend builds different than builds of backend services which typically take configuration from environment at runtime.

Requirements set upon deployment

We typically require specific features from our deployment setup:

  1. Each deployed component (frontend or backend) is deployed in a specific version.

  2. The version is represented by an artifact persisted in a service.

a) We use S3 buckets for persisting frontend artifacts.

b) We use ERC repository for persisting backend service artifacts (docker images tagged with specific version).

3. We have a specific repository that contains explicit versions of components deployed in each environment. Making an update on staging/production requires making a change in this repository (updating the version) and pushing it to CI so that it takes care of updating the code in CDN/Backend clusters.

Detecting what changed in the monorepo

Lerna allows to easily determine the scope of changes that occurred in the repository since the last tagged build.

To release the new version, we use the lerna version command. This command detects which packages have been modified since the last version tag.

For the sake of argument, let’s assume that we’re at a stage where the monorepo consists of:

  • components-library  -  the frontend component

  • broker-app - another frontend component

  • partner-app  - the library used as dependency by both command components

Here’s how Lerna’s change detection works

If we modify the components-library component, only this package will have its version increased by the lerna version command.

On the other hand, if the components-library is modified, it will result in generating a new version of all 3 packages.

The same mechanism is what we typically use in the CI setup. Following the bash script is usually part of our CI setup which detects which components should be run. This script resides in ./scripts/test_changes.sh file:

function skip_if_not_changed() {
    if [ "$FORCE_CHANGED_ALL" == "true" ]; then
        echo "Not checking changes to $$1, because FORCE_CHANGE_ALL=true";
        return 1;
    fi;
    PACKAGE=$1;
    _load_changed;
    EMPTY=$(echo $CHANGED | jqn --color=false "filter(x => x.name === \"$PACKAGE\") | isEmpty");
    if [ "$EMPTY" == "true" ]; then
        echo "Skipping $PACKAGE, because it has not changed";
        return 0;
    else
        return 1;
    fi;
};

The complete CI script for running CI tests will look like this:

lerna bootstrap;
source ./scripts/test_changes.sh;
skip_if_not_chagned components-library || lerna run test-ci --scope components-library --stream;
skip_if_not_chagned partner-app || lerna run test-ci --scope partner-app --stream;
skip_if_not_chagned broker-app || lerna run test-ci --scope broker-app --stream;

This configuration runs CI tests only for relevant changes.

Build bucket

As mentioned earlier, we typically require that our deployment builds are stored as versioned artifacts. For projects that use AWS, we create the so-called build bucket which contains all the versions built for a specific environment.

Once the version is built, it can be deployed from the deployment repository CI described above. This part of the flow requires a DevOps engineer to specify which component gets updated and which version should be used to do that.

Note that this setup makes it very easy to rollback to any previous version in case we detect a breaking change.

Another very important thing to note is that despite using a monorepo for code organization, the components are built and deployed separately. It’s a common misconception that monorepos force the project into a monolithic architecture.

That’s just not true.

Lerna makes organizing dependencies between libraries and components “cheap,” reducing the friction caused by code modularization. This leads developers to create smaller code units that are easier to maintain and reuse.

Conclusion

As you can see, building a monorepo with Lerna brings many benefits to frontend projects. In our case, we get to reuse the components-library code (and all other code which is repeated) when working on other frontend applications. That has a huge impact on our productivity and accelerates our projects.

Marek Kowalski
Marek Kowalski

CTO / Co-Founder

Categories
Follow Us
AnimatedLogoTextImageAnimatedLogoFishesImage
RUMBLEFISH POLAND SP Z O.O.Filipa Eisenberga 11/3 31-523 Kraków, Polska
NIP: 6772425725REGON: 368368380KRS: 0000696628
P: +48 601 265 364E: hello@rumblefish.dev
Copyright © 2024 Rumblefish