Create a REST API with AWS Lambda and API Gateway using AWS CDK

Photo by David Clode / Unsplash
Photo by David Clode / Unsplash

Building APIs following the REST standard is expected, which provides many benefits for developers and consumers. Defining the architecture that will host the application is also crucial to ensure availability and reliability.

Cloud providers such as AWS provides many services for building backend applications without any hassle, and one common usage is the combination of the AWS API Gateway and Lambda to build serverless API.

In this post, we will see how to build a RESTful API on AWS using these two services, and we will use the AWS CDK to define the infrastructure as code.

The use case

Ghast is a blogging platform that grew hyper-fast, and its users need access to their blog post data outside of the platform to build custom integrations with other third-party services.

To fulfill the request of the users, we must build a public API that exposes endpoints to interact with the system, such as creating, updating, or retrieving blog posts. To have a highly available and scalable system, we want to build it on a Serverless architecture on AWS.

Entities Schema

The database to use is MongoDB, so we are talking more about collections and documents. This is a simplified database diagram of the Ghast system with only the user and post collections.

Database schema of the system.
Database schema of the system.

The API endpoints

The table below summarizes the list of endpoints the API will expose

API Endpoint Description
[POST] /posts Create a new blog post
[PUT] /posts/{id} Update a post (title, content, status, tags, isFeatured)
[DELETE] /posts/{id} Delete a post
[GET] /posts Search posts (by user, status, keyword)
[GET] /posts/{id} Retrieve a post

Architecture diagram

The system will have one Lambda function per route and be served through an API Gateway. The Lambda function will connect to a MongoDB database to write and read the data. The picture below shows the architecture of the Ghast API system.

The architecture diagram of the system.
The architecture diagram of the system.

We will use the AWS CDK to define and deploy the system infrastructure on AWS.

Prerequisites

You need the following tools to complete this tutorial

  • An AWS account, a free tier is enough
  • AWS CLI configured (check out this link to see how to do it)
  • A free MongoDB cluster on MongoDB Altas
  • Node.js 18+ - download link
  • Docker for building the Lambda function code, optional if you test on AWS directly.

Install the AWS CDK v2 and the AWS SAM CLI

We will use the AWS CDK to:

  • Generate a CDK project with the required dependencies
  • Imperatively write the infrastructure stack using TypeScript
  • Generate the CloudFormation template from the infrastructure code
  • Deploy the CloudFormation template on AWS.

Run the command below to install and verify if the installation succeeded by checking the version.


npm install -g aws-cdk
cdk --version

The AWS SAM CLI is needed to invoke the Lambda function locally or simulate an instance of the API Gateway. Follow the AWS documentation guide to install it on your computer.

You can skip it if you will deploy the infrastructure on AWS using the AWS CDK and test it there.

Set up the project using the AWS CDK

Create a folder that will hold the source code of our project.


mkdir ghast-api
cd ghast-api

We will write the infrastructure in TypeScript; let's initialize a new project with the TypeScript template by running the command below:


cdk init app --language typescript

This command creates the required files and folders and installs the node modules. The folder structure looks like the one in this picture.

Project structure generated by the AWS CDK CLI.
Project structure generated by the AWS CDK CLI.

At the project root folder, create a folder src that will hold all the application logic.

Define the database schema

We will use Mongoose to define the MongoDB schema of the User and Post. Run the command below to install it:


npm install mongoose

Create folder src/models, then create a file named user.model.ts and the code below:


import mongoose, { Document, Schema } from 'mongoose';

type UserDocument = Document & {
  fullName: string;
  email: string;
  password: string;
  birthDate: Date;
};

type UserInput = {
  fullName: UserDocument['fullName'];
  email: UserDocument['email'];
  password: UserDocument['password'];
  birthDate: UserDocument['birthDate'];
};

const userSchema = new Schema({
  fullName: {
    type: Schema.Types.String,
    required: true,
    unique: true,
  },
  email: {
    type: Schema.Types.String,
    required: true,
    unique: true,
  },
  password: {
    type: Schema.Types.String,
    required: true,
  },
  birthDate: {
    type: Schema.Types.Date,
    required: true,
  }
}, {
  collection: "users",
  timestamps: true,
});

