Monorepo Backend Application with Bundled Packages
Tags: monorepo, javascript, node, webdev
Published: 2024-05-10 10:54:46

I spent hours searching and struggling to set up a server within a monorepo alongside private packages, trying various scripts and methods. Little did I know, the solution was right in front of me the whole time, simple and straightforward to implement.

The frustrating part is, when you're working on a [Next.js](https://nextjs.org/) project within a monorepo, adding your module to the [transpilePackages](https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages) entry in the configuration is all it takes. However, for a backend applications with a custom build step, it's not as straightforward.

I began by creating a monorepo using [turbo](https://turbo.build/repo/docs/getting-started/create-new) with applications and packages. It took me 5 minutes with everything installed and working like a charm! Then I was setting up the backend application, utilizing [swc](https://swc.rs/) to transpile TypeScript into CommonJS format. 

My goal was to facilitate type sharing between my web application and backend, utilizing a shared library named "common". Implementing this in the web application, built with Next.js, was straightforward. However, I encountered challenges with the backend. While the transpilation process worked seamlessly on project files, the bundled version required a TypeScript file from the common package, causing issues.

Determined to find a solution, I rolled up my sleeves and delved into options for transpiling my common module. Despite exhaustive searching, `swc` didn't offer the necessary functionality, and I wasn't keen on resorting to Babel. Then, I stumbled upon `esbuild`, which promised to transpile all my code into a single bundle. It was a game-changer! 
##The Solution 
To build the project I had to create a `build.mjs` file.
To transpile and bundle the project, simple run:
```bash
> node build.mjs
```

This is the content of the file:
```js

import * as esbuild from "esbuild";
import packageJson from "./package.json" assert { type: "json" };

// Bundle internal dependencies
const external = Object.keys(packageJson.dependencies).filter(
  (dep) => !dep.startsWith("@repo")
);

await esbuild.build({
  entryPoints: ["./src/index.js"],
  sourcemap: true,
  bundle: true,
  tsconfig: "tsconfig.json",
  platform: "node",
  outfile: "dist/index.js",
  external,
});
```

### Why mjs ?
The build process is waiting for the result, and I'm utilizing import statements. While I could include `"type": "module"` in my `package.json` file, doing so risks the bundle targeting `esm`, which would stress all the dependencies to use `esm`. However, it's worth noting that some libraries still don't support `esm`, which can be frustrating, but it's the reality we face.

### External Dependencies
Initially, I attempted to run the build process without including the external entry. This led to lots of errors as it attempted to bundle all external dependencies into my bundle. I had hoped for a more robust solution. Consequently, I sought a method to bundle only my private packages, leaving the external ones outside.

Iterating through the dependencies and excluding my private ones, identifiable by their known names, assures me that they will be included in the bundle. For instance, `@repo/common` will be bundled, while `@fastify/cors` will not.

### The Result
A bundle with my private packages inside!
There is no need to a build step in the common package, only on the top level. The checks I do have in the packages is lint, type checking and tests.


I hope that by sharing this article, I've saved someone valuable time in their search. This approach offers a practical method for managing a backend application within a monorepo while leveraging private packages.