DEV Community

Anton Golub
Anton Golub

Posted on

NestJS + esbuild workarounds

Q: Do we need ESM for backend apps?
A: Yes. More and more pkgs are moving to the ESM format, this is inevitable.

Q: Do we need bundles?
A: Probably, yes.

  • If your Nestjs app is distributed as a container with node_modules inside, bundling can greatly reduce its size.
du -hs ./node_modules
495M   ./node_modules
du -hs ./output.js   
10M    ./output.js
Enter fullscreen mode Exit fullscreen mode
  • Bundle is the best point for a thorough ISEC audit: to search for redos, suspicious API usages, protestware, etc.

  • The fewer fs I/O, the faster the application starts.

  • (TS) bundle absorbs the all required contents, so there is no tsc-esm transpilation issues

Q: esbuild, swc, rollup, parcel or babel?
A: esbuild.

Stack

:-(

const config =  {
  platform: 'node',
  target: ['node18', 'es2021'],
  format: 'esm',
  bundle: true,
}
Enter fullscreen mode Exit fullscreen mode

Problems

https://github.com/nestjs/nest-cli/issues/1157
https://github.com/nestjs/swagger/issues/1450
https://github.com/evanw/esbuild/pull/509
https://github.com/evanw/esbuild/issues/566

0. Some Nestjs internals are lazy-loaded, so sometimes they may be omitted, but in another cases they should be bundled. How to handle? https://esbuild.github.io/api/#external

const config =  {
  ...
  external: [
    'mqtt',
    'amqplib',
    'class-transformer/storage'
  ]
}
Enter fullscreen mode Exit fullscreen mode

1. openapi is not defined. https://github.com/nestjs/swagger/issues/1450

__decorate([
  Post('event-unsafe-batch'),
  HttpCode(200),
  openapi.ApiResponse({ status: 200, type: String }),
  __param(0, Body()),
  __param(1, Req()),
  __metadata("design:type", Function),
  __metadata("design:paramtypes", [Object, Object]),
  __metadata("design:returntype", Promise)
], EventUnsafeController.prototype, "logEventBatch", null);
Enter fullscreen mode Exit fullscreen mode

2. openapi / class-validator DTOs are referenced by require API. https://github.com/microsoft/TypeScript/issues/43329

export class CspReportDto {
  static _OPENAPI_METADATA_FACTORY() {
    return { timestamp: { required: false, type: () => Object }, 'csp-report': { required: true, type: () => require("./csp.dto.js").CspReport } };
  }
}
Enter fullscreen mode Exit fullscreen mode

3. NodeJS builtins are referenced via require API.

var require_async4 = __commonJS({
  "node_modules/resolve/lib/async.js"(exports, module2) {
    var fs2 = require("fs");
Enter fullscreen mode Exit fullscreen mode

4. esbuild-compiled ESM bundle cannot refer to views/redoc.handlebars

const redocFilePath = path_1.default.join(__dirname, "..", "views", "redoc.handlebars");
Enter fullscreen mode Exit fullscreen mode

5. _OPENAPI_METADATA_FACTORY class fields may be empty, so the swagger declaration cannot be properly rendered.

var Meta = class {
};
// →
var Meta = class {
  static _OPENAPI_METADATA_FACTORY() {
    return { appName: { required: false,  type: () =>  String }, appHost: { required: false,  type: () =>  String }, appVersion: { required: false,  type: () =>  String }, appNamespace: { required: false,  type: () =>  String }, appConfig: { required: false,  type: () =>  typeof (_a3 = typeof Record !== "undefined" && Record) === "function" ? _a3 : Object }, deviceInfo: { required: false,  type: () =>  typeof (_b3 = typeof Record !== "undefined" && Record) === "function" ? _b3 : Object }, userAgent: { required: false,  type: () =>  String }, envProfile: { required: false,  enum:  typeof (_c = typeof import_substrate2.EnvironmentProfile !== "undefined" && import_substrate2.EnvironmentProfile) === "function" ? _c : Object } }
  }
};
Enter fullscreen mode Exit fullscreen mode

6. Extra type wrappers cannot be processed by openapi / class-validator / class-transformer

  __metadata("design:type", typeof (_d = typeof Array !== "undefined" && Array) === "function" ? _d : Object)
  __metadata("design:type", typeof (_e = typeof import_substrate2.LogLevel !== "undefined" && import_substrate2.LogLevel) === "function" ? _e : Object)
  // →
  __metadata("design:type", Array)
  __metadata("design:type", import_substrate2.LogLevel)
Enter fullscreen mode Exit fullscreen mode

How to fix?

The right solution is certainly to improve the tools: create issues, discuss, suggest PRs.

How to fix it right here and right now?

@anatine/esbuild-decorators + old good monkey patching.
Fragile. Wrong. Terrible.

// build.js
import { build } from 'esbuild'
import path from 'node:path'
import { esbuildDecorators } from '@anatine/esbuild-decorators'

const cwd = process.cwd()
const outfile = path.resolve(cwd, 'output.js')
const tsconfig = path.resolve(cwd, 'tsconfig.json')
const entryPoints = [path.resolve(cwd, 'src/main/ts/index.ts')]
const config =  {
  platform: 'node',
  target: ['node18', 'es2021'],
  format: 'esm',
  bundle: true,
  keepNames: true,
  plugins: [
    esbuildDecorators({
      tsconfig,
      cwd
    }),
  ],
  tsconfig,
  entryPoints,
  outfile,
  external: [
    'kafkajs',
    'mqtt',
    'amqplib',
    'amqp-connection-manager',
    'nats',
    '@grpc/grpc-js',
    '@grpc/proto-loader',
    '@nestjs/websockets/socket-module',
    'class-transformer/storage'
  ]
}

await build(config)
Enter fullscreen mode Exit fullscreen mode
node build.js && nestjs-esm-fix --target=./output.js
Enter fullscreen mode Exit fullscreen mode

But it works.

Top comments (2)

Collapse
 
demsey2 profile image
Dawid Tomkalski

how do you know what external modules can be excluded? do you have a script to detect that?

Collapse
 
niraltmark profile image
Nir Altmark

What was the bundle size before and after?