Skip to content
NestJS ns deployment 4 min read

Serverless Deployment

NestJS is built on top of Node HTTP frameworks, which makes it a natural fit for serverless runtimes like AWS Lambda, Azure Functions, or Google Cloud Functions. The trick is to stop listening on a port and instead translate each cloud event into a request the Nest application can answer. The two challenges that dominate serverless deployments are wrapping the app correctly and keeping cold starts short, and this page walks through both.

How serverless changes the runtime model

In a traditional deployment you call app.listen(3000) and the process stays alive, handling many requests over a long-lived socket. On Lambda there is no port: the platform invokes an exported handler function once per request (or per batch of records), passing an event and a context. Your job is to bridge that event-driven contract to Nest’s request/response pipeline.

The standard bridge is serverless-http, which converts an API Gateway / Lambda event into a Node-compatible request and serializes the response back into the shape the platform expects.

Never call app.listen() in a Lambda handler. Listening on a port keeps the invocation hanging until it times out. Use app.init() instead so the DI container, middleware, and lifecycle hooks are wired without opening a socket.

Wrapping the app with serverless-http

Install the adapter alongside the Express types Nest already uses:

npm install serverless-http
npm install -D @types/aws-lambda

The key idea is to bootstrap Nest once and cache the resulting handler in a module-level variable. Because Lambda reuses a warm execution context across invocations, this cache means only the first request pays the bootstrap cost.

// src/lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from 'serverless-http';
import express from 'express';
import type { Handler, APIGatewayProxyEvent, Context } from 'aws-lambda';
import { AppModule } from './app.module';

let cachedHandler: Handler;

async function bootstrap(): Promise<Handler> {
  const expressApp = express();
  const app = await NestFactory.create(
    AppModule,
    new ExpressAdapter(expressApp),
    { logger: ['error', 'warn'] },
  );

  app.enableCors();
  // Apply global pipes/filters here exactly as you would in main.ts.
  await app.init(); // NOT app.listen()

  return serverlessExpress(expressApp);
}

export const handler: Handler = async (
  event: APIGatewayProxyEvent,
  context: Context,
) => {
  context.callbackWaitsForEmptyEventLoop = false;
  cachedHandler = cachedHandler ?? (await bootstrap());
  return cachedHandler(event, context);
};

Setting callbackWaitsForEmptyEventLoop = false lets the function return as soon as the response is ready, even if open database pools or timers remain in the loop — without it, Lambda waits and you pay for idle billing time.

Deploying with the Serverless Framework

A minimal serverless.yml points the platform at the compiled handler and routes every path through a single proxy integration:

service: nestjs-api

provider:
  name: aws
  runtime: nodejs20.x
  memorySize: 1024
  timeout: 15

functions:
  api:
    handler: dist/lambda.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: any

Build and deploy:

npm run build
npx serverless deploy

Output:

Deploying nestjs-api to stage dev (us-east-1)

✔ Service deployed to stack nestjs-api-dev (74s)

endpoint: ANY - https://abc123.execute-api.us-east-1.amazonaws.com/{proxy+}
functions:
  api: nestjs-api-dev-api (3.1 MB)

Taming cold starts

A cold start is the time spent initializing a fresh execution environment: loading the bundle, parsing modules, and running your Nest bootstrap. The levers that matter most:

TechniqueEffectTrade-off
Cache the handler at module scopeSkips bootstrap on warm invocationsNone — always do this
Bundle and tree-shake (esbuild/webpack)Less code to load and parseBuild step complexity
Increase memorySizeMore CPU, faster initHigher per-ms cost
Lazy-load heavy modulesSmaller initial graphPay cost on first use
Provisioned concurrencyEliminates cold startsFixed monthly cost

Bundling has the biggest payoff. The default Nest build ships thousands of files; bundling collapses them into one. With the Nest CLI and the SWC/webpack option:

npm install -D @nestjs/cli
// nest-cli.json
{
  "compilerOptions": {
    "webpack": true,
    "webpackConfigPath": "webpack.config.js"
  }
}

Avoid pulling in modules whose initialization is expensive (large config validators, ORM metadata scanning) at the top of AppModule if only a few routes need them. Defer them with LazyModuleLoader so the cold start does not pay for code a given request never touches.

Reusing connections across invocations

Database and HTTP clients should be created once and reused. Because the cached handler keeps the Nest container alive, a connection pool opened during app.init() persists across warm invocations automatically — provided you do not tear it down. Do not register a graceful-shutdown hook that closes pools on every request; let the platform recycle the environment instead.

// Keep the pool small — Lambda runs one request per container at a time.
TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  extra: { max: 2 },
});

Best Practices

  • Bootstrap once and cache the handler at module scope so warm invocations skip initialization entirely.
  • Call app.init(), never app.listen(), inside the Lambda handler.
  • Set context.callbackWaitsForEmptyEventLoop = false to avoid paying for idle event-loop time.
  • Bundle the app with webpack or esbuild to shrink the package and slash parse time.
  • Keep connection pools tiny (1-2 connections) since each container serves one concurrent request.
  • Use provisioned concurrency for latency-sensitive endpoints where cold starts are unacceptable.
  • Trim the logger and disable unnecessary CORS/Swagger setup in the serverless path to keep bootstrap lean.
Last updated June 14, 2026
Was this helpful?