Compiling a Apollo Federated Graph with esBuild 🚀
Tags: javascript, typescript, graphql, tutorial
Published: 2021-11-02 18:49:58

## Introduction

At Course Hero, we are starting to build our Apollo Federated Graph Services. For our local environment, we use Kubernetes to deploy our code. Keeping the advantages/disadvantages on the side, when it comes to building our local code it's going to take time due that we need to bundle the binary and sync it to K8 to be able to see it.

Our goal is to bundle and ship that local code as soon as possible to reduce the waiting time. Saving seconds here are goals here.

Below we're going go into details about how we were able to save around ~21 seconds when it comes to building the app binary, with esbuild 🚀

**EsBuild build**: Done in around **313ms**
**Webpack build**: Done in around **21.07sec**

## Current Setup

To give a background on the current setup of the project; 
- Monorepo setup using [Lerna](https://lerna.js.org/)
- Using Typescript, Node, and Express
- Gulp
- [Apollo Federation](https://www.apollographql.com/docs/federation/)

### Current Local Build Process

The current process of building a package locally is by running through a gulp task, using [ttypescript](https://www.npmjs.com/package/ttypescript)  to compile the TS and [@vercel/ncc](https://github.com/vercel/ncc) to build the binary:
```bash
npx gulp graph-accounts:local
```

Stats of the build, without esBuild:
```
[19:46:41] Starting 'graph-accounts:compile'...
[19:46:45] Finished 'graph-accounts:compile' after 4.07s
```

```js
// ttypescript.gulp.compile.js

const project = ts.createProject(`packages/${projectName}/tsconfig.json`, {
  typescript: require("ttypescript"),
});

return project
  .src()
  .pipe(project())
  .pipe(gulp.dest(`packages/${projectName}/lib`));
```

```
[19:46:45] Starting 'graph-accounts:binary'...
[19:47:02] Finished 'graph-accounts:binary' after 17s
```
```bash
npx @vercel/ncc build ./packages/graph-accounts/lib/index.js -o ./build/graph-accounts/bin/
```

```js
// binary.gulp.non-esbuil.js

const { spawnSync } = require("child_process");

const result = spawnSync(
  "npx",
  [
    "@zeit/ncc",
    "build",
    `./packages/${projectName}/lib/index.js`,
    "-o",
    `./build/${projectName}/bin/`,
  ],
  { stdio: "inherit" }
);
```

The total time spent in the `compile` and `binary` tasks were around **21.07sec**.

## Bundling with Esbuild

With the esbuild, we were able to reduce time on the `compile` and `binary` tasks to a stunning **313ms** that is a **20.7sec** 🚀 reduction.

Below are the stats for the two tasks, but before we go into details let's see how our esbuild is setups.

```
[19:53:10] Starting 'graph-accounts:compile'...
[19:53:10] Finished 'graph-accounts:compile' after 289 ms
[19:53:10] Starting 'graph-accounts:binary'...
[19:53:10] Finished 'graph-accounts:binary' after 24 ms
```

### Esbuild Setup

First, let start by installing `esbuild` as a dev dependency:
```
yarn add -D esbuild 
```

Below is a sample of our Monorepo folder structure:

```
graphql-services
├── packages
│   ├── graph-accounts
│   │   ├── ...
│   │   └── esbuild.config.server.js
│   └── graph-gateway
│       ├── ...
│       └── esbuild.config.server.js
├── scripts
│   ├── gulp
│   └── esbuild.config.base.js // base configs for esbuild
├── gulpfile.js
├── package.json
├── package.json
└── tsconfig.settings.json
```

Let dive into the `esbuild.config.base.js` configs. These are the default base config that we want esbuild to build off. We want to set the format of our build to [commonjs](https://esbuild.github.io/api/#format-commonjs) and the [platform](https://esbuild.github.io/api/#platform) to node . The [external](https://esbuild.github.io/api/#external) property can come in handy when you want to exclude a file or package from the build.

```js
// esbuild.config.base.js

module.exports = {
  external: ['express', 'newrelic'],
  platform: 'node',
  target: 'node16.13.0',
  bundle: true,
  minify: true,
  format: 'cjs'
}
```

Now that we have the base config that we can extend. Let go over the esbuild.config file for each of the underlying services. One key thing here is how we look up the env variables that we want to send over with the esbuild bundle.

```js
// esbuild.config.server.js 

const path = require('path')
const baseConfig = require('../../scripts/esbuild.config.base')

const define = {}
// lookup all the env in process.env, to be sent to the esbuild bundle
const keys = Object.assign({}, process.env)
for (const k in keys) {
  define[`process.env.${k}`] = JSON.stringify(keys[k])
}

const config = Object.assign({}, baseConfig, {
  entryPoints: [path.resolve(__dirname, 'src/index.ts')],
  outfile: path.resolve(__dirname, 'lib', 'index.js'),
  define,
  // TSConfig, normally esbuild automatically discovers tsconfig.json, but we can specified here
})

module.exports = config;
```

Our `compile` gulp task reads the underlying service `esbuild.config.server.js` to compile the code.

```js
// compile.task.js 

{
  ...
  compile: (projectName) => {
    return new Promise(async (resolve, reject) => {
      const esbuildConfig = require(`../../packages/${projectName}/esbuild.config.server.js`)
      try {
        esbuild.buildSync(esbuildConfig)
      } catch (error) {
        reject()
      }
      resolve()
    })
  }
}
```

Now we can run `npx gulp graph-accounts:compile` and 🎉 🎉

```
[19:53:10] Starting 'graph-accounts:compile'...
[19:53:10] Finished 'graph-accounts:compile' after 289 ms
```

```js
// bundle.esbuild.js

spawnSync(
  'cp',
  [
    '-a',
    `./packages/${projectName}/lib/.`,
    `./build/${projectName}/bin/`,
  ], { stdio: 'inherit' },
)
```

## Summary

Setting [esbuild](https://esbuild.github.io/) was very easy and the developer experience that we were able to get was stunning, without adding many dependencies. It saved us a tremendous amount of development time trying to build the apps, so give it a try!

Follow-up; Doing a comparison with Webpack and investigating [devspace](https://devspace.sh/) and [telepresence](https://www.telepresence.io/) for hot reloading experience between our local K8.


[**We’re hiring!**](https://www.coursehero.com/jobs/)