const User = mongoose.model<UserDocument>('User', userSchema);

export { User, UserDocument, UserInput };

Create a file src/models/post.model.ts and add the code below:


import mongoose, { Document, Schema } from 'mongoose';

enum PostStatusEnum {
  draft = 'draft',
  published = 'published',
  archived = 'archived',
}

type PostDocument = Document & {
  title: string;
  slug: string;
  content: string;
  tags: string[];
  status: PostStatusEnum;
  viewCount: number;
  isFeatured: boolean;
  author: string;
};

type PostInput = {
  title: PostDocument['title'];
  slug: PostDocument['slug'];
  content: PostDocument['content'];
  tags: PostDocument['tags'];
  status: PostDocument['status'];
  isFeatured: PostDocument['isFeatured'];
  author: PostDocument['author'];
};

const userSchema = new Schema({
  title: {
    type: Schema.Types.String,
    required: true,
    unique: true,
  },
  slug: {
    type: Schema.Types.String,
    required: true,
    unique: true,
  },
  content: {
    type: Schema.Types.String,
    required: true,
  },
  tags: {
    type: Schema.Types.Array,
    required: true,
  },
  status: {
    type: Schema.Types.String,
    required: true,
    enum: PostStatusEnum,
    default: PostStatusEnum.draft,
  },
  viewCount: {
    type: Schema.Types.Number,
    required: true,
    default: 0,
  },
  isFeatured: {
    type: Schema.Types.Boolean,
    required: true,
    default: false,
  },
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    index: true,
  },
}, {
  collection: "posts",
  timestamps: true,
});

const Post = mongoose.model<PostDocument>('Post', userSchema);

export { Post, PostDocument, PostInput };

Connect to the database

The application must establish a connection to the database before performing any CRUD operations.

Managing database connections is tricky with AWS Lambda because each invocation will create a new one; we must reuse the previous connection to avoid hitting the maximum database connection limit.

Mongoose provides a source code to reuse the database connection. Create a file src/utils/db-connection.ts and add the code below:


import mongoose from "mongoose";

let databaseConnection = null;
const databaseURL = process.env.DATABASE_URL;

export const connectToDatabase = async () => {
  if (!databaseURL) {
    throw new Error('The database connection string is not defined');
  }

  if (databaseConnection == null) {
    databaseConnection = mongoose.connect(databaseURL, {
      serverSelectionTimeoutMS: 5000
    }).then(() => mongoose);

    // `await`ing connection after assigning to the `databaseConnection` variable to avoid multiple function calls creating new connections
    await databaseConnection;
  }

  return databaseConnection;
}

The DATABASE_URL environment variable will be injected into the Lambda function at deployment time. Find the database URL in the MongoDB Atlas console.

Define the API Gateway infrastructure

The API Gateway is the entry point of the system. It will receive HTTP requests and forward them to the rproperLambda function to process the request and return the response.

To define it with the AWS CDK, update the file lib/ghast-api-stack.ts with the code below:


import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigw from 'aws-cdk-lib/aws-apigateway';

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

    const api = new apigw.RestApi(this, `GhastApiGateway`, {
      restApiName: `ghast-api`,
      deployOptions: {
        metricsEnabled: true,
        loggingLevel: apigw.MethodLoggingLevel.INFO,
        dataTraceEnabled: true,
      },
      cloudWatchRole: true,
    });
  }
}

The property cloudWatchRoleautomagically configure the API gateway to write logs into CloudWatch.

Steps for creating an API route

Creating a working API route consists of the three following steps:

  1. Write the Lambda function handler logic
  2. Write the Lambda function infrastructure
  3. Connect the API Gateway route to the Lambda function using IaC

In the next step of this post, we will start creating API routes.

Build the endpoint to create a post

Create a folder handlers, then create a file named create-post.ts and add the code below:


import mongoose from 'mongoose';
import { z } from 'zod';
import { APIGatewayProxyHandler } from 'aws-lambda';
import { Post, PostInput, PostStatusEnum } from "../models/post.model";
import { connectToDatabase } from "../utils/db-connection";

