Modernizing Express, Heroku tech stack with NestJS and AWS - PoC
Tags: express, heroku, nestjs, aws
Published: 2023-03-31 08:56:02

# TL;DR

We will create a working PoC which proves that **migration from Express** based app **hosted in Heroku** **to NestJS** app **hosted in AWS** with **serverless architecture** based on **API Gateway**, **Fargate** and **Aurora** is not so complicated. We will also use the **IaC** approach based on **AWS CDK**.  

# MVP Solution 

When I joined [moojo](https://en.getmoojo.com/) as a Head of Engineering September 2022 - beta tests phase was almost done and we were preparing for the official launch of our **MVP (Minimum Viable Product)** with invoicing and instant payment features.

Backend was a **monolithic**, **Node.js** app using **Express** framework and **Postgress** database, both hosted on **Heroku**. Solution was using a **functional programming** approach, **TypeScript** and modern, next-generation ORM (Object Relational Mapper) like **Prisma**, which provided us with type-safety and automated migration.

It was simple approach following **KISS (Keep It Simple Stupid)** design principle, which was perfect at the time for the MVP solution. This was also a **conscious decision** the interim tech lead, who started the project and wanted to create unopinioned solution and leave **bigger decisions for the successor**.

## Challenges

This setup was quite good, but when the application started to grow - we noticed a few challenges.

### Framework

It was **hard to manage dependencies** without modern design patterns known as Dependency Injection, **OpenAPI/Swagger** documentation was often **out of sync** with actual implementation, freedom of doing things in unopinionated framework was not optimal for less experienced team members (they needed some **guidance and documentation**).

We could of course improve **existing solution**, but on the other hand - there are already frameworks, which provide those things **out-of-the-box**, so it would be like reinventing the wheel. **[NestJS](https://nestjs.com/)** seemed like a perfect fit for us, question was - how hard will it be to migrate.

### Cloud

**Heroku's** advantage is that it's super **easy to setup and deploy** your app. A few clicks in the Heroku Dashboard is enough to setup a compute or data store. Some disadvantage is **lack of flexibility** - it's always about **trade-offs**. Heroku is built on top of AWS, but it doesn't give you access to the full **richness of 200+ AWS services**. Configuration is sometimes limited e.g. Bucketeer Heroku add-on vs S3 (Simple Storage Service).

We potentially needed something **more advanced** (in the long term). **AWS (Amazon Web Services)**, which I used in other projects seemed like a natural migration step.

## PoC (Proof of Concept)

As every startup - we are busy with delivering new features e.g. [Insurance](https://en.getmoojo.com/blog/embedded-finance-fur-freelancer-berliner-start-up-moojo-launcht-versicherungsprodukte-mit-hiscox) for IT freelancers, so **rewriting everything was not an option**. I decided to create **PoC (Proof of Concept)** to check what is possible and answer some questions:

1. Is it possible to **implement new features using NestJS, but leave already implemented features in Express unchanged** in our monolith (but rewrite them one-by-one using [The Boy Scout Rule](https://www.informit.com/articles/article.aspx?p=1235624&seqNum=6) when we have time)? 
2. Is it easy to **Dockerize** our app, take advantage of modern approach such as **serverless**, **IaC (Infrastructure as Code)** in AWS?

### NestJS

#### Hello World!

I started with creating a **new Nest project**, by following [First steps](https://docs.nestjs.com/first-steps) in [NestJS Documentation](https://docs.nestjs.com/).

```
npm i -g @nestjs/cli
nest new moojo
```

This created the working "Hello World!" NestJS app:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgz7m0tzuqms20sf7h8m.png)

Source code: [NestJS new project | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/ee2822814438c89cbe064f4654fe03a40c36b109)

#### Express app in NestJS

Next thing I wanted to verify is **using the existing Express app in NestJS**. I found a solution in the Stack Overflow question - [Adding NestJS as express module results in Nest being restarted](https://stackoverflow.com/a/68943920).  

All you have to do is to modify `bootstrap` function in `main.ts` and use **`ExpressAdapter`**:

```ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const adapter = new ExpressAdapter(expressApp);
  const app = await NestFactory.create(AppModule, adapter);

  await app.listen(3000);
}
```

At this point I had **two working endpoints**, one for **NestJS** and one from **Express**:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oce7i9ckwezq323fs47t.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h08s79xr12xd47uyejgy.png)

Source code: [Express app in NestJS | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/58c8342a91fcb46fe1e8bede54b60c4e9d5f7264)

#### Prisma

I wanted to create some **more realistic example for the migration to AWS**, so I decided to add a **Postgres** database and **Prisma** (ORM we already use at moojo).

There is a great example for that (with users and blog posts) in NestJS documentation - [Prisma | NestJS](https://docs.nestjs.com/recipes/prisma), so just follow the recipe.

The only thing **missing in the recipe** steps was updating `app.module.ts` with a newly created services

```ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostService } from './post.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
  providers: [AppService, UserService, PostService, PrismaService],
})
export class AppModule {}
```
Source code: [Prisma recipe | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/dcd925ae6c8aff26d4ac1492d751dbb4119647a5)

#### OpenAPI (Swagger)

In order to easily **test this solution** I decided to setup **OpenAPI/Swagger documentation**.

Everything you need to know is again in NestJS documentation - [OpenAPI (Swagger) | NestJS](https://docs.nestjs.com/openapi/introduction), so you can take a look.

You just have to initialize Swagger using **`SwaggerModule`** in `main.ts` and add use **`@ApiProperty`** [decorators](https://docs.nestjs.com/openapi/types-and-parameters) in a few places.

At this point OpenAPI / **Swagger UI** was working

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bvww2yx9y17qh8f4ghlw.png)

and I was able to **create user, post and publish it**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kybxtkhuqpykvek0fvo0.png)

Source code: [OpenAPI/Swagger | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/68a71b3abbb297cc796a41026689d24126934884)

### AWS

#### Architecture

It would be great to start **migration to AWS without bigger refactoring** of our code. Later we can gravitate to more cloud-native solution, but **lift-and-shift** is a great first step.

I really like the serverless-first approach and used it a lot in my previous projects. With lift-and-shift approach - possibilities are limited, but we still can leverage benefits of **serverless computing** such as no servers to manage, automatic scaling, built-in high availability or pay-for-use billing which can be especially beneficial for staging and developer environments.

All of this can be achieved with the following high level architecture with **API Gateway**, **Fargate** and **Aurora Serverless**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smd51jf5f0fhxho4c663.png)

In order to integrate API Gateway with Fargate in a secure and reliable way - we also need **ALB (Application Load Balancer)** and **VPC Link**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n0uhahoz4g1m7tod63ht.png)

Fargate and Aurora will run within a private subnet... actually two **private subnets** in two different **Availability Zones** to achieve high availability.

To enable Fragate tasks to download Docker image from **ECR (Elastic Container Registry)**, we will have to deploy two **NAT Gatways** in a **public subnets**.

**Security groups** can be used to control the traffic that is allowed between all resources.

You can find out more about this architecture on AWS blog e.g. [Access Private applications on AWS Fargate using Amazon API Gateway PrivateLink]
(https://aws.amazon.com/blogs/compute/access-private-applications-on-aws-fargate-using-amazon-api-gateway-privatelink/)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ndht7omkr12j3fxxrd5.png)

#### Infrastructure as Code

There are many benefits of **IaC (Infrastructure as Code)**, including speed of provisioning new environments, lower risk of human errors, improved consistency or a way to version your infrastructure.

There are also many great solutions which can help with that - Terraform, Pulumi, CloudFormation, **AWS CDK (Cloud Development Kit)** - just to name a few. For me - using the same programming language e.g. **TypeScript** for all aspects of the technology stack is a powerful concept. I also like to use native technologies in the given environment and I already have experience with AWS CDK, so I decided to go with it.

#### AWS CDK app

Before we create a new AWS CDK app, we need to install and setup a few things. There is a good tutorial [Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) in the official [AWS CDK v2 Developer Guide](https://docs.aws.amazon.com/cdk/v2/guide/).

When all prerequisites done and AWS CDK installed

```
npm install -g aws-cdk
```

we can finally create our [First AWS CDK app](https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html) in `moojo` folder, which we rename later to `cloud`.

```
cdk init app --language typescript
```

Source code: [AWS CDK app | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/0ad3484aad8a7f4f3f6778af06c7d6b59b3ee8e7)

#### VPC (Virtual Private Cloud)

Let's start building our infrastructure with VPC (Virtual Private Cloud) with **private** and **public subnet** configurations, **Availability Zones** and **NAT Gateways**.

vpc.cdk.ts
```ts
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";

export class VpcCdkConstruct extends Construct {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.vpc = new ec2.Vpc(this, "moojo-vpc", {
      vpcName: "moojo-vpc",
      cidr: "10.0.0.0/16",
      maxAzs: 2,
      natGateways: 2,
      natGatewayProvider: ec2.NatProvider.gateway(),
      subnetConfiguration: [
        {
          name: "moojo-public-subnet",
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24,
        },
        {
          name: "moojo-private-subnet",
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
      ],
    });
  }
}
```

moojo-stack.ts
```ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import { VpcCdkConstruct } from "./vpc.cdk";

export class MoojoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    cdk.Tags.of(this).add("app", "Moojo");

    const vpc = new VpcCdkConstruct(this, "vpc").vpc;
  }
}
```

After those changes we can deploy our app

```
cdk deploy
```

and watch our stack being created in CloudFormation

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i9ud7ggnldko8j6ycqln.png)

You can also verify if VPC was created in a VPC dashboard

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cjxnamweuuko3j2gxveb.png)

Source code: [VPC | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/1d60fdcb74074178bd287bf9a370908423ac4944)

#### RDS (Relational Database Service)

Let's create **Aurora Serverless** (part of RDS) next. We will create a **Serverless Cluster** with **Aurora PostgreSQL** engine in our **VPC**. We will setup things like **removal policy** to destroy to easily cleanup after we are done with PoC, **scaling configuration** to some bare minimum to save some costs. We also need to enable the **Data API** to be able to use **Query Editor** in AWS Console.

As you can see - some of the configuration makes sense only for PoC and it's **not production ready**. Point of PoC is to check and verify possible solutions, answer some questions NOT become production ready solution.

rds.cdk.ts
```ts
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as rds from "aws-cdk-lib/aws-rds";

interface RdsProps {
  vpc: ec2.Vpc;
}

export class RdsCdkConstruct extends Construct {
  public readonly cluster: rds.ServerlessCluster;

  constructor(scope: Construct, id: string, { vpc }: RdsProps) {
    super(scope, id);

    const securityGroup = new ec2.SecurityGroup(this, "moojo-rds-sg", {
      vpc,
      securityGroupName: "moojo-rds-sg",
      description: "Security group for Moojo RDS (Amazon Relational Databases)",
    });

    this.cluster = new rds.ServerlessCluster(this, "moojo-dbcluster", {
      clusterIdentifier: "moojo-dbcluster",
      defaultDatabaseName: "moojo",
      engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL,
      parameterGroup: new rds.ParameterGroup(this, "ParameterGroup", {
        engine: rds.DatabaseClusterEngine.auroraPostgres({
          version: rds.AuroraPostgresEngineVersion.VER_10_21, // TODO: adjust supported version
        }),
      }),
      enableDataApi: true, // TODO: needed for Query Editor (AWS console)
      removalPolicy: cdk.RemovalPolicy.DESTROY, // TODO: only for PoC
      vpc,
      scaling: {
        // TODO: adjust it
        autoPause: cdk.Duration.hours(1),
        minCapacity: rds.AuroraCapacityUnit.ACU_2,
        maxCapacity: rds.AuroraCapacityUnit.ACU_2,
      },
      securityGroups: [securityGroup],
    });
  }
}
```
moojo-stack.ts
```ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import { RdsCdkConstruct } from "./rds.cdk";
import { VpcCdkConstruct } from "./vpc.cdk";

export class MoojoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    cdk.Tags.of(this).add("app", "Moojo");

    const vpc = new VpcCdkConstruct(this, "vpc").vpc;
    const rds = new RdsCdkConstruct(this, "rds", { vpc });
  }
}
```

After deploying our stack again, we can check if our database works. Let's go to **Query Editor** and **connect to our database**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbo1lm8hutdmqgjxv3vt.png)

You copy **Secrets manger ARN** in **Secrets Manager**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkcyf1y0ea6emp3ti2gb.png)

When connected - we can **copy SQL** from the `migration.sql` file **generated by Prisma**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/190hr3xqdibweu1nmdyj.png)

and execute it to create our tables.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmi80uc65m4gd7jy2h5h.png)

Source code: [RDS | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/88a47b73eda7fd00ef47e24b21ced5eeb9932070)

#### ELB (Elastic Load Balancer)

As a next step - let's create **ALB (Application Load Balancer)** with a **target group**, **listener** and **security group**.

elb.cdk.ts
```ts
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";

interface ElbProps {
  vpc: ec2.Vpc;
}

export class ElbCdkConstruct extends Construct {
  public readonly alb: elbv2.ApplicationLoadBalancer;
  public readonly listener: elbv2.ApplicationListener;
  public readonly securityGroup: ec2.SecurityGroup;
  public readonly targetGroup: elbv2.ApplicationTargetGroup;

  constructor(scope: Construct, id: string, { vpc }: ElbProps) {
    super(scope, id);

    this.securityGroup = new ec2.SecurityGroup(this, "moojo-elb-sg", {
      vpc,
      securityGroupName: "moojo-elb-sg",
      description: "Security group for Moojo ELB (Elastic Load Balancer)",
    });

    this.securityGroup.addIngressRule(
      ec2.Peer.anyIpv4(), // TODO: restrict?
      ec2.Port.tcp(80),
      "Allow from anyone on port 80"
    );

    this.alb = new elbv2.ApplicationLoadBalancer(this, "moojo-alb", {
      loadBalancerName: "moojo-alb",
      vpc,
      securityGroup: this.securityGroup,
      deletionProtection: false,
    });

    this.targetGroup = new elbv2.ApplicationTargetGroup(this, "moojo-alb-tg", {
      vpc,
      targetType: elbv2.TargetType.IP,
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      targetGroupName: "moojo-alb-tg",
    });

    this.listener = new elbv2.ApplicationListener(this, "moojo-alb-listener", {
      loadBalancer: this.alb,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultAction: elbv2.ListenerAction.forward([this.targetGroup]),
    });
  }
}
```

moojo-stack.ts
```ts
import { RdsCdkConstruct } from "./rds.cdk";
import { VpcCdkConstruct } from "./vpc.cdk";
import { ElbCdkConstruct } from "./elb.cdk";

export class MoojoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    cdk.Tags.of(this).add("app", "Moojo");

    const vpc = new VpcCdkConstruct(this, "vpc").vpc;
    const rds = new RdsCdkConstruct(this, "rds", { vpc });
    const elb = new ElbCdkConstruct(this, "elb", { vpc });
  }
}
```

Source code: [ELB | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/88b0172971677c85de5ff3b14622de0e28f111fb)

#### API Gateway

Let's create **API Gateway** (v2) with **HTTP API** and integrate it with our ALB (Application Load Balancer) using **VPC Link**.

gw.cdk.ts
```ts
import { Construct } from "constructs";
import * as apigwv2 from "@aws-cdk/aws-apigatewayv2-alpha";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as integrations from "@aws-cdk/aws-apigatewayv2-integrations-alpha";

import { ElbCdkConstruct } from "./elb.cdk";

interface ApiGatewayProps {
  vpc: ec2.Vpc;
  elb: ElbCdkConstruct;
}

export class ApiGatewayCdkConstruct extends Construct {
  constructor(scope: Construct, id: string, { vpc, elb }: ApiGatewayProps) {
    super(scope, id);

    const httpApi = new apigwv2.HttpApi(this, "moojo-http-api", {
      apiName: "moojo-http-api",
    });

    const vpcLink = new apigwv2.VpcLink(this, "moojo-vpc-link", {
      vpc,
      vpcLinkName: "moojo-vpc-link",
    });

    const integration = new integrations.HttpAlbIntegration(
      "moojo-http-alb-integration",
      elb.listener,
      {
        method: apigwv2.HttpMethod.ANY, // TODO: restrict?
        vpcLink,
      }
    );

    const route = new apigwv2.HttpRoute(this, "moojo-http-route", {
      httpApi,
      routeKey: apigwv2.HttpRouteKey.with("/{proxy+}", apigwv2.HttpMethod.ANY), // TODO: restrict?
      integration,
    });
  }
}
```

moojo-stack.ts
```ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

import { ApiGatewayCdkConstruct } from "./gw.cdk";
import { ElbCdkConstruct } from "./elb.cdk";
import { RdsCdkConstruct } from "./rds.cdk";
import { VpcCdkConstruct } from "./vpc.cdk";
import { ElbCdkConstruct } from "./elb.cdk";

export class MoojoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    cdk.Tags.of(this).add("app", "Moojo");

    const vpc = new VpcCdkConstruct(this, "vpc").vpc;
    const rds = new RdsCdkConstruct(this, "rds", { vpc });
    const elb = new ElbCdkConstruct(this, "elb", { vpc });
    const gw = new ApiGatewayCdkConstruct(this, "gw", { vpc, elb });
  }
}
```

Source code: [GW | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/5a78e36be7a6132f06172c1de9e85e0048e94d0c)

#### ECS (Elastic Container Service)

It's time for the last part - the heart of our backend. We will create **ECS (Elastic Container Service) Cluster**, **Fargate** task definition and service. I will show you how I **Dockerized** our app and pushed the **docker image** to **ECR (Elastic Container Registry)**.

**Dockerization** is super simple. All you have to do is to grab `Dockerfile` from Tom Ray's article [How to write a NestJS Dockerfile optimized for production](https://www.tomray.dev/nestjs-docker-production) and save it  in our NestJS `moojo` folder.

**Image** will be **created and pushed to ECR** with this simple code

```ts
const image = new DockerImageAsset(this, "moojo-ecr-image", {
  directory: path.join(__dirname, "../../", "moojo"),
  platform: Platform.LINUX_AMD64,
});
```

Keep in mind that this **ECR configuration** is for **PoC only**, it has to be adjusted for the production workload. There is also a security issue - I get the database URL and pass it as an environment variable in an unsafe way (this will have to be fixed in a production ready solution, but it's good enough for the PoC).

ecs.cdk.ts
```ts
import { Construct } from "constructs";
import { DockerImageAsset, Platform } from "aws-cdk-lib/aws-ecr-assets";
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as sm from "aws-cdk-lib/aws-secretsmanager";
import * as path from "path";

import { ElbCdkConstruct } from "./elb.cdk";
import { RdsCdkConstruct } from "./rds.cdk";

interface EcsProps {
  vpc: ec2.Vpc;
  elb: ElbCdkConstruct;
  rds: RdsCdkConstruct;
}

export class EcsCdkConstruct extends Construct {
  constructor(scope: Construct, id: string, { vpc, elb, rds }: EcsProps) {
    super(scope, id);

    const cluster = new ecs.Cluster(this, "moojo-ecs-cluster", {
      clusterName: "moojo-ecs-cluster",
      vpc,
    });

    const securityGroup = new ec2.SecurityGroup(this, "moojo-ecs-sg", {
      vpc,
      securityGroupName: "moojo-ecs-sg",
      description: "Security group for Moojo ECS (Elastic Container Service)",
    });

    const taskDefinition = new ecs.FargateTaskDefinition(
      this,
      "moojo-ecs-task-definition",
      {
        // TODO: adjust it
        memoryLimitMiB: 1024,
        cpu: 512,
      }
    );

    const image = new DockerImageAsset(this, "moojo-ecr-image", {
      directory: path.join(__dirname, "../../", "moojo"),
      platform: Platform.LINUX_AMD64,
    });

    const logging = new ecs.AwsLogDriver({
      streamPrefix: "moojo",
    });

    taskDefinition.addContainer("moojo-ecs-container", {
      containerName: "moojo-ecs-container",
      logging,
      image: ecs.ContainerImage.fromDockerImageAsset(image),
      portMappings: [
        {
          containerPort: 3000,
          protocol: ecs.Protocol.TCP,
        },
      ],
      environment: {
        NODE_ENV: "production",
        DATABASE_URL: this.getDatabaseUrlUnsafe(rds.cluster.secret!), // TODO: do it in a secure way
      },
    });

    const service = new ecs.FargateService(this, `${id}`, {
      serviceName: "moojo-ecs-service",
      taskDefinition,
      securityGroups: [securityGroup],
      cluster,
      // TODO: adjust it
      desiredCount: 1,
      enableExecuteCommand: true,
      maxHealthyPercent: 200,
      minHealthyPercent: 100,
    });

    // TODO: adjust it
    const scaling = service.autoScaleTaskCount({
      minCapacity: 1,
      maxCapacity: 2,
    });

    // TODO: adjust it
    scaling.scaleOnCpuUtilization(`${id}-cpuscaling`, {
      targetUtilizationPercent: 85,
      scaleInCooldown: cdk.Duration.seconds(120),
      scaleOutCooldown: cdk.Duration.seconds(30),
    });

    rds.cluster.connections.allowDefaultPortFrom(
      service,
      "Fargate access to Aurora"
    );

    service.connections.allowFrom(
      elb.alb,
      ec2.Port.tcp(80),
      "Allow traffic from ELB"
    );

    elb.listener.addTargets("moojo-ecs-targets", {
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [service],
      healthCheck: {
        // TODO: adjust it
        healthyHttpCodes: "200",
        healthyThresholdCount: 3,
        interval: cdk.Duration.seconds(30),
      },
    });
  }

  // TODO: do it in a secure way!!!
  private getDatabaseUrlUnsafe(secret: sm.ISecret): string {
    const host = secret.secretValueFromJson("host").unsafeUnwrap();
    const port = secret.secretValueFromJson("port").unsafeUnwrap();
    const username = secret.secretValueFromJson("username").unsafeUnwrap();
    const password = secret.secretValueFromJson("password").unsafeUnwrap();
    const database = secret.secretValueFromJson("dbname").unsafeUnwrap();

    return `postgresql://${username}:${password}@${host}:${port}/${database}`;
  }
}
```

moojo-stack.ts
```ts
import { Construct } from "constructs";

import { ApiGatewayCdkConstruct } from "./gw.cdk";
import { EcsCdkConstruct } from "./ecs.cdk";
import { ElbCdkConstruct } from "./elb.cdk";
import { RdsCdkConstruct } from "./rds.cdk";
import { VpcCdkConstruct } from "./vpc.cdk";
export class MoojoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    cdk.Tags.of(this).add("app", "Moojo");

    const vpc = new VpcCdkConstruct(this, "vpc").vpc;
    const rds = new RdsCdkConstruct(this, "rds", { vpc });
    const elb = new ElbCdkConstruct(this, "elb", { vpc });
    const gw = new ApiGatewayCdkConstruct(this, "gw", { vpc, elb });
    const ecs = new EcsCdkConstruct(this, "ecs", { vpc, elb, rds });
  }
}
```

When deployed - you can grab API Gateway URL from the AWS Console


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p63y8lp3a884y7p5bfpp.png)

and give it a try using Swagger UI.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mkskze72zbjt3g3moz12.png)

As you can see everything worked like in our local environment.

Source code: [ECS | GitHub](https://github.com/JacekKosciesza/moojo-nestjs-aws-poc/commit/6c65cd7b1176c429bcfb7815f08a33c2acedcf7e)