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

Thread Thread
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

Just by adding the package section to the serverless.yml, and changing the handler there to dist/src/main.handler. Make it work, except now it's complaining about not finding modules, so I need to install them one by one : tslib,uid, iterare .... and it does not sound good. What am I missing?

Thread Thread
 
thatkit profile image
Nikita

OK. Now I would say some files are not packaged at all. Could you check the packaged file system in AWS Lambda editor? It seems the package.json is missing

Thread Thread
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

The file is present in the zip I downloaded from the lambda console.

Thread Thread
 
thatkit profile image
Nikita

Could you please show the latest error you've got?

Thread Thread
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

ERROR Error: Cannot find module 'fast-safe-stringify'
Require stack:

  • /var/task/node_modules/@nestjs/core/injector/module-token-factory.js
  • /var/task/node_modules/@nestjs/core/injector/compiler.js
  • /var/task/node_modules/@nestjs/core/injector/container.js
  • /var/task/node_modules/@nestjs/core/injector/index.js
  • /var/task/node_modules/@nestjs/core/index.js
  • /var/task/dist/src/main.js
  • /var/task/s_api.js
  • /var/runtime/index.mjs at Module._resolveFilename (node:internal/modules/cjs/loader:1077:15) at _require.i.require (/var/task/serverless_sdk/index.js:9:73131) at require (node:internal/modules/cjs/helpers:119:18) at Object. (/var/task/node_modules/@nestjs/core/injector/module-token-factory.js:7:31) at Module._compile (node:internal/modules/cjs/loader:1256:14) at Module._extensions..js (node:internal/modules/cjs/loader:1310:10) at Module.load (node:internal/modules/cjs/loader:1119:32) at Module._load (node:internal/modules/cjs/loader:960:12) at Module.require (node:internal/modules/cjs/loader:1143:19) at _require.i.require (/var/task/serverless_sdk/index.js:9:73397) { code: 'MODULE_NOT_FOUND', requireStack: [ '/var/task/node_modules/@nestjs/core/injector/module-token-factory.js', '/var/task/node_modules/@nestjs/core/injector/compiler.js', '/var/task/node_modules/@nestjs/core/injector/container.js', '/var/task/node_modules/@nestjs/core/injector/index.js', '/var/task/node_modules/@nestjs/core/index.js', '/var/task/dist/src/main.js', '/var/task/s_api.js', '/var/runtime/index.mjs' ] }
Thread Thread
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

I have uploaded my code to: github.com/arcovoltaico/nextjs

Thread Thread
 
thatkit profile image
Nikita

I remember a similar error in my setup. I had a module that wasn't compatible in this serverless build shema. I would try to build a serverless function with a minimum number of dependencies to figure out is it a general issue for all of your dependencies or a particular one.

At least, this is what I did with my codename.

Thread Thread
 
thatkit profile image
Nikita

Thanks for the git link. I've checked some of the configs, so a few notes:

  1. The handler path in serverless.yml should be dist/main.handler, not dist/src/main.handler. It's the path your nest app compiles things to.
  2. You need to replace the code in main.ts with the code in lamda.ts, since you're running a cachedServer in lamda.ts
Thread Thread
 
jordi_alhambra_0ff9e573b0 profile image
Jordi Alhambra

I think that was not the case. I compared this project with a plain Severless one that also works after being deployed, and I realised that the node-modules folder did contain all the child dependencies because I used npm.
So the culprit is pnpm, and it happens to NOT install them. I'm still not understanding how it can work locally. The fix is instead of running npmp install, choose pnpm install --shamefully-hoist instead.
Maybe not ideal, but the only option I know so far.

Thread Thread
 
thatkit profile image
Nikita

I'm glad your issue got resolved! Although, I'm quite sure with point 2 mentioned just above.

p.s. You could also try to switch to the basic npm as an ubiquitous standard and swap it with pnpm when there is a need for some optimization.

Happy serverless coding anyway!

Thread Thread
 
imflamboyant profile image
Marko Djakovic

Interesting and detailed discussion, thanks for your comments! I'm glad that you managed to resolve it in the end! I wasn't able to pitch in unfortunately.

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

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

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 • 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. 👍