const createPostBodySchema = z.object({
  authorId: z.string(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()).min(1),
  isFeatured: z.boolean(),
  status: z.enum([
    PostStatusEnum.draft,
    PostStatusEnum.published,
    PostStatusEnum.archived
  ]),
});

type CreatePostBodyInput = z.infer<typeof createPostBodySchema>;

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  await connectToDatabase();

  const payload = JSON.parse(event.body ?? '{}');

  const validationResult = createPostBodySchema.safeParse(payload);

  if (validationResult.success) {
    const { data } = validationResult;
    const postInput: PostInput = {
      author: new mongoose.Types.ObjectId(data.authorId),
      content: data.content,
      title: data.title,
      isFeatured: data.isFeatured,
      tags: data.tags,
      status: data.status,
      slug: data.title.toLowerCase().replace(' ', '-'),
    };

    const [createdPost] = await Post.create([postInput]);

    return {
      statusCode: 200,
      headers: { "Content-Type": "text/json" },
      body: JSON.stringify(createdPost),
    };
  } else {
    return {
      statusCode: 400,
      headers: { "Content-Type": "text/json" },
      // @ts-ignore
      body: JSON.stringify({ message: "Invalid input data", errors: validationResult.error }),
    };
  }
};

The handler function is typed in APIGatewayProxyHandler because the API gateway will invoke it and forward the request body, headers, path, etc...

We use Zod to validate the request body at the runtime. The function returns an error status to the API gateway when the input validation fails.

Install the Node.js package for Zod and the AWS Lambda types:


npm install --save-dev @types/aws-lambda
npm install zod

Lambda function infrastructure code

Open the file lib/ghast-api-stack.ts and append the code below:


const createPostFn = new lambda.NodejsFunction(this, `CreatePostFunction`, {
      entry: path.resolve(__dirname, '../src/handlers/create-post.ts'),
      functionName: `ghast-api-create-post`,
      handler: 'handler',
      memorySize: 512,
      environment: {
        DATABASE_URL: process.env.DATABASE_URL,
      },
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(15),
      bundling: {
        target: 'es2020',
      }
    });

The "entry" property value tells the code to execute when we invoke the Lambda function.

The property "handler" indicates the function to execute in the file src/handlers/create-post.ts

Since the code is written in TypeScript and the Lambda function code runs the JavaScript, the Lambda Node.js construct uses Esbuild under the hood the bundle the function code and transpile the TS code to JS code.

The "bundling" property allows setting Esbuild bundle options.

Load the environment variables

To Lambda function infrastructure code load the environment variable DATABASE_URL injected when synthesizing the CDK code.

Create a file .env and add the code below:


DATABASE_URL="mongodb+srv://<user>:<password>@<host>/<database_name>?retryWrites=true&w=majority"

Replace the values <user>, <password>, <host>, and <database_name> with your own.

To make Typescript recognize the environment variables injected through process.env and provide autocompletion, create a file named env.d.ts at the root directory and add the code below:


export type EnvironmentVariables = {
  DATABASE_URL: string;
};

declare global {
  namespace NodeJS {
    // @ts-ignore
    type ProcessEnv = EnvironmentVariables;
  }
}

export {};

Update the tsconfig.json to add the following code:


{
  "files": ["env.d.ts"]
}

We created the Lambda infrastructure code; now we must tell the API gateway to invoke this Lambda function for POST requests having the URL path /posts.

Open the file lib/ghast-api-stack.ts and append the code below:


const createPostLambdaIntegration = new apigw.LambdaIntegration(createPostFn);
    
const blogPostsResource = api.root.addResource("posts");
blogPostsResource.addMethod("POST", createPostLambdaIntegration);

Test locally with the SAM CLI

You can run the command cdk synth anytime to validate your code infrastructure.

When the code synthesis succeeds, it generates a folder named cdk.out containing the infrastructure's CloudFormation template and the bundled Lambda function code.

Docker must be running, and you must log into the AWS ECR public repository where the Docker image of Node.js 18 for the Lambda function will be pulled.

Otherwise, you will get the error below:

The CDK synth failed because it cannot pull Node.js image from the ECR.
The CDK synth failed because it could not pull the Node.js image from the ECR.

