Run a NestJS API with Postgres on AWS, from ECS Fargate, Lambda and Elastic Beanstalk by hand to provisioning the infrastructure from your TypeScript code.
NestJS gives a Node.js backend the structure that plain Express leaves to you, with modules, providers and dependency injection. Running it on a laptop is npm run start:dev, and most of the effort comes when the same app has to run on AWS with a database next to it.
A production setup needs an image registry, compute, a load balancer, RDS Postgres in private subnets, credentials in Secrets Manager and IAM roles to tie them together. This guide starts with provisioning that from your TypeScript code and then walks through ECS Fargate, Lambda and Elastic Beanstalk by hand.
What a NestJS app on AWS usually needs:
Encore combines the NestJS code you already have with the AWS infrastructure it needs, because the Postgres database and Pub/Sub topics are declared in TypeScript next to the Nest providers that use them. encore run starts Postgres and the topics locally with your services, and a deploy creates the same resources in your own AWS account.

The approach in the NestJS guide in the Encore docs keeps Nest for modules, providers and dependency injection and lets Encore handle the HTTP layer and the infrastructure. You add encore.dev to the project, and an encore.service.ts file at the root marks the directory as one service:
import { Service } from 'encore.dev/service';
// Encore will consider this directory and all its subdirectories as part of the nest service.
export default new Service('nest');
The database is declared with SQLDatabase, and Knex connects to it through the connection string Encore provides, so the existing Nest providers that inject the Knex instance stay the same:
import { SQLDatabase } from 'encore.dev/storage/sqldb';
import knex from 'knex';
// Define a database named 'appDB', using the database migrations
// in the "./migrations" folder. Encore automatically provisions,
// migrates, and connects to the database.
const DB = new SQLDatabase('appDB', {
migrations: './migrations',
});
// Use Knex.js to connect to the database
const ORM = knex({
client: 'pg',
connection: DB.connectionString,
});
export const databaseProviders = [
{
provide: 'DATABASE_CONNECTION',
useFactory: async () => ORM,
},
];
Nest then starts as a standalone application context instead of an HTTP server, which gives the Encore endpoints access to the same services that a Nest controller would have injected:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CatsService } from './cats/cats.service';
import { CatsModule } from './cats/cats.module';
const applicationContext: Promise<{ catsService: CatsService }> =
NestFactory.createApplicationContext(AppModule).then((app) => {
return {
catsService: app.select(CatsModule).get(CatsService, { strict: true }),
};
});
export default applicationContext;
The controller is where the change happens. Each route becomes an Encore api endpoint that calls the Nest service, and a Pub/Sub topic declared in the same file lets work such as search indexing happen after the request has returned:
import { api } from 'encore.dev/api';
import { Subscription, Topic } from 'encore.dev/pubsub';
import applicationContext from '../applicationContext';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';
// SNS and SQS on AWS, a local broker under encore run.
export const catCreated = new Topic<{ id: number; name: string }>('cat-created', {
deliveryGuarantee: 'at-least-once',
});
// Encore APIs replace the Nest controller, and the Nest service stays as it is.
export const create = api(
{ expose: true, method: 'POST', path: '/cats' },
async (dto: CreateCatDto): Promise<{ cat: Cat }> => {
const { catsService } = await applicationContext;
const cat = await catsService.create(dto);
await catCreated.publish({ id: cat.id, name: cat.name });
return { cat };
},
);
export const list = api(
{ expose: true, method: 'GET', path: '/cats' },
async (): Promise<{ cats: Cat[] }> => {
const { catsService } = await applicationContext;
return { cats: await catsService.findAll() };
},
);
// Delivery is at-least-once, so the handler is keyed on the cat id.
const _ = new Subscription(catCreated, 'index-cat', {
handler: async (event) => {
console.log('indexing cat', event.id, event.name);
},
});
Running encore run starts Postgres locally, applies the migrations, boots the Nest context and serves the endpoints, so the topic and subscriber run on your machine as well:
Connecting an AWS account in the Encore dashboard and pushing the code lets the next deploy create the VPC, load balancer, ECS service, RDS instance and SNS/SQS in that account, as described in the AWS infrastructure docs. Because the database and topic live in the application code, an AI coding agent that adds a new Nest module can declare the table migration and the topic in the same change and run the result under encore run before it reaches AWS.
encore build docker produces a standard Docker image if you want to run the containers yourself.Nest controllers, guards and interceptors are replaced by Encore endpoints and Encore's own auth handler, so the HTTP layer is a real code change, while modules, providers and services carry over. The NestJS vs Encore comparison covers where the two overlap, and the NestJS microservices guide shows how a larger app splits into services.
Run in your terminal to get started locally.
ECS on Fargate runs the Nest app as a container on capacity AWS manages. A multi-stage Dockerfile builds the app with dev dependencies and ships only dist and the production node_modules, running as the node user that the official image already includes:
FROM node:24-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
The image is pushed to ECR, and the task definition references the database URL in Secrets Manager rather than putting it in a plain environment variable, which ECS injects when the task starts:
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:nest/database-url"
}
]
Beyond the Dockerfile, the RDS instance, its subnet group and security group, the secret, the task execution role and its permission to read that secret, the log group and the target group are separate resources, and most teams describe them in CDK or Terraform and maintain a Compose file locally that approximates the same setup. The Node.js on AWS guide goes through those pieces in more detail.
@codegenie/serverless-express adapts the Express app that Nest runs on to the event format Lambda passes in. Version 5.0.0 is the current release, it targets the Node.js 24 Lambda runtime and it only supports promise-based handlers:
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@codegenie/serverless-express';
import type { Context, Handler } from 'aws-lambda';
import express from 'express';
import { AppModule } from './app.module';
let server: Handler | undefined;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
await app.init();
return serverlessExpress({ app: expressApp });
}
// Reuse the Nest app across invocations of a warm Lambda container.
export const handler = async (event: unknown, context: Context) => {
server ??= await bootstrap();
return server(event, context, () => {});
};
npm run start:dev.Nest builds its whole module graph on a cold start, so a larger app pays for that on the first request to each new container. A function can run for up to 15 minutes, but API Gateway has its own shorter integration timeout, and many concurrent functions opening Postgres connections usually calls for RDS Proxy, which is one more resource to define in SAM, CDK or Terraform.
Elastic Beanstalk has a managed Node.js platform that runs the app on EC2 instances behind a load balancer it creates. It uses a Procfile at the root if there is one and otherwise runs npm start, so a Nest app only needs a start script that runs the built dist/main.js.
Each environment is one application, so a queue worker or a second Nest service means another environment, and SQS, S3 and IAM around it are still set up separately. Custom configuration lives in .ebextensions and platform hooks, which are hard to test before a deploy. The Elastic Beanstalk alternatives guide covers where teams usually move from there.
The manual routes all leave RDS, Secrets Manager, IAM and networking to be described somewhere, which is usually where CDK or Terraform comes in, while Encore derives those resources from the code that uses them. A well-organized codebase moves more easily either way, and the NestJS project structure guide covers how to lay out modules. The quickstart walks through a first Encore app.
For one container, ECS on Fargate is the usual choice, since you push an image to ECR and run it as a service behind a load balancer without managing servers. When the app also needs Postgres, queues and secrets, Encore keeps your Nest modules and provisions the database, Pub/Sub and IAM in your own AWS account from the TypeScript code, so there is no Terraform or CDK to write for those resources.
Yes. The @codegenie/serverless-express package wraps the Express app that Nest runs on as a Lambda handler behind API Gateway or a function URL. Version 5 targets Node.js 24 and only supports promise-based handlers. Cold starts grow with the size of the Nest module graph, and many concurrent functions talking to Postgres usually need RDS Proxy.
Elastic Beanstalk has a managed Node.js platform that runs npm start or a Procfile on EC2 instances behind a load balancer, which suits a single app. ECS on Fargate runs a container image and gives you more control over services, task roles and scaling, and it is the better base once there is more than one service or a worker.
Store the connection string in AWS Secrets Manager and reference its ARN in the secrets section of the ECS task definition, which injects it as an environment variable when the task starts. The task execution role needs permission to read that secret. With Encore the RDS credentials are created and passed to the service for you.
Add encore.dev to the Nest project, put an encore.service.ts file at the root, start Nest as a standalone application context and expose your Nest services through Encore api endpoints. Declare the database with new SQLDatabase and connect Knex to its connectionString. encore run starts it all locally, and a deploy to a connected AWS account provisions ECS, RDS and SNS/SQS.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.