DEV Community

Cover image for 🥇 The Lambdalith Advantage: A Complete Guide to NestJS Deployment on AWS Lambda Using CDK
Valentin BEGGI for Serverless By Theodo

Posted on • Updated on

🥇 The Lambdalith Advantage: A Complete Guide to NestJS Deployment on AWS Lambda Using CDK

TL;DR 📚

  • Why is a Lambdalith approach a high ROI choice for many? 💸
  • Learn how to deploy a monolithic NestJS app on AWS Lambda using Webpack and AWS CDK. 🚀

The Lambdalith Edge for NestJS on AWS Lambda 🌟

🧠 A Lambdalith is a monolithic architecture approach for serverless applications where a single AWS Lambda function serves the entire API, rather than deploying separate functions for each endpoint.

Opt for a Lambdalith and reap multiple benefits for your NestJS API:

  • Faster Rollouts: Quicker deployments and streamlined management, irrespective of your number of routes.
  • Minimized Cold Starts: Enhanced performance through more frequent reuse of a single Lambda function.
  • Easier Logging: A single point for logs simplifies monitoring and alert setup.
  • Full NestJS Benefits: Fully exploit NestJS's rich features and community support.

While a Lambdalith might mean lengthier cold starts and broader control scopes, its efficiency, simplicity, and high return on investment are unmatched.

Monorepo structure 🚧📦: I strongly advice you embrace a monorepo structure, with a package for your API and a package for your infrastructure (CDK).

Setting Up the Infrastructure with AWS CDK 🏗️

AWS CDK transforms infrastructure into code. Kick things off by installing AWS CDK and initiating a TypeScript project with cdk init app --language typescript.

In the lib/my-stack.ts file, begin with the core of your setup: the Lambda function.

// LambdaNestStack in stack.ts
const apiNestHandlerFunction = new Function(this, "ApiNestHandler", {
  code: Code.fromAsset("api/dist"), // 👈 This is crucial
  runtime: Runtime.NODEJS_18_X,
  handler: "main.handler",
  environment: {}, // 👈 You might need env variables
});
Enter fullscreen mode Exit fullscreen mode

Next up, create a Rest API with a Lambda proxy at its root. This API Gateway acts as the traffic controller, directing all requests to your Lambda-powered NestJS app. All route paths will be directed to your single Lambda. 🗾

const api = new RestApi(this, "Api", {
    deploy: true,
    defaultMethodOptions: {
    apiKeyRequired: true,
    },
});

api.root.addProxy({
    defaultIntegration: new LambdaIntegration(apiNestHandlerFunction, { proxy: true }),
});

const apiKey = api.addApiKey("ApiKey"); // 👈 to ease your testing

const usagePlan = api.addUsagePlan("UsagePlan", {
    name: "UsagePlan",
    apiStages: [
    {
        api,
        stage: api.deploymentStage,
    },
    ],
});

usagePlan.addApiKey(apiKey);
Enter fullscreen mode Exit fullscreen mode

In this snippet, Code.fromAsset("api/dist") is crucial. It points to the location of our bundled NestJS app, ensuring efficient Lambda execution.

Prepping the NestJS App for Lambda 🦁

Start by creating a new NestJS app with nest new api. Then, install the @nestjs/platform-express and @vendia/serverless-express packages.
You now have a classic NestJS app, ready to be adapted for AWS Lambda.

Next to the main.ts file, create a new lambda.ts file. This file will be the entry point of our Lambda function.

// lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@vendia/serverless-express';
import { Context, Handler } from 'aws-lambda';
import express from 'express';

import { AppModule } from './app.module';

let cachedServer: Handler;

async function bootstrap() {
  if (!cachedServer) {
    const expressApp = express();
    const nestApp = await NestFactory.create(
      AppModule,
      new ExpressAdapter(expressApp),
    );

    nestApp.enableCors();

    await nestApp.init();

    cachedServer = serverlessExpress({ app: expressApp });
  }

  return cachedServer;
}

const handler = async (event: any, context: Context, callback: any) => {
  const server = await bootstrap();
  return server(event, context, callback);
};

module.exports.handler = handler;
Enter fullscreen mode Exit fullscreen mode

This code will be executed by AWS Lambda. It creates a NestJS app and adapts it to the AWS Lambda environment. It also ensures that the NestJS app is only created once, improving performance. ⚡

🗒️ Side Note: You could easily setup a single main.ts entry point by leveraging env variables to deduce the execution context: Lambda or local

Now, we need to bundle this TypeScript code into a single file...🤓

Packing it Up with Webpack 📦🧙‍♂️

There are several ways to bundle a NestJS app for AWS Lambda. You could use Lambda Layers, but this is not the most efficient approach. Instead, we'll use Webpack to bundle our NestJS app into a single file, which we'll then deploy with AWS CDK.

Let's start by creating a new webpack.config.js file in our API package. This file will define our Webpack configuration.

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['./src/lambda.ts'],
    externals: [],
    output: {
      ...options.output,
      libraryTarget: 'commonjs2',
    },
    plugins: [
      ...options.plugins,
      new webpack.IgnorePlugin({
        checkResource(resource) {
          // Ignoring non-essential modules for Lambda deployment
          return lazyImports.includes(resource);
        },
      }),
    ],
  };
};
Enter fullscreen mode Exit fullscreen mode

This configuration bundles our Lambda entry file (lambda.ts) and its dependencies, creating a lean and efficient package for AWS Lambda!

Make sure to create a build-lambda script in your package.json file!

{
  "scripts": {
    "build-lambda": "nest build --webpack --webpackPath webpack.config.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Deploying the NestJS App: To the Cloud! ☁️

Your NestJS app is now a compact bundle, thanks to Webpack. Deploying? It's as simple as:

  • Build: Run npm run build-lambda in your API package.
  • Deploy: In your infrastructure package, execute cdk deploy.

And like that, your NestJS app ascends to AWS Lambda, primed for action. 💫

Your High-Performance NestJS App now lives on AWS 🚀

Congratulations! You've unlocked the strategy for a potent, scalable, and efficient NestJS app on AWS Lambda, all packaged neatly with Webpack and AWS CDK. 👏👏

Please feel free to comment if anything was unclear or if you found a better way to achieve this result! 💬

Top comments (1)

Collapse
 
ddewaele profile image
Davy De Waele

Great article ! We've also used this pattern and so far it has been working for us.

However as I was watching AWS re:Invent 2023 - Best practices for serverless developers (SVS401), I noticed they states the following

  • Lambda-lith : this works, and scales operationally, but lots of responsibility sits in that single lambda. They don't seem to promote this as the best solution.
  • Micro lambda : every single API route is a single lambda. An approach somewhat promoted by frameworks like SST / SAM. However they acknowledge that this can become an operational nightmare
  • Pragmatic lambda : grouping functions based on bounded context / permission groups / ..... = best of both worlds.

I like the Pragmatic-lambda approach, but they don't really offer any practical solution for it. Surely we don't want to implement our own little mini framework in a single lambda. We are thinking about using different NestJS lambdas for this, so splitting up our LambdaLith into separate lambas / routes.

Was wondering if your have any thoughts on this ?

Thanks again for the write-up !