Run the command below to log into the AWS public ECR:


aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws

The command to synthesize the CDK stack is cdk synth; it will work but will not load the environment variable DATABASE_URL.

We must read the 'env file first and export the variables. Below is the one-line command that achieves that:


export $(grep -v '^#' .env | xargs) && cdk synth

To start an instance of the API Gateway locally, run the command below:


sam local start-api -t ./cdk.out/GhastApiStack.template.json

You will get the output similar to the screenshot below:

Start the API gateway locally using the AWS SAM CLI.
Start the API gateway locally using the AWS SAM CLI.

Testing locally

To create a post, we must provide an authorId which is the post's author. Since we don't have a user in the DB, let's create a seed script to insert two users.

Create a file named src/utils/seed.ts and add the code below:


import mongoose from "mongoose";
import * as dotenv from "dotenv";
import { User } from "../models/user.model";

dotenv.config();

const usersData = [
  {
    email: "john.doe@email.com",
    fullName: "John Doe",
    password: "e10adc3949ba59abbe56e057f20f883e",
    birthDate: new Date(1996, 10, 11),
  },
  {
    email: "jane.doe@email.com",
    fullName: "Jane Doe",
    password: "7d793037a0760186574b0282f2f435e7",
    birthDate: new Date(1997, 9, 10),
  },
];

(async () => {
  const databaseConnection = await mongoose.connect(process.env.DATABASE_URL, {
    // and tell the MongoDB driver to not wait more than 5 seconds before erroring out if it isn't connected
    serverSelectionTimeoutMS: 5000
  });

  try {
    await User.create(usersData);
  } catch (e) {
    console.error(e);
  } finally {
    await databaseConnection.disconnect();
  }
})();

Add the following line in the package.json in the scripts section:


"scripts": {
    "db:seed": "ts-node ./src/utils/seed.ts"
}

Install the dotenv package and then run the command to seed the user in the database.


npm install --save-dev dotenv
npm run db:seed

Go to the MongoDB Atlas cloud console, browse the users' collections in the ghast-api database.

Browse the users collections from the MongoDB Atlas cloud console.
Browse the users' collections from the MongoDB Atlas cloud console.

Use your favorite HTTP client to send a POST request to http://127.0.0.1:3000/posts.

0:00
/
Testing the blog post creation on the API gateway running locally.

The execution takes longer because we are testing locally. Once deployed on the cloud, it will be much faster.

We can see when we pass a body without the required fields, the validation at the API level performed by Zod throws an error which is great for building robust API.

Build the endpoint to retrieve all the posts

Create a file handlers/find-all-posts.ts and add the code below:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { Post } from "../models/post.model";
import { connectToDatabase } from "../utils/db-connection";

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  await connectToDatabase();

  const posts = await Post.find().sort({ createdAt: 1 });

  return {
    statusCode: 200,
    headers: { "Content-Type": "text/json" },
    body: JSON.stringify({ data: posts }),
  };
};

Lambda function infrastructure code

Open the file lib/ghast-api-stack.ts and append the code below:


const findAllPostsFn = new lambda.NodejsFunction(this, `FindAllPostsFunction`, {
      entry: path.resolve(__dirname, '../src/handlers/find-all-posts.ts'),
      functionName: `ghast-api-all-posts`,
      handler: 'handler',
      memorySize: 512,
      environment: {
        DATABASE_URL: process.env.DATABASE_URL,
      },
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(15),
      bundling: {
        target: 'es2020',
      }
    });

Let's create the API endpoint that will execute the Lambda function.


const findAllPostsLambdaIntegration = new apigw.LambdaIntegration(findAllPostsFn);

blogPostsResource.addMethod("GET", findAllPostsLambdaIntegration);

Synthesize the CDK stack and start the API gateway with the SAM CLI.


export $(grep -v '^#' .env | xargs) && cdk synth
sam local start-api -t ./cdk.out/GhastApiStack.template.json

Make a GET request to http://127.0.0.1:3000/posts; it will return all the posts.

0:00
/
Testing the blog posts list retrieval on the API gateway running locally.

Build the endpoint to retrieve a single post

