Deploying an AWS Lambda with Nest.js via Serverless

ยท

4 min read

Deploying an AWS Lambda with Nest.js via Serverless

For this practice, we will launch a basic app into an AWS Lambda function. The idea is to leverage the nest.js router instead of manually creating each endpoint/method for the Lambda definition. The main components are as follows:

Step 1: Create and set up a Nest.js app with Lambda config

By running the following command you will create the app, or you could an existing one.

nest new my-awesome-project

The next step is to generate the CRUD endpoints in src/app.controller.ts so you can test the verbs out.

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('/')
  getHelloApi(): string {
    return 'Hello from Lambda';
  }

  @Post('/post')
  async post(@Body() body: any) {
    return body;
  }

  @Put('/put')
  async put(@Body() body: any) {
    return body;
  }

  @Delete('/delete')
  async delete() {
    return 'deleted resource!';
  }
}

Unlike the default Nest src/main.ts file, the Serverless framework requires you to build a main handler differently. For Lambda to load and run the app, you need to create a file like this: src/lambda.ts.

// lambda.ts
import { Handler, Context } from 'aws-lambda';
import { Server } from 'http';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const express = require('express');

// NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely
// due to a compressed response (e.g. gzip) which has not been handled correctly
// by aws-serverless-express and/or API Gateway. Add the necessary MIME types to
// binaryMimeTypes below
const binaryMimeTypes: string[] = [];

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  if (!cachedServer) {
    const expressApp = express();
    const nestApp = await NestFactory.create(
      AppModule,
      new ExpressAdapter(expressApp),
    );
    nestApp.use(eventContext());
    nestApp.setGlobalPrefix('api');
    await nestApp.init();
    cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
  }
  return cachedServer;
}

export const handler: Handler = async (event: any, context: Context) => {
  cachedServer = await bootstrapServer();
  return proxy(cachedServer, event, context, 'PROMISE').promise;
};

And of course, install the required dependencies:

npm i @nestjs/platform-express
npm i aws-lambda
npm i aws-serverless-express

Step 2: Serverless definition

When working with Serverless framework we need to make yml definition files for deployments. The same definition can run locally for testing purposes.

Install serverless npm package if needed

npm i serverless -g

You might need to login, do it with this command

serverless login

Once logged in all the credentials will setup in your host

Now that serverless is ready, we create our definition file serverless.yml in app's root:

service: my-service

plugins:
  - serverless-plugin-optimize
  - serverless-offline
  - serverless-jetpack

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  memorySize: 2048
  timeout: 20
  stage: "dev" #prefix "dev" if undefined
  environment:
    MY_ENV_VAR: 'MY VALUE'

functions:
  main: # The name of the lambda function
    # The module 'handler' is exported in the file 'src/lambda'
    handler: dist/lambda.handler
    description: Nestjs backend
    events:
      - http:
          path: /api/{proxy+}
          method: any
          cors: true

The important stuff here are the following attributes:

  • events.http: this refers to the REST capacity for the API. The event "httpApi" has different capacities. AWS Official Docs comparison here.

  • events.http.path: "/api" prefix is placed, then "{proxy+}" definition will allow us to route requests using Nest app router instead of function's events paths

  • The provider section contains the technical aspect of the "server". Official docs here.

There are some dev dependencies we need that works as plugins. Two of them for optimization and compression and one for offline testing.

  npm i -D serverless-plugin-optimize
  npm i -D serverless-offline
  npm i -D serverless-jetpack

Step 3: Offline (local) start

After all these steps you are ready to run your app. I added the next line for simplicity to package.json

"local": "npm run build && sls offline start"

After run it you'll face a response like this:

Now you can visit the local url: http://localhost:3000/dev/api/ (prefix dev and api) are set in serverless.yml file and api is a global prefix set in our nest.js app. These prefixes can be modified.

Step 4: Deploy function to AWS Lambda

Before you can deploy the function, you must set up AWS credentials with the minimum required permissions for Lambda, API Gateway, and CloudFormation. If you haven't done this yet, you can follow this guide.

Finally, we can proceed with the deployment. You can run the following commands together or one by one.

"deploy": "npm run build && sls deploy"

Up to this point you should be able to get an endpoint from API Gateway using the specified stage like the following:

Testing time

Now you can hit your methods and verbs by using the provided base endpoint; in case you follow the template the urls would be:

GET: https://<gateway_api_id>.execute-api.us-east-1.amazonaws.com/dev/api/

POST: https://<gateway_api_id>.execute-api.us-east-1.amazonaws.com/dev/api/post

PUT: https://<gateway_api_id>.execute-api.us-east-1.amazonaws.com/dev/api/put

DELETE: https://<gateway_api_id>.execute-api.us-east-1.amazonaws.com/dev/api/delete

You've done it! You now have a basic tinker app. It's up to you to keep exploring Nest features like DTO implementation, validation, and more. In the next article, we'll cover authentication and other key features when working with Lambdas.