Intro to monorepo with Nx

No Comments

What is a monorepo?

To understand why you may want to use the build system Nx (see website), we should first talk about what a monorepo is and why you may want to use one. A monorepo is a repository (probably a Git repository) that contains more than a single project. The simplest of all reasons why this could be appealing is when two or more projects are supposed to share some codebase, like when you build an SPA (single-page application) and a Node backend and some of your TypeScript type definitions are shared between the two projects. There are a couple of ways you can achieve this, but simply importing them into both projects from a shared directory usually seems like the simplest way.

How does Nx fit into this?

So what do you need Nx for if everything in a monorepo can be imported in a very simple way? Turns out, the world of monorepos isn’t that simple after all. For example, your TypeScript compiler or Babel might complain that you’re now importing things from outside your current source root directory. It would also be kind of nice if your shared code didn’t have to be imported with a ridiculously long import statement like import { athing, ortwo } from "../../../../../../shared/mylib" which will surely break when you move something around during refactoring. In addition, when you’ve worked for a while with multiple projects in a single repository, your folder structure might be a bit of a mess (i.e. lots of folders which contain projects, some of which aren’t even projects, but helpers or bash scripts for some task you’ve done in the past). Nx will help you with those things by providing the following in an opinionated way:

  • bootstrappers for common projects (Node, Nest, Angular, React, …)
  • unified interface for code generators (react-components, nestjs-controllers, …)
  • structuring projects and libraries (for shared code)
  • providing tools for recurring tasks

As with all opinionated frameworks you’ll receive a trade-off between full control over everything (and the hassle that comes with maintaining it) and the loss of some granularity (in favor of things that work as documented and have the same look and feel all around).

To put it into perspective: if you’re the kind of person who likes to do an eject right after creating a new React application with create-react-app, you probably won’t have much fun with Nx. But if you’re trying to avoid an eject at all cost, so you don’t have to maintain the whole build chain yourself, you might find that Nx is to your liking.

Starting with something basic

Now that you know what you’re getting into, let’s create a simple Nx monorepo with a web application that communicates with a Node backend. We’ll be using React and Express with TypeScript. You won’t need any special knowledge of either of these, most of the magic will happen in the command line.

Bootstrapping Nx monorepo

To get started with an Nx monorepo, fire up your terminal of choice and navigate to a directory where you want to create your monorepo. The following command will create a new directory for the monorepo and bootstrap Nx in it.

npx create-nx-workspace getting-started-with-nx

Nx will prompt you to specify what you want to create. For this tutorial it makes sense to simply select apps. If we were to go with a more complete workspace type, there won’t be much to explain in this tutorial.

Selecting a (mostly) empty structure for our new project

Nx will also ask about whether to use Nx Cloud or not. Nx Cloud would cache build artifacts and with that improve building speed on your machine and any other machine (i.e. CI, team member’s machines). Since our project probably won’t ever leave your machine, it’s safe to say you will not need it. If you’re interested in more details about what Nx Cloud can do for you, I recommend you read through the Nx.Cloud website. They have a free entry-level tier, so you might as well give it a try some time.

Creating a React app

To create the frontend part of this tutorial, we’ll use the Nx command line interface. You’ve bootstrapped your Nx repo in the previous step, now navigate your terminal into the newly created directory and execute:

yarn add @nrwl/react
yarn nx g @nrwl/react:application frontend

The first line is necessary to add the React tooling to your Nx repo. The second line will generate a React app called frontend.

Again Nx will ask for some details. In this case it will ask you how you wish to style your components in React. You could select any of the options according to your preferences. If you’re unsure, select styled-components as I did for this tutorial. (No worries, we won’t do any CSS in this tutorial)

Monorepo with Nx: Selection of styling method during creation of react app

After a few moments you should have a fully functional React application which you can now start with the command:

yarn nx serve frontend

Starting the react app in the command line

Fire up your favorite browser and navigate to http://localhost:4200/ to see the sample page created by Nx.

Navigating the browser to http://localhost:4200/ while the app is running

You can leave the terminal open and the application running for the next steps.

Creating a Node app

Creating an additional application in Nx is pretty much always the same in Nx. Let’s create a very simple Node backend with express as framework. To do so, run the following commands in your Nx repo:

yarn add @nrwl/express
yarn nx g @nrwl/express:application backend

Again the first will add the Nx tooling to your repo, the second will generate the actual application.

Having completed this step you already have a functional backend. Fire it up by executing:

yarn nx serve backend

The backend will start on port 3333 by default, so use your browser to navigate to http://localhost:3333/api. You should see a valid response, although it might not be as pretty as Firefox presents it in the following screenshot.

Starting the express app

Again, you can leave the terminal open and leave the application running. The dev servers will reload whenever you change a file to serve the newest version of your applications.

Tie the knot between frontend and backend

Since we haven’t done any coding so far, it’s high time to change that.

Fire up your favorite IDE (or plain editor) and open the Nx monorepo directory.
Hint: Unsure what editor or IDE to use? Use Visual Studio Code. Nx provides plugins for it and as a long-term user I can say that it’s a pretty decent IDE.

Let’s have our backend return something else than a static string. Open up apps/backend/src/main.ts and make it look like this:

import * as express from "express";
 
const app = express();
 
app.get("/api", (req, res) => {
  const name = req.query.my_name_is;
  res.send({ message: `Hello ${name}, Welcome to backend!` });
});
 
