Johnny.sh

Nest JS On Lambda: A Concise Guide

August 28, 2022

Over the past two years or so, I’ve come to really appreciate the Nest JS framework. In my eyes, it brings that rigid, convention-driven application building experience that you would get with something like Ruby on Rails or Java Spring, and brings it to Node JS’s highly-productive ecosystem and high-performance runtime. One of the biggest gripes people have with Node is the lack of structure and convention, and Nest provides a great solution to that problem.

The other facet of this blog post is AWS Lambda, a powerful serverless platform that lets you run your code without thinking about servers.

Recently, I’ve been working on some projects that combine both of these two nice things: Nest JS and Lambda.

Nest JS does provide a basic doc about integrating Nest with another framework called Serverless, the result of which allows you to deploy your app to Lambda. Using Serverless™️ framework was a bit of overkill in my situation, and I just wanted to deploy my app to Lambda without having to get approval for an additional framework, adding yet another CLI to the stack, along with the plethora of decisions needed there.

I just want to deploy my Nest app to Lambda. Here’s how to do that.

TLDR: The source code for an example on GitHub can be found on this link here.

How does it work?

The strategy here is actually using Webpack under the hood. Yep. That’s right. Our old frontend powerhouse has now infiltrated the backend Node JS space, at long last.

A high level overview of how this works:

  • We run yarn build
  • Typescript compiles all of our typescript code to Javascript
  • Webpack pulls in all of our dependencies from node_modules
  • Webpack then concatenates all of our Nest app source code and node_modules into one .js file.
  • That one file is our deployment artifact, which we then deploy to lambda

In the end, we’re moving complexity to the build step. Our build step is complex, but deployment and runtime become dead easy — there’s only one file to worry about. Granted, it’s might be a behemoth 300k line .js file, but it is indeed just one file😉.

How to configure Nest JS to run on Lambda

Luckily, we don’t have to mess around with Webpack too much. We simply need to do two things:

Add --webpack to build command

In package.json, update your build command like so:

{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build --webpack",
    //...
  }
}

Just add the --webpack flag. Nest already has built in support for this flag, and no additional plugins or installation is needed. Easy!

Add webpack.config.js file

You also need to create a webpack config file in the root of your project. Call this file webpack.config.js, and add this code to it:

module.exports = (options, webpack) => {
  const lazyImports = [
    '@nestjs/microservices/microservices-module',
    '@nestjs/websockets/socket-module',
  ];

  return {
    ...options,
    externals: [],
    output: {
      ...options.output,
      libraryTarget: 'commonjs2',
    },
    plugins: [
      ...options.plugins,
      new webpack.IgnorePlugin({
        checkResource(resource) {
          if (lazyImports.includes(resource)) {
            try {
              require.resolve(resource);
            } catch (err) {
              return true;
            }
          }
          return false;
        },
      }),
    ],
  };
};

Common Issues, Caveats, and workarounds

Depending on Webpack to compile our backend code does create some potential issues.

Firstly, you cannot use any Node JS code which is using CommonJS modules. In other words, code using the require keyword will not compile properly, and will break when you try to run it. Make sure your code is using ES modules; that goes for both your application code and your dependencies in node_modules.

Secondly, some dependencies may break the build and/or runtime for other reasons. Any node_module dependency which relies on native modules of the host computer will probably break your app at runtime. For example, the pg-native module will break at runtime as it tries to import libpq.

If you have a dependency which requires or imports such code, but does not execute that code, it may still break your app. You can create a workaround for this by using webpack.ignorePlugin. Add this to your webpack.config.js:

{
  "plugins": [
    // ...
    new webpack.IgnorePlugin({ resourceRegExp: /^pg-native$/ })
  ]
}

Lastly, if you have any code that depends on the file system, this will break your app. This is not due to the Webpack approach here, but this is an issue which is true for any Lambda or other serverless runtime application.

Writing for Portability

One big advantage of this approach: since Nest JS is already a full-blown backend application framework, you can write your code in a way that is agnostic to the deployment target, to avoid vendor lock-in. Typically when writing a Node JS backend component you need to choose early on: do I want this to run on Lambda? Or will this be a long-running traditional server?

If your requirement is to run the component on Lambda, you might choose Serverless framework or, even worse, just write a raw freestyle Node JS script. After going down either of these routes, there’s no turning back. If cold starts are an issue for meeting your business goals, or if you need to run your app in a long-running server process, there’s no way to convert your Serverless app to run on express. The same is true for an express app: it’s difficult to convert an existing tradition express server to to run on Lambda.

With this Nest JS strategy, you don’t have to make this tradeoff, and can easily avoid vendor lock-in to a specific deployment target. You can deploy with Webpack and run your app as a Lambda, paying only for what you use, OR, if needed, you can also deploy your nest JS app as a long-running traditional server using Nest JS’s built-in default bootstrap function.

In other words, you can write your Nest JS code to be compatabile with both Lambda and a traditional long-running server. This is what we call “portability” — the application is written to be compatible irregardless of the infrastructure, instead of written around/coupled to the infrastructure it will run on

To do this, we edit the main.ts file in our Nest app like so:

import { NestFactory } from '@nestjs/core';
import { APIGatewayEvent } from 'aws-lambda';
import { AppModule } from './app.module';
import { CatService } from './cats/cats.service.ts';

export const handler = async (event: APIGatewayEvent) => {
  const appContext = await NestFactory.createApplicationContext(AppModule);
  const catService = appContext.get(CatService);
  const res = await catService.handleEvent(event);
  return res;
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

if (process.env.NEST_ENV === 'HTTP') {
  bootstrap();
}

As you may know, main.ts is the default server entrypoint for a Nest app. The bootstrap function here is the default function to start a Nest app on a port in the traditional way. Here, all we have to do is use an environment variable called NEST_ENV to decide whether or not to start the service.

When building for Lambda, set the NEST_ENV environment variable to LAMBDA, and when running it as a server set the NEST_ENV to HTTP. Now our Nest app is easily portable between deployment targets.

In Conclusion?

In conclusion, I would say this is one option you can take if you’re using Node JS, Lambda, Typescript, and you want high-quality clean code but you don’t know where it is going to be deployed in the future. It’s not the only option, and it’s definitely not without its downsides. I mean, we’re using Webpack to bundle backend code for sheist’s sake 🙃🙃 that has to be a sin of some kind.

As mentioned before, there’s an example repo on my github link here.

Nonetheless, it works, and makes for quite clean code that is easy to deploy. If you have any issues or questions, please email me: johnny@johnny.sh.

Key visual: Somewhere in Montana, with Nest JS logos laid on top.

Last modified: August 30, 2022
/about
/uses
/notes
/talks
/projects
/podcasts
/reading-list
/spotify
© 2024