DEV Community

Cover image for Deploy a NestJS API to AWS Lambda with Serverless Framework

Deploy a NestJS API to AWS Lambda with Serverless Framework

Marko Djakovic on April 20, 2022

Have you ever wondered how easy it can be to deploy and host an API? Scalable, stable, a piece of cake to deploy and costs almost nothing. The goal...
Collapse
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

Thanks for this tutorial. Sadly after running sls deploy, when I go to the production URL ending with / song, I get a 500 error. In Cloudwatch the error is: Cannot find module './dist/lambda.js'". Any idea on how to fix it? Cheers

Collapse
 
thatkit profile image
Nikita

It might be because nest-cli.json config file only expects main.ts to get compiled to main.js. So, one should specifylambda.ts as well or instead write hanlder() function in main.ts.

    "telegram-bot": {
      "type": "application",
      "root": "apps/telegram-bot",
      "entryFile": "main", // change to lambda
      "sourceRoot": "apps/telegram-bot/src",
      "compilerOptions": {
        "tsConfigPath": "apps/telegram-bot/tsconfig.app.json"
      }
    },
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra • Edited

I tried both options, but both results were Cannot find module either ./dist/lambda.js or ./dist/main.js
For the first: I added the entryFile to my out-of-the-box best v.10.0.0 different looking nest-cli.json :

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
 "entryFile": "lambda", // added as suggested
  "compilerOptions": {
    "deleteOutDir": true
  }
}
Enter fullscreen mode Exit fullscreen mode

For the second option, I added the handler to the main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { configure as serverlessExpress } from '@vendia/serverless-express';

let cachedServer;
export const handler = async (event, context) => {
  if (!cachedServer) {
    const nestApp = await NestFactory.create(AppModule);
    await nestApp.init();
    cachedServer = serverlessExpress({
      app: nestApp.getHttpAdapter().getInstance(),
    });
  }

  return cachedServer(event, context);
};

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

Enter fullscreen mode Exit fullscreen mode

and updated the serverless.yml from

functions:
  api:
    handler: dist/lambda.handler
Enter fullscreen mode Exit fullscreen mode

to

functions:
  api:
    handler: dist/main.handler
Enter fullscreen mode Exit fullscreen mode

my tsconfig.json in case is relevant is:

{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "allowJs": true,
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "ES2021",
    "sourceMap": true,
    "outDir": "./dist",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
  }

}

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
thatkit profile image
Nikita

Thanks for the reply! I guess I have to try the guide my own to check the reason solid clear. 'll get back with an update!

Thread Thread
 
thatkit profile image
Nikita

I'm back! :)
I've tried the whole thing with my NestJS + serverless.yaml setup and everything works good enough. However, I'm using Yandex Serverless Functions instead of AWS Lambda, but I tend to think this part is irrelevant to the issue.

Here're some of the info:

  1. nest version 9.5.0
  2. serverless versions
Framework Core: 3.38.0 (local) 3.38.0 (global)
Plugin: 7.2.0
SDK: 4.5.1
Enter fullscreen mode Exit fullscreen mode
  1. serverless.yaml
service: jojob-test

useDotenv: true

plugins:
  - serverless-offline
  - '@yandex-cloud/serverless-plugin'