const port = process.env.port || 3333;
const server = app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/api`);
});
server.on("error", console.error);

This extends the /api endpoint so that it takes the parameter my_name_is from the query string of the request and echoes the name in the response.

Now we need to pass the my_name_is argument in from the frontend. In order to do so, we need to make a couple of adjustments to our web app. The following snippet shows an adjusted version of the file apps/frontend/src/app/app.tsx.

import { useCallback, useState } from "react";
 
export function App() {
  const [input, setInput] = useState("");
  const [message, setMessage] = useState();
  const onButtonClick = useCallback(async () => {
    if (input) {
      const response = await fetch(
        `/api?my_name_is=${encodeURIComponent(input)}`
      );
 
      const jsonResult = await response.json();
      setMessage(jsonResult.message);
    }
  }, [input]);
 
  return (
    <>
      <label>
        Name:
        <input name="name" type="text" value="{input}" /> setInput(event.target.value)}
        />;
      </label>
      <button>Click me</button>
      {message ?</ pre>
<pre>{message}</ pre> : null}
    </>
  );
}
 
export default App;

If you haven’t worked with React before, this might look a bit like magic.
The essentials are:

  • There is an HTML input-element that stores its UI state in the variable input
  • A button captioned “Click me” executes a callback
  • The callback named onButtonClick performs an HTTP GET to /api that passes along the current input state
  • The response of the callback is stored in message
  • When message is set, it will be displayed on the page, wrapped in pre-tags

You might have noticed that we’re fetching data from /api which the browser will interpret as http://localhost:4200/api. We could simply replace /api with the full URL here and be done with it. However, there are two reasons we’re not going to do that:

  1. Usually you don’t want to put fixed URLs in your UI code. Your app simply won’t work anywhere but on a local machine.
  2. To comply with same origin policies, requests are usually routed through the same domain.

We can easily simulate the routing functionality with a bit of Nx configuration. The process is explained in detail as an Nx Tutorial. Two things need to be done:

Firstly, we’ll create a new file apps/frontend/proxy.conf.json and put in the following content:

{
  "/api": {
    "target": "http://localhost:3333",
    "secure": false
  }
}

This essentially tells the react dev server to forward every request to /api to our backend, which is running at http://localhost:3333.

Secondly, we need to tell Nx to actually use this file.
To do so, edit the file apps/frontend/project.json and add the proxyConfig property as shown below in the serve target options.

"serve": {
    "executor": "@nrwl/web:dev-server",
    "defaultConfiguration": "development",
    "options": {
        ...
        "proxyConfig": "apps/frontend/proxy.conf.json"
    },
    ...
}

Frontend and backend in action

When you start both of the apps (yarn nx serve backend and yarn nx serve frontend), you can now navigate to http://localhost:4200 and see the frontend work together with our backend.

Frontend and Backend working together

Congrats! You have just created your first full-stack Node application with a little help of Nx.

Sharing code

As mentioned in the introduction, it is possible to easily share code between two apps in Nx. To give you an idea of how this works, execute the following command in your Nx repo to create a shared library.

yarn nx generate @nrwl/node:library shared-code

This will create a new shared library called shared-code in the libs/ directory inside your repo. You will also get a stub to get you started with your new library. You will find the stub in libs/shared-code/src/lib/shared-code.ts and it will look something like this:

export function sharedCode(): string {
  return "shared-code";
}

While not the most useful program ever created, this is quite sufficient for showing that both the frontend and the backend are able to access this code. To do so, open up your apps/backend/src/main.ts again and tell your backend to send the result of the shared code as an HTTP header like this

app.get("/api", (req, res) => {
  const name = req.query.my_name_is;
  // New:
  res.header("x-shared-code", sharedCode());
  res.send({ message: `Hello ${name}, Welcome to backend!` });
});

The actual magic is that you can import the sharedCode-function like this:

import { sharedCode } from "@getting-started-with-nx/shared-code";

This is possible because Nx created a common namespace for your repo during bootstrap.
It also added the required configuration to your tsconfig.base.json in the root directory of your repo when you generated the shared library. It works exactly the same for the frontend when you edit your apps/frontend/src/app/app.tsx and add a usage of the shared library code in the onButtonClick-callback like this:

const onButtonClick = useCallback(async () => {
  if (input) {
    const response = await fetch(
      `/api?my_name_is=${encodeURIComponent(input)}`
    );
 
    // New:
    if (response.headers.get("x-shared-code") === sharedCode()) {
      alert("This line is secure!");
    }
 
    const jsonResult = await response.json();
    setMessage(jsonResult.message);
  }
}, [input]);

When you click the button in your frontend again, you should see that the verification of the additional header from the backend works. Because both apps use the same code, you don’t have wo worry about whether the sharedCode function will ever deliver diverging results to the frontend and backend.

Frontend and Backend using shared code

Summary & outlook

This tutorial is merely a starting point in case you wanted to try out Nx. There are a lot more really cool things about Nx to explore.

I created a GitHub repo with all the code shown in this tutorial. Take a look at the commit history to see the changes after each step described in this tutorial.

Since we’re only getting started, let me give you an outlook on what’s ahead and what tutorials I could be writing next. Let me know what you’re interested in (not necessarily in this order):

  • Common Nx tasks (using executors and builders)
  • Custom executors/builders and generators
  • Building Nx apps in a CI/CD (with GitHub Actions)

Johann plays for the codecentric team Stuttgart since 2016. He likes to call himself Señor Full Stack Developer and enjoys building castles in the sky a.k.a. cloud solutions. He loves to experiment with new JavaScript/Node.js frameworks and technologies all day long.

Comment

Your email address will not be published.