Building a Serverless API with AWS CDK, Lambda, and RDS

Published: March 28, 2023Reading time: 5 min
Femi Adigun profile picture

Femi Adigun

Senior Software Engineer & Coach

Updated March 02, 2025

AWS CDK
Lambda Functions
RDS PostgreSQL

Prerequisites

  • Basic knowledge of serverless functions like AWS Lambda and API Gateway
  • Basic knowledge of CloudFormation stack
  • Basic knowledge of AWS CDK
  • Basic understanding of REST API
  • Basic understanding of RDS database and VPC
  • AWS CLI installed and configured with the correct credentials
  • Node and npm installed

The Architecture

AWS Architecture Diagram
  • API Gateway will expose REST Endpoints
  • API Gateway communicates with Lambda function
  • Lambda functions will connect to RDS database instance inside a VPC
  • Internet connection and private VPC connection

Instantiate The CDK

Let's make a directory for our project:

mkdir cdk-demo cd cdk-demo cdk init --language typescript

This command will generate some files and directories for us, the most important ones are bin and lib. We will be executing most of codes inside these two directories.

The bin directory contain a file <project-name>.ts while the lib directory contains a file <project-name>-stack.ts

The generated file in the bin directory will be used to synthesize your CDK application into a CloudFormation template, while the file in lib directory defines your CDK stack. All the AWS resources you want to provision will be defined here.

Our Plan

  1. Create a VPC or find default VPC
  2. Instantiate secrets manager
  3. Find Security group
  4. Find subnet group
  5. Find secret
  6. Create Database Instance
  7. Create APIGateway class
  8. Create Lambda class
  9. Create service class

Creating a Database Instance

Create a new file, I will call mine rds.ts in our lib directory. Create a function that accepts vpc and scope as argument, I will call mine rds.

export const rds = (vpc: IVpc, scope: Construct) => {
  
}

Define Security Group

Define our security group by lookup name. The default security group is okay for this demo; however, you can also create a new security group if you want. Attach the VPC to this security group.

export const rds = (vpc: IVpc, scope: Construct) => const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);

Define Subnet Group

Define our subnet group, use findLookupByName and pass the name of the default subnet group in our AWS account with other parameters.

export const rds = (vpc: IVpc, scope: Construct) => {
  const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
  const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
  
}

Database Credentials

Get the database credentials from secrets manager or create a new one. I already have my credentials stored and auto rotated by secrets manager.

export const rds = (vpc: IVpc, scope: Construct) => {
  const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
  const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
  const newSecret = new DatabaseSecret(scope,"my-secret", {
    username: "postgres",
    secretName: "dev/cdkdemo"
  });
  
}

Create Credentials

Cast the secret value to our database credential.

export const rds = (vpc: IVpc, scope: Construct) => {
  const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
  const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
  const newSecret = new DatabaseSecret(scope,"my-secret", {
    username: "postgres",
    secretName: "dev/cdkdemo"
  });
  const cred = Credentials.fromSecret(newSecret, "postgres");
  
}

Database Instance

Let's create our database instance and return all the necessary variables.

export const rds = (vpc: IVpc, scope: Construct) => {
  const dbSec = SecurityGroup.fromLookupByname(scope,"my-sec-rds","default",vpc);
  const dbSubNet = SubnetGroup.fromSubnetGroupName(scope, "my-sub-net","default-vpc-078eccc7aal3f");
  const newSecret = new DatabaseSecret(scope,"my-secret", {
    username: "postgres",
    secretName: "dev/cdkdemo"
  });
  const cred = Credentials.fromSecret(newSecret, "postgres");
  const newInstance = new DatabaseInstance(scope, "NewRdsInstance", {
    instanceIdentifier: "mydemo-withpg",
    databaseName: "postgres",
    engine: DatabaseInstanceEngine.POSTGRES,
    instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MICRO),
    storageEncrypted: true,
    vpc,
    allocatedStorage: 20,
    maxAllocatedStorage: 100,
    deletionProtection: false,
    removalPolicy: RemovalPolicy.DESTROY,
    storageType: StorageType.GP2,
    securityGroups: [dbSec],
    subnetGroup: dbSubNet,
    credentials: cred,
    multiAz: false,
  });
  const dbProxy = undefined;
  return { newInstance, dbSec, dbSubNet, dbProxy, vpc, newSecret };
}

Create API Gateway Class

This is a generic class that can be reused to create all our API Gateway instances. The constructor takes one parameter,scope of that Construct. Construct is a logical node in a CDK stack.

We will provide a name for our REST API, indicate cloudwatch role and other properties of the cloudwatch log.

We will also define a method called addIntegration() that takes 3 parameters: method, path, lambda.

export class ApiGateway extends RestApi {
  constructor(scope: Construct) {
    super(scope, "ApiGateway", {
      restApiName: "user",
      cloudWatchRole: true,
      deployOptions: {
        accessLogDestination: new LogGroupLogDestination(
          new LogGroup(scope, "ApiLogGroup", {
            logGroupName: "essl-customers",
            retention: RetentionDays.ONE_DAY,
            removalPolicy: RemovalPolicy.DESTROY,
          })
        ),
      },
    });
  }
  addIntegration(method: string, path: string, lambda: IFunction) {
    const resources = this.root.resourceForPath(path);
    resources.addMethod(method, new LambdaIntegration(lambda));
  }
}

Create Generic Lambda Class

This generic class will be used to create and deploy NodeJS lambda functions. It takes 3 parameters:scope (A construct object), filename (our NodeJS Lambda function code) and functionName.

The class constructor also specifies the architecture of our lambda functions, the runtime, entry point for our codes and log retention duration.

export class Lambda extends NodejsFunction {
  constructor(scope: Construct, fileName: string, fnName: string) {
    super(scope, fnName, {
      architecture: Architecture.ARM_64,
      runtime: Runtime.NODEJS_18_X,
      entry: path.join(__dirname, ../resources/{fileName}),
      logRetention: RetentionDays.ONE_DAY,
    });
  }
}

Next Steps

In the next part of this tutorial, we'll cover:

  • Creating the Service Files for CRUD operations
  • Implementing Lambda Functions
  • Connecting Lambda Functions to RDS
  • Testing our API
  • Deploying our CDK application

Note: Make sure to have the necessary imports at the top of each file. We will cover those in detail in the next part of this tutorial.

Author avatar

Femi Adigun

AWS Certified Solutions Architect

Related Topics:
AWS CDKLambdaAPI GatewayRDSServerless