DEV Community

alony
alony

Posted on

Monorepo Backend Application with Bundled Packages

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 project within a monorepo, adding your module to the 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 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 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:

> node build.mjs
Enter fullscreen mode Exit fullscreen mode

This is the content of the file:


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,
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)