package:
  patterns:
    - '!**'
    - package.json
    - package-lock.json
    - dist/**
    - config/production.js

provider:
  name: yandex-cloud
  runtime: nodejs18
  httpApi:
    payload: '1.0'

  environment:
    TG_TOKEN: ${env:TG_TOKEN}

    HH_CLIENT_ID: ${env:HH_CLIENT_ID}
    HH_CLIENT_SECRET: ${env:HH_CLIENT_SECRET}

    YOO_KASSA_SHOP_ID_TEST: ${env:YOO_KASSA_SHOP_ID_TEST}
    YOO_KASSA_SHOP_ARTICLE_ID_TEST: ${env:YOO_KASSA_SHOP_ARTICLE_ID_TEST}
    YOO_KASSA_TOKEN_TEST: ${env:YOO_KASSA_TOKEN_TEST}

    OAUTH_BASE_URL: ${env:OAUTH_BASE_URL}

functions:
  main:
    handler: dist/apps/telegram-bot/main.handler
    memorySize: 512
    timeout: 30
    account: function-sa
    events:
      - http:
          method: post
          path: /${self:provider.environment.TG_TOKEN}

resources:
  trigger-sa:
    type: yc::ServiceAccount
    roles:
      - serverless.functions.invoker
  function-sa:
    type: yc::ServiceAccount
    roles:
      - editor
Enter fullscreen mode Exit fullscreen mode

A note here! In the package rule I'm making sure that the dist directory with my main.ts file is compiled and other files that I do need.

  1. webpack.telegram.config.js (yes, since I need one according to the official docs)
/* eslint-disable @typescript-eslint/no-var-requires */
const nodeExternals = require('webpack-node-externals');
const NodeConfigWebpack = require('node-config-webpack');

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

  return {
    ...options,
    externals: [
      nodeExternals({
        allowlist: ['config'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new NodeConfigWebpack(),
      new webpack.IgnorePlugin({
        checkResource(resource) {
          if (lazyImports.includes(resource)) {
            try {
              require.resolve(resource);
            } catch (err) {
              return true;
            }
          }
          return false;
        },
      }),
    ],
    output: {
      ...options.output,
      libraryTarget: 'commonjs2',
    },
  };
};
Enter fullscreen mode Exit fullscreen mode
  1. nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/jojob-api/src",
  "compilerOptions": {
    "deleteOutDir": true,
    "webpack": true,
    "tsConfigPath": "apps/jojob-api/tsconfig.app.json"
  },
  "projects": {
    "telegram-bot": {
      "type": "application",
      "root": "apps/telegram-bot",
      "entryFile": "main",
      "sourceRoot": "apps/telegram-bot/src",
      "compilerOptions": {
        "tsConfigPath": "apps/telegram-bot/tsconfig.app.json"
      }
    }
  },
  "monorepo": true,
  "root": "apps/jojob-api"
}
Enter fullscreen mode Exit fullscreen mode
  1. And finally, main.ts
import { NestFactory } from '@nestjs/core';
import serverlessExpress from '@vendia/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { TelegramBotModule } from './telegram-bot.module';

let server: Handler;

async function bootstrap(): Promise<Handler> {
  const app = await NestFactory.create(TelegramBotModule);
  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();

  return serverlessExpress({ app: expressApp });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) => {
  server = server ?? (await bootstrap());

  return server(event, context, callback);
};

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
thatkit profile image
Nikita

I'm not entirely sure what the exact reason behind your issue, but I guess you could try to debug if the main.ts/lambda.ts is being compiled into main.js/lambda.js and, furthermore, being packaged by Serverless Framework (in a .zip archive). I hope my configs will be of some help

Collapse
 
anilsonix profile image
Anil Kumar Soni

Thanks, great article.
Earlier I was able to run and test nest-api locally by running npm run start:dev.
But running this cmd with serverless-express , its not running on locally with watch mode.

How can I do this , so that both npm run start:dev gives me local api to play around.

Collapse
 
imflamboyant profile image
Marko Djakovic AWS Community Builders

Hi, thanks for reading and for your comment! I believe you should be able to run it just as before, so npm run start:dev should work. The serverless-express wrapper is only ran when it's deployed via serverless.

To emulate Lambda locally serverless-offline plugin can be used, however I am not sure if it supports the watch mode equivalent.

In any case let me know if you find any specifics, or if I misunderstood your question.

Collapse
 
anilsonix profile image
Anil Kumar Soni

Thanks, I was following another tut also, got confused. In another tut it modify the main.ts rather than creating a new file lambda.ts.

Thread Thread
 
imflamboyant profile image
Marko Djakovic AWS Community Builders

Cool 😉

Collapse
 
trinhxyz profile image
Anthony Trinh

Hi, thanks for the article!

I was just wondering if you've run into the issue where DTO decorators for class-transformer and class-validator work locally in testing, but do not work once deployed to AWS ApiGateway/Lambda? Locally, the request will just be outright reject and it won't even attempt to process the request, when deployed it will attempt to process a request with a malformed DTO.

I am using the exact same tooling as you, and very similar set up.

Thanks!

Collapse
 
imflamboyant profile image
Marko Djakovic AWS Community Builders • Edited

Hi, thanks for the comment. If I understand your question correctly, I'd say that this is expected behavior. Lambda should proxy every request, it's up to your Nest app to handle validation responses, and just proxy them back to API Gateway.

If I misunderstood then feel free to provide more details about your issue and I'll try to pitch in. 👍