Create a file handlers/find-one-post.ts and add the code below:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { Post } from "../models/post.model";
import { connectToDatabase } from "../utils/db-connection";

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  await connectToDatabase();

  const postId = event.pathParameters.id;

  const post = await Post.findOne({ _id: postId });

  if (post) {
    return {
      statusCode: 200,
      headers: {"Content-Type": "text/json"},
      body: JSON.stringify({ data: post }),
    };
  }

  return {
    statusCode: 404,
    headers: { "Content-Type": "text/json" },
    body: JSON.stringify({ message: `No post found with the ID "${postId}"` }),
  };
};

Lambda function infrastructure code

Open the file lib/ghast-api-stack.ts and append the code below:


const findSinglePostFn = new lambda.NodejsFunction(this, `FindSinglePostFunction`, {
      entry: path.resolve(__dirname, '../src/handlers/find-one-post.ts'),
      functionName: `ghast-api-single-post`,
      handler: 'handler',
      memorySize: 512,
      environment: {
        DATABASE_URL: process.env.DATABASE_URL,
      },
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(15),
      bundling: {
        target: 'es2020',
      }
    });

Create the API endpoint that will execute the Lambda function.


const findSinglePostLambdaIntegration = new apigw.LambdaIntegration(findSinglePostFn);

const blogPostResource = blogPostsResource.addResource("{id}");
blogPostResource.addMethod("GET", findSinglePostLambdaIntegration);

Synthesize the CDK stack and start the API gateway with the SAM CLI, then make a GET request to http://127.0.0.1:3000/posts/{id} where the parameter's path {id} must be replaced by the ID of a blog post in the database.

The request will return the post if it exists.

0:00
/
Testing a blog post retrieval on the API gateway running locally.

Build the endpoint to update a blog post

Create a file handlers/update-post.ts and add the code below:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { z } from "zod";
import { Post, PostStatusEnum, UpdatePostInput } from "../models/post.model";
import { connectToDatabase } from "../utils/db-connection";

const updatePostBodySchema = z.object({
  authorId: z.string(),
  title: z.string(),
  content: z.string(),
  tags: z.array(z.string()).min(1),
  isFeatured: z.boolean(),
  status: z.enum([
    PostStatusEnum.draft,
    PostStatusEnum.published,
    PostStatusEnum.archived
  ]),
});
export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  const payload = JSON.parse(event.body ?? '{}');

  const validationResult = updatePostBodySchema.safeParse(payload);
  
  if (!validationResult.success) {
    return {
      statusCode: 400,
      headers: { "Content-Type": "text/json" },
      // @ts-ignore
      body: JSON.stringify({ message: "Invalid input data", errors: validationResult.error }),
    };
  }
  
  const { data } = validationResult;

  await connectToDatabase();

  const postId = event.pathParameters.id;

  const post = await Post.findOne({ _id: postId });

  if (!post) {
    return {
      statusCode: 404,
      headers: { "Content-Type": "text/json" },
      body: JSON.stringify({ message: `No post found with the ID "${postId}"` }),
    };
  }

  if (!post.author.equals(data.authorId)) {
    return {
      statusCode: 400,
      headers: { "Content-Type": "text/json" },
      body: JSON.stringify({ message: `Only the author can edit the post` }),
    };
  }

  const postInput: UpdatePostInput = {
    content: data.content,
    title: data.title,
    isFeatured: data.isFeatured,
    tags: data.tags,
    status: data.status,
    slug: data.title.toLowerCase().replace(/ /gm, '-'),
  };

  await Post.updateOne({ _id: postId }, postInput);

  const updatedPost = await Post.findById(postId);
  
  return {
    statusCode: 200,
    headers: {"Content-Type": "text/json"},
    body: JSON.stringify({ data: updatedPost }),
  };
};

Lambda function infrastructure code

Open the file lib/ghast-api-stack.ts and append the code below:


const updatePostFn = new lambda.NodejsFunction(this, `UpdatePostFunction`, {
      entry: path.resolve(__dirname, '../src/handlers/update-post.ts'),
      functionName: `ghast-api-update-post`,
      handler: 'handler',
      memorySize: 512,
      environment: {
        DATABASE_URL: process.env.DATABASE_URL,
      },
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(15),
      bundling: {
        target: 'es2020',
      }
    });

Create the API endpoint that will execute the Lambda function.


const updatePostLambdaIntegration = new apigw.LambdaIntegration(updatePostFn);

const blogPostResource = blogPostsResource.addResource("{id}");
blogPostResource.addMethod("PUT", updatePostLambdaIntegration);

Synthesize the CDK stack and start the API gateway with the SAM CLI, then make a PUT request to http://127.0.0.1:3000/posts/{id} where the parameter's path {id} must be replaced by the ID of a blog post in the database.

In the request body, provide the required field; the request will return the post updated.

0:00
/
Testing a blog post update on the API gateway running locally.

Build the endpoint to delete a blog post

Create a file handlers/delete-post.ts and add the code below:


import { APIGatewayProxyHandler } from 'aws-lambda';
import { Post } from "../models/post.model";
import { connectToDatabase } from "../utils/db-connection";

export const handler: APIGatewayProxyHandler = async (event, context) => {
  context.callbackWaitsForEmptyEventLoop = false;

  await connectToDatabase();

  const postId = event.pathParameters.id;
  
  await Post.deleteOne({ _id: postId });

  return {
    statusCode: 204,
    headers: {"Content-Type": "text/json"},
    body: undefined,
  };
};

Lambda function infrastructure code

Open the file lib/ghast-api-stack.ts and append the code below:


const deletePostFn = new lambda.NodejsFunction(this, `DeletePostFunction`, {
      entry: path.resolve(__dirname, '../src/handlers/delete-post.ts'),
      functionName: `ghast-api-delete-post`,
      handler: 'handler',
      memorySize: 512,
      environment: {
        DATABASE_URL: process.env.DATABASE_URL,
      },
      runtime: Runtime.NODEJS_18_X,
      timeout: cdk.Duration.seconds(15),
      bundling: {
        target: 'es2020',
      }
    });

Create the API endpoint that will execute the Lambda function.


const deletePostLambdaIntegration = new apigw.LambdaIntegration(deletePostFn);

const blogPostResource = blogPostsResource.addResource("{id}");
blogPostResource.addMethod("DELETE", deletePostLambdaIntegration);

Synthesize the CDK stack and start the API gateway with the SAM CLI, then make a DELETE request to http://127.0.0.1:3000/posts/{id} where the parameter's path {id} must be replaced by the ID of a blog post in the database.

You can call the endpoint to retrieve one blog post to ensure the post has been deleted; the request will respond with a 404 HTTP status code.

0:00
/
Testing a blog post deletion on the API gateway running locally.

Deploy the stack on AWS

Our API works locally; we want to deploy it on AWS to make it available over the Internet.

The deployment process is simple since you need to synthesize and deploy the CDK stack. Run the command below and the process, to complete.


export $(grep -v '^#' .env | xargs) && cdk synth
export $(grep -v '^#' .env | xargs) && cdk deploy

Once the stack is deployed on AWS, the URL of the API gateway will be outputted in the terminal.

Deploy the stack on AWS in the eu-west-1 region.

Test the API on AWS

Replace the local URL with the API gateway URL in the cloud and test it again. The significant difference is the request execution is fast compared to the local one.

0:00
/
Test all the API endpoints in the cloud.

You also noted that the first response takes time, and the following are way faster; if you are familiar with our AWS Lambda works, you know it is the Lambda cold start responsible for the latency in the first invocation.

Destroy the stack

All the services used in this post have a generous free tier, so you can test the project without fear. To delete everything created in the cloud, go to the project root directory on your computer and run the command below:


cdk destroy

Confirm the deletion and wait for the stack to be deleted.

Delete the project stack on AWS.
Delete the project stack on AWS.

Wrap up

Building a REST API using AWS Lambda and the API gateway requires many steps that can be summarized to:

  1. Defining the Lambda stack and the code to execute
  2. Create the Lambda integration with the API gateway
  3. Attach a route and the HTTP method to the Lambda integration
  4. Synthesize and test locally

Defining the infrastructure in TypeScript using the AWS CDK is great for keeping the configuration close to the code and easily replicating the application in multiple environments.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.