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 • Edited 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 (5)

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 !

Collapse
 
manishmandal profile image
Manish Mandal • Edited

Never trust AWS people on this, they don't want our apps to be simple and platform-independent.

Lambdalith's approach can help us move to vm/containers anytime we want.

Dividing services based on DDD's bounded context is a great approach, I use that for most backends.

Collapse
 
mathenshall profile image
Mat

I would add that using a graphql server as a lambda that represents that bounded context/domain works really well in this approach. And the LambdaLith approach to lambda is ideally suited

Collapse
 
davidd22 profile image
david munsa

nice

now how do i actually invoke the api ?

should i call it like before just change the base URL?

i mean if my http call looked like this

{{baseUrlPrd}}/product/hash-tags

and now my aws api gateway metohd look like this

https://3p6eyut9b3.execute-api.eu-central-1.amazonaws.com/prod/lambda-general

how should i call it now ?

thanks

Collapse
 
douglasgc profile image
Douglas Gabriel Cardoso

Hello everyone, I've been developing a solution for modular monoliths using Lambda and NestJS. In this project, my main focus has been on optimizing the bundle with esbuild and adhering to the best practices for RDS, such as using RDS proxy for connection pooling. I believe this could be beneficial to many. The project also includes structured services, APIs, Swagger documentation, as well as the execution of migrations and seed data.

github.com/atlas-cli/nestjs-boiler...