Deploy a Serverless API with AWS CDK

Build, test, and deploy a Lambda-backed status API with AWS CDK.

Introduction

30 Second Summary

When an app says it is healthy, people expect that answer to be trustworthy. A safe release needs proof before it reaches real users.

In this project, you will build a tested status API on AWS with TypeScript infrastructure defined through AWS CDK. Your live GET /health endpoint returns a JSON response from AWS Lambda.

What You'll Build

Picture your live GET /health endpoint returning ok from infrastructure that passed your tests before deployment.

By the end of this project, you'll have:

  • A local status check that prints a 200 proxy response plus a JSON body containing "status":"ok" before any cloud deployment.
  • A failing test turned green by a reusable AWS CDK construct that defines exactly one GET /health route.
  • A live endpoint that returns { service: 'cdk-status-api', status: 'ok' } after you inspect its AWS CloudFormation changes.
  • Secret Mission: An optional extension that adds a tested GET /version route through the same safe review loop while preserving GET /health.

Are there any prerequisites?

You'll need a Linux machine with Visual Studio Code plus either curl or wget.

Your AWS account needs configured AWS CLI credentials with permission to bootstrap and deploy CDK resources.

Before We Start

A clear goal gives the work ahead a purpose. Take a moment to commit to what you are building and why reviewing infrastructure matters before any cloud resources are deployed.

Prepare a Reproducible Linux Workspace

An AWS CDK workflow can drift when machines use different Node.js versions or dependency releases. A cloud deployment also needs a deliberate check of the active AWS identity.

In this step, you'll lock Node.js to 24 LTS through nvm 0.40.7. You'll also pin every project dependency.

Finally, you'll verify the local toolchain against your configured AWS credentials. That checkpoint removes machine-specific surprises before any cloud resources exist.

In this step, get ready to:
  • Prepare Node.js 24 LTS with nvm 0.40.7.
  • Create the pinned project structure.
  • Verify the local toolchain plus active AWS identity.
Install nvm and Node.js

nvm keeps the Node.js version for this project separate from other projects on your Linux machine. Its installer needs either curl or wget to download the setup script.

  • Check which supported downloader is available by running these commands:
command -v curl
command -v wget

What do these checks do?

  • Each command asks your shell to locate the named downloader.
  • A printed path confirms that downloader is available.
  • One available downloader is enough for the nvm installer.

You should see a path for curl or wget. Keep the available downloader in mind for the installer branch below.

Neither downloader has a path?

Install curl or wget with your Linux distribution's package manager. Then repeat both checks.

Need commands for your distribution? Help me install curl or wget on Linux.

  • Check your current nvm state by running this command:
nvm --version

What does this check do?

The command prints the loaded nvm release when the shell already knows about nvm. An unavailable command means this shell still needs the installation.

✔️ I have the required nvm release

Your nvm installation already matches 0.40.7. Continue to the Node.js installation below.

ⓧ I see an older release

Update nvm with the installer that matches the downloader path you found. Run only one of the following commands.

  • Update nvm with curl when command -v curl printed a path:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash

What does the curl installer do?

curl downloads the official nvm 0.40.7 installation script. Bash runs that script so your shell profile can load nvm.

  • Update nvm with wget when command -v wget printed a path:
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash

What does the wget installer do?

wget downloads the same official nvm 0.40.7 installation script. Bash runs it against your shell environment.

  • Close the current terminal window.
  • Use your Linux application launcher to open a fresh terminal.
  • Confirm the refreshed shell can find nvm by running:
command -v nvm

What confirms the update?

The command should print nvm. This confirms the fresh shell loaded the updated function.

nvm still unavailable?

Repeat the matching installer command. Check its final lines for the shell profile it updated.

Still stuck? Help me load nvm after updating it.

ⓧ nvm is not found

Install nvm with the downloader path you found. Run only one of the following commands.

  • Install nvm with curl when command -v curl printed a path:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash

What does the curl installer do?

curl downloads the official nvm 0.40.7 installation script. Bash runs that script so future shells can load nvm.

  • Install nvm with wget when command -v wget printed a path:
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash

What does the wget installer do?

wget downloads the same official nvm 0.40.7 installation script. Bash runs it against your shell environment.

  • Close the current terminal window.
  • Use your Linux application launcher to open a fresh terminal.
  • Confirm the refreshed shell can find nvm by running:
command -v nvm

What confirms the installation?

The command should print nvm. This confirms the fresh shell loaded the installed function.

nvm still unavailable?

Repeat the matching installer command. Check its final lines for the shell profile it updated.

Still stuck? Help me load my new nvm installation.

  • Install the latest Node.js 24 release by running:
nvm install 24

What does this command do?

nvm downloads the latest available Node.js 24 release. It also selects that release for the current shell.

  • Confirm the selected Node.js line by running:
node --version

What should this print?

The version should begin with v24. That prefix proves your shell is using the required LTS line.

Good progress. Your shell now runs the Node.js line that the project expects.

Still seeing another Node.js line?

Run the nvm installation command again from the refreshed shell. The completion message should identify a Node.js 24 release.

Need help? Help me select Node.js 24 with nvm.

Create the pinned project files

The workspace records its Node.js line in .nvmrc. Its configuration files also pin the compiler plus CDK dependencies.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The command changes your terminal location to the Desktop. This gives the new project a predictable location.

  • Create the cdk-status-api folder plus its four source directories by running these commands:
mkdir -p ~/Desktop/cdk-status-api/{bin,lib,lambda,test}
cd ~/Desktop/cdk-status-api

What do these commands create?

  • The first command creates bin, lib, lambda, and test inside cdk-status-api.
  • The second command moves your shell into the new project directory.
  • Confirm the four directories exist by running:
ls

What should the directory list show?

You should see bin, lib, lambda, and test in the output.

Missing a project directory?

Repeat the directory creation command from your Desktop. The -p option safely keeps directories that already exist.

Still stuck? Help me create the CDK project directories.

  • Use your Linux application launcher to start Visual Studio Code.
  • Select File from the menu bar.
  • Select Open Folder....
  • Choose the cdk-status-api folder on your Desktop.
  • Select Select Folder.
  • Select Yes, I trust the authors if the Workspace Trust dialog appears.

The Explorer should show cdk-status-api with its four directories. Visual Studio Code now treats that folder as your workspace.

  • Select New File... in the Explorer.
  • Enter .nvmrc as the file name.
  • Set the project Node.js line by copying this content:
24

What does this file do?

The .nvmrc file records Node.js 24 as the workspace runtime. nvm reads this value when you run its install command inside the folder.

  • Save .nvmrc with Ctrl+S.
  • Open the integrated terminal with View followed by Terminal.
  • Apply the runtime recorded in .nvmrc by running:
nvm install

How does nvm use the file?

With no version argument, nvm reads .nvmrc from the current workspace. It installs or selects the recorded Node.js line.

You should see a message confirming that a Node.js 24 release is active.

nvm cannot read the project version?

Check that .nvmrc contains only 24. Confirm that the integrated terminal opened inside cdk-status-api.

Still stuck? Help me use my .nvmrc file.

  • Select New File... in the Explorer.
  • Enter package.json as the file name.
  • Define the project scripts plus exact dependency pins by copying this configuration:
{
  "name": "cdk-status-api",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "tsc",
    "test": "npm run build && node --test",
    "cdk": "cdk",
    "verify": "node --version && npm --version && cdk --version && aws sts get-caller-identity",
    "synth": "cdk synth",
    "diff": "cdk diff",
    "bootstrap": "cdk bootstrap",
    "deploy": "cdk deploy --outputs-file outputs.json",
    "destroy": "cdk destroy"
  },
  "dependencies": {
    "aws-cdk-lib": "2.268.0",
    "constructs": "10.8.1"
  },
  "devDependencies": {
    "aws-cdk": "2.1141.0",
    "typescript": "5.9.3"
  }
}

What does this configuration control?

  • The scripts create one repeatable command for each build or deployment checkpoint.
  • The dependency entries pin the CDK libraries to 2.268.0 plus 10.8.1.
  • The development entries pin the local CDK CLI to 2.1141.0 plus the TypeScript compiler to 5.9.3.
  • Save package.json with Ctrl+S.
  • Confirm package.json appears in the Explorer.

Seeing JSON warnings?

Compare every quote or comma with the reference block. JSON requires double quotes around each key.

Need help? Help me fix package.json.

  • Select New File... in the Explorer.
  • Enter tsconfig.json as the file name.
  • Configure strict TypeScript compilation by copying this content:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "rootDir": ".",
    "outDir": "dist",
    "strict": true,
    "declaration": true,
    "sourceMap": true,
    "skipLibCheck": true
  },
  "include": ["bin/**/*.ts", "lib/**/*.ts"],
  "exclude": ["node_modules", "cdk.out", "dist"]
}

What does this compiler configuration do?

  • The compiler targets modern JavaScript through ES2022.
  • Strict checking catches unsafe TypeScript assumptions during the build.
  • Compiled files go into dist while generated directories stay outside the source scan.
  • Save tsconfig.json with Ctrl+S.
  • Confirm tsconfig.json appears in the Explorer.

TypeScript configuration marked invalid?

Check the braces around compilerOptions. Confirm that the include plus exclude arrays remain outside that object.

Still stuck? Help me fix tsconfig.json.

  • Select New File... in the Explorer.
  • Enter cdk.json as the file name.
  • Define how the CDK CLI starts the app by copying this configuration:
{
  "app": "npm run build --silent && node dist/bin/status-api.js"
}

What does this CDK configuration do?

The app command compiles the TypeScript project first. It then starts the generated CDK entry point from dist/bin/status-api.js.

  • Save cdk.json with Ctrl+S.
  • Confirm cdk.json appears in the Explorer.

CDK app command marked invalid?

Check that the entire app command remains inside one pair of double quotes. Confirm that the JSON object has one closing brace.

Need help? Help me fix cdk.json.

  • Select New File... in the Explorer.
  • Enter .gitignore as the file name.
  • Exclude generated project output by copying these entries:
node_modules/
dist/
cdk.out/
outputs.json

What do these entries exclude?

The file keeps downloaded dependencies plus generated build output out of source control. It also excludes synthesized CDK output plus deployment outputs.

  • Save .gitignore with Ctrl+S.
  • Confirm .gitignore appears in the Explorer.

Generated files still look included?

Check that each entry occupies its own line. Confirm that directory entries end with a forward slash.

Still stuck? Help me check my .gitignore file.

✔️ Awesome, I've got everything!

Great. Double check that all five configuration files are saved in cdk-status-api.

ⓧ I'd like to double check the full code

Compare each saved file with its complete reference below.

24

This complete .nvmrc reference contains the required Node.js line.

{
  "name": "cdk-status-api",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "tsc",
    "test": "npm run build && node --test",
    "cdk": "cdk",
    "verify": "node --version && npm --version && cdk --version && aws sts get-caller-identity",
    "synth": "cdk synth",
    "diff": "cdk diff",
    "bootstrap": "cdk bootstrap",
    "deploy": "cdk deploy --outputs-file outputs.json",
    "destroy": "cdk destroy"
  },
  "dependencies": {
    "aws-cdk-lib": "2.268.0",
    "constructs": "10.8.1"
  },
  "devDependencies": {
    "aws-cdk": "2.1141.0",
    "typescript": "5.9.3"
  }
}

This complete package.json reference contains every pinned dependency plus project script.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "rootDir": ".",
    "outDir": "dist",
    "strict": true,
    "declaration": true,
    "sourceMap": true,
    "skipLibCheck": true
  },
  "include": ["bin/**/*.ts", "lib/**/*.ts"],
  "exclude": ["node_modules", "cdk.out", "dist"]
}

This complete tsconfig.json reference defines the TypeScript build boundary.

{
  "app": "npm run build --silent && node dist/bin/status-api.js"
}

This complete cdk.json reference defines the CDK app command.

node_modules/
dist/
cdk.out/
outputs.json

This complete .gitignore reference excludes generated project artifacts.

Install dependencies and verify your identity

The exact pins in package.json make the toolchain repeatable. The final verification script checks those local tools against the AWS CLI identity already configured on your machine.

The first dependency installation can pause while npm downloads packages. That quiet period is normal.

  • Install the pinned project dependencies by running:
npm install

What does this command do?

npm reads the exact dependency versions from package.json. It installs them into the local node_modules directory.

The installation also creates package-lock.json to record the resolved dependency tree.

  • Confirm node_modules appears in the Explorer.
  • Confirm package-lock.json appears in the Explorer.

Your pinned CDK toolchain is now installed inside the project. The global machine configuration cannot silently substitute another CDK CLI.

Dependency installation failed?

Check that the terminal is inside cdk-status-api. Confirm that package.json has no JSON warnings.

Still stuck? Help me troubleshoot npm install.

Before you run the final check, which tool versions plus AWS identity fields do you expect your workspace to report?

  • Run the workspace verification script from cdk-status-api by running:
npm run verify

What does the verification script check?

  • Node.js prints the runtime selected through nvm.
  • npm prints its installed version.
  • The project-local CDK CLI prints its version.
  • The AWS CLI returns the active caller identity.

You should see a Node.js version beginning with v24. npm plus CDK should each print a version.

The final JSON response should include your expected Account plus Arn values. That identity is the one future bootstrap plus deployment commands use.

Unexpected AWS identity?

Stop before the bootstrap step if the account or ARN is unexpected. Correct the active AWS profile before continuing.

Need help tracing the credentials? Help me identify my active AWS CLI profile.

That's the foundation locked in. Your project now has a pinned runtime plus a verified deployment identity. Next up, you'll make the status response work locally before introducing cloud infrastructure.

Make the Status Response Work Locally

Your workspace now has the verified Node.js 24 LTS toolchain. That foundation gives you a controlled place to test application behavior.

The first useful slice is the response contract that AWS Lambda sends through Amazon API Gateway. You will build it locally so deployment concerns stay out of this behavior check.

In this step, get ready to:
  • Create an asynchronous handler that returns an API Gateway proxy response.
  • Invoke the handler locally to inspect its outer response and serialized body.
Create the local Lambda handler

An API Gateway proxy response wraps application data in an outer object with an HTTP status. Its body contains a serialized JSON string for the caller.

  • Launch Visual Studio Code from your Linux application launcher.
  • Use its folder picker to select the existing cdk-status-api directory.

The file sidebar now shows the bin, lib, lambda, and test directories from your workspace.

  • Select the existing lambda directory in the file sidebar.
  • Use the new-file control to create index.js inside lambda.

You now have an empty lambda/index.js file ready for the local handler.

  • Fill lambda/index.js by copying the code below:
exports.handler = async () => ({
  statusCode: 200,
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    service: 'cdk-status-api',
    status: 'ok',
  }),
});

What Does This Handler Return?

  • The exports.handler assignment makes the function available when Node.js loads the module.
  • The async keyword makes the handler return a Promise.
  • The statusCode value tells API Gateway that the request succeeded with 200.
  • The headers object identifies the response content as application/json.
  • The JSON.stringify() call converts the service data into the string required by body.
  • Save lambda/index.js.
  • Confirm the editor tab shows lambda/index.js with the saved handler code.

Handler File in the Wrong Place?

  • Check that index.js sits inside the existing lambda directory.
  • Check that the filename ends with .js.
  • Still stuck? Help me fix my local Lambda handler file.

✔️ Awesome, I've got everything!

Your saved lambda/index.js contains the complete local handler.

ⓧ I'd like to double check the full code

  • Compare lambda/index.js with the complete file below.
exports.handler = async () => ({
  statusCode: 200,
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    service: 'cdk-status-api',
    status: 'ok',
  }),
});
Invoke and inspect the response

A direct invocation lets Node.js call the handler without any cloud resources. The printed value exposes the outer proxy response plus the JSON string stored inside body.

Before you run this, which parts of the response do you expect Node.js to print outside the serialized body value?

  • Invoke the local handler from the terminal by running this command:
node -e "require('./lambda').handler().then(console.log)"

What Does This Command Prove?

  • The require('./lambda') call loads the module from lambda/index.js.
  • The handler() call returns a Promise. The then(console.log) call prints its fulfilled value.
  • You should see statusCode set to 200. You should also see a content-type header set to application/json.
  • The body value appears as a quoted JSON string. It contains "status":"ok" alongside the service name.

That is your first visible status response working. The handler now produces the proxy shape that API Gateway needs.

Local Handler Not Loading?

  • Check that your terminal is still in the cdk-status-api directory.
  • Confirm that lambda/index.js is saved before running the command again.
  • Compare the exported identifier with exports.handler if the command cannot find the function.
  • Still stuck? Help me troubleshoot the local handler invocation.

Your status behavior is now proven locally. Next, you will write an infrastructure test that exposes the missing Lambda-backed route.

Write a Failing Infrastructure Test

Your local handler now returns the health response on demand. The next goal is to describe the cloud boundary around it.

That response does not prove an AWS Lambda function exists. It also does not prove that an Amazon API Gateway HTTP API route can reach it.

You will encode those requirements as AWS CDK assertions before adding the resources. The assertions become an executable specification for the next build.

In this step, get ready to:
  • Create an empty TypeScript stack with its app entry point.
  • Define assertions for the Lambda function plus its HTTP route.
  • Run the test to expose the missing deployment boundary.
Create the empty CDK app

The StatusApiStack class defines one deployment boundary using TypeScript. Its constructor accepts optional stack properties while the stack itself remains empty.

  • Return to the cdk-status-api workspace in Visual Studio Code.
  • Use the file tree's new-file control to create status-api-stack.ts inside the existing lib directory.
  • Build the empty stack class by pasting this code:
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class StatusApiStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
  }
}

What Does This Stack Class Do?

  • The extends Stack declaration gives StatusApiStack the behavior of a CDK stack.
  • The scope parameter identifies the parent construct.
  • The id parameter identifies this stack within the construct tree.
  • The question mark in props?: StackProps makes the properties parameter optional.
  • The super(scope, id, props) call initializes the base stack with those values.
  • Save lib/status-api-stack.ts.
  • Confirm status-api-stack.ts appears beneath lib in the file tree.

Stack File in the Wrong Place?

  • Confirm the complete path is lib/status-api-stack.ts inside cdk-status-api.
  • Compare the capitalization of StatusApiStack with the code above.
  • Still stuck? Help me check my empty CDK stack class.

An App is the top-level container for CDK stacks. Calling app.synth() asks the app to assemble every registered stack.

  • Use the file tree's new-file control to create status-api.ts inside the existing bin directory.
  • Register StatusApiStack with the app by pasting this code:
import { App } from 'aws-cdk-lib';
import { StatusApiStack } from '../lib/status-api-stack';

const app = new App();
new StatusApiStack(app, 'StatusApiStack');
app.synth();

How Does the App Entry Point Work?

  • The App import provides the root of the construct tree.
  • The app identifier holds that root object.
  • The new StatusApiStack(...) expression registers one stack named StatusApiStack.
  • The app.synth() call prepares the registered stack for synthesis.
  • Save bin/status-api.ts.
  • Confirm status-api.ts appears beneath bin in the file tree.

App Entry Point Not Linked?

  • Confirm the complete path is bin/status-api.ts.
  • Check that the stack import uses ../lib/status-api-stack exactly.
  • Still stuck? Help me connect my CDK app to StatusApiStack.
Define the infrastructure assertions

An infrastructure assertion inspects the template produced from a stack. The Template assertion API lets the test require specific resources plus exact properties.

  • Use the file tree's new-file control to create status-api-stack.test.js inside the existing test directory.
  • Define the required infrastructure by pasting this test:
const test = require('node:test');
const { App } = require('aws-cdk-lib');
const { Template } = require('aws-cdk-lib/assertions');
const { StatusApiStack } = require('../dist/lib/status-api-stack');

test('synthesizes one Lambda-backed GET /health route', () => {
  const app = new App();
  const stack = new StatusApiStack(app, 'TestStatusApiStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::Lambda::Function', 1);
  template.hasResourceProperties('AWS::Lambda::Function', {
    Runtime: 'nodejs24.x',
    MemorySize: 128,
    Timeout: 3,
    LoggingConfig: {
      LogFormat: 'JSON',
    },
  });
  template.hasResourceProperties('AWS::ApiGatewayV2::Api', {
    ProtocolType: 'HTTP',
  });
  template.resourceCountIs('AWS::ApiGatewayV2::Route', 1);
  template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
    RouteKey: 'GET /health',
  });
});

What Does This Test Specify?

  • The node:test import supplies the test function.
  • The Template.fromStack(stack) call turns the construct tree into a template that assertions can inspect.
  • The first count assertion requires exactly one AWS::Lambda::Function resource.
  • The Lambda property assertion pins the runtime to nodejs24.x.
  • The API assertion requires the HTTP protocol.
  • The route assertions require exactly one route with the key GET /health.
  • Save test/status-api-stack.test.js.
  • Confirm status-api-stack.test.js appears beneath test in the file tree.

Test Import Paths Look Wrong?

  • Confirm the test path is test/status-api-stack.test.js.
  • Check that the stack import points to ../dist/lib/status-api-stack.
  • Check that every resource type matches the capitalization shown above.
  • Still stuck? Help me check my CDK infrastructure assertion test.
Run the planned failure

Before you run the test, which assertion do you expect to report the first missing resource?

  • Reveal the missing deployment boundary by running this command:
npm test

Why Is This Failure Useful?

The test script compiles the TypeScript files first. A successful build proves the app entry point plus stack class are valid.

The Node.js test runner then evaluates the synthesized template. The first failed assertion identifies the first missing resource in the specification.

You should see the TypeScript build complete before the test is marked as failed. The failure reports a resource-count mismatch for AWS::Lambda::Function.

This is the intended shortfall. The empty stack cannot satisfy the API or route assertions yet.

Test Stops Before the Assertion?

  • Check bin/status-api.ts for a TypeScript syntax problem if the build fails.
  • Check lib/status-api-stack.ts for a mismatched class name if the stack cannot compile.
  • Confirm the test imports the compiled stack from ../dist/lib/status-api-stack.
  • Still stuck? Help me reach the intended CDK assertion failure.

✔️ Awesome, I've got everything!

Your TypeScript build succeeds. Your infrastructure test now fails for the planned missing-resource reason.

ⓧ I'd like to double check the full code

  • Compare your cumulative project files with these exact references:
node_modules/
dist/
cdk.out/
outputs.json
24
import { App } from 'aws-cdk-lib';
import { StatusApiStack } from '../lib/status-api-stack';

const app = new App();
new StatusApiStack(app, 'StatusApiStack');
app.synth();
{
  "app": "npm run build --silent && node dist/bin/status-api.js"
}
exports.handler = async () => ({
  statusCode: 200,
  headers: {
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    service: 'cdk-status-api',
    status: 'ok',
  }),
});
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class StatusApiStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
  }
}
{
  "name": "cdk-status-api",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "tsc",
    "test": "npm run build && node --test",
    "cdk": "cdk",
    "verify": "node --version && npm --version && cdk --version && aws sts get-caller-identity",
    "synth": "cdk synth",
    "diff": "cdk diff",
    "bootstrap": "cdk bootstrap",
    "deploy": "cdk deploy --outputs-file outputs.json",
    "destroy": "cdk destroy"
  },
  "dependencies": {
    "aws-cdk-lib": "2.268.0",
    "constructs": "10.8.1"
  },
  "devDependencies": {
    "aws-cdk": "2.1141.0",
    "typescript": "5.9.3"
  }
}
const test = require('node:test');
const { App } = require('aws-cdk-lib');
const { Template } = require('aws-cdk-lib/assertions');
const { StatusApiStack } = require('../dist/lib/status-api-stack');

test('synthesizes one Lambda-backed GET /health route', () => {
  const app = new App();
  const stack = new StatusApiStack(app, 'TestStatusApiStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::Lambda::Function', 1);
  template.hasResourceProperties('AWS::Lambda::Function', {
    Runtime: 'nodejs24.x',
    MemorySize: 128,
    Timeout: 3,
    LoggingConfig: {
      LogFormat: 'JSON',
    },
  });
  template.hasResourceProperties('AWS::ApiGatewayV2::Api', {
    ProtocolType: 'HTTP',
  });
  template.resourceCountIs('AWS::ApiGatewayV2::Route', 1);
  template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
    RouteKey: 'GET /health',
  });
});
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "rootDir": ".",
    "outDir": "dist",
    "strict": true,
    "declaration": true,
    "sourceMap": true,
    "skipLibCheck": true
  },
  "include": ["bin/**/*.ts", "lib/**/*.ts"],
  "exclude": ["node_modules", "cdk.out", "dist"]
}

That planned failure is a win because your infrastructure specification now runs. Next up, you will build the reusable construct that satisfies it.

Build the Tested Serverless API Construct

Your local handler returns the promised status response. The failing test now proves that no deployed route can reach it.

In this step, you will turn that test specification into a reusable AWS CDK construct written in TypeScript. The test becomes the gate that proves your infrastructure matches the design.

In this step, get ready to:
  • Define the reusable Lambda construct with bounded logging.
  • Expose only GET /health through an Amazon API Gateway HTTP API.
  • Synthesize a passing AWS CloudFormation assembly.
Build the reusable status construct

An AWS Lambda function executes your existing handler. A user-controlled Amazon CloudWatch Logs log group gives the learning stack explicit retention and cleanup behavior.

Why use a reusable construct?

The StatusApi construct owns the resources that make the service work. This keeps the API design together when another stack needs to reuse it.

The StatusApiStack class handles deployment-level choices such as tags and outputs. Putting every resource directly in that class would tie the service design to one deployment context.

  • In the Visual Studio Code window from earlier, right-click the lib folder in the Explorer sidebar.
  • Select New File.
  • Enter status-api.ts.
  • Paste the logging and compute resources into lib/status-api.ts using this code:
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class StatusApi extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const logGroup = new logs.LogGroup(this, 'HealthLogs', {
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const handler = new lambda.Function(this, 'HealthFunction', {
      description: 'Returns the public health status for the CDK status API',
      runtime: lambda.Runtime.NODEJS_24_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      memorySize: 128,
      timeout: Duration.seconds(3),
      logGroup,
      loggingFormat: lambda.LoggingFormat.JSON,
    });
  }
}

What does this construct define?

  • The HealthLogs log group keeps events for seven days. Its removal policy deletes the group with this learning stack.
  • The HealthFunction function uses the Node.js 24 runtime. It loads index.handler from the existing lambda directory.
  • The function receives 128 MB of memory. Its timeout stops an invocation after three seconds.
  • The JSON logging format gives each runtime log entry a structured shape.
  • Save lib/status-api.ts.
  • Compile the new construct by running this command:
npm run build

What does this build check prove?

The build script runs the TypeScript compiler against your bin and lib files. A clean return to the prompt proves that the new class and resource properties are valid.

Good progress. The reusable logging and compute layer now compiles without TypeScript errors.

Seeing a TypeScript build error?

  • Check that every import in lib/status-api.ts matches the code above.
  • Check that the LogGroup block closes before the Function block begins.
  • Still stuck? Help me fix my StatusApi TypeScript build error.
  • In lib/status-api.ts, place the cursor directly below the StatusApi class declaration.
  • Add the typed endpoint property:
  public readonly apiEndpoint: string;

Why expose this property?

The apiEndpoint property lets the parent stack read the API's generated URL. Its readonly modifier prevents callers from replacing that value after construction.

  • In lib/status-api.ts, place the cursor after the handler resource block.
  • Paste the integration and exact health route into the constructor:
    const healthIntegration = new integrations.HttpLambdaIntegration(
      'HealthIntegration',
      handler,
    );

    const api = new apigwv2.HttpApi(this, 'HttpApi', {
      apiName: 'cdk-status-api',
      description: 'Public, read-only status API for the AWS CDK learning project',
    });

    api.addRoutes({
      path: '/health',
      methods: [apigwv2.HttpMethod.GET],
      integration: healthIntegration,
    });

    this.apiEndpoint = api.apiEndpoint;

How does the route work?

  • The HealthIntegration connects the HTTP route to HealthFunction.
  • The HttpApi resource creates the public API named cdk-status-api.
  • The route accepts only GET requests at /health.
  • The final assignment exposes the generated endpoint through the typed property.
  • Save lib/status-api.ts.
  • Compile the completed construct by running this command:
npm run build

What does this second build check?

This build checks the endpoint property and the HTTP API imports. It also confirms that the route configuration uses valid CDK types.

The compiler should return to the prompt without an error. Your reusable construct now contains the complete Lambda-backed health route.

Does the endpoint property fail to compile?

  • Check that public readonly apiEndpoint: string; sits inside the StatusApi class.
  • Check that this.apiEndpoint = api.apiEndpoint; appears before the constructor closes.
  • Still stuck? Help me fix the endpoint property and health route.

✔️ Awesome, I've got everything!

Your lib/status-api.ts file is saved. The complete construct compiles successfully.

ⓧ I'd like to double check the full code

  • Compare lib/status-api.ts with this complete version:
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export class StatusApi extends Construct {
  public readonly apiEndpoint: string;

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

    const logGroup = new logs.LogGroup(this, 'HealthLogs', {
      retention: logs.RetentionDays.ONE_WEEK,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const handler = new lambda.Function(this, 'HealthFunction', {
      description: 'Returns the public health status for the CDK status API',
      runtime: lambda.Runtime.NODEJS_24_X,
      handler: 'index.handler',
      code: lambda.Code.fromAsset('lambda'),
      memorySize: 128,
      timeout: Duration.seconds(3),
      logGroup,
      loggingFormat: lambda.LoggingFormat.JSON,
    });

    const healthIntegration = new integrations.HttpLambdaIntegration(
      'HealthIntegration',
      handler,
    );

    const api = new apigwv2.HttpApi(this, 'HttpApi', {
      apiName: 'cdk-status-api',
      description: 'Public, read-only status API for the AWS CDK learning project',
    });

    api.addRoutes({
      path: '/health',
      methods: [apigwv2.HttpMethod.GET],
      integration: healthIntegration,
    });

    this.apiEndpoint = api.apiEndpoint;
  }
}
Connect the construct to the stack

The new construct remains outside the synthesized application until the stack creates it. The stack also owns the tags and output that describe this deployment.

  • In the Explorer sidebar from earlier, select lib/status-api-stack.ts.
  • Replace the file with this completed stack class:
import { CfnOutput, Stack, StackProps, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StatusApi } from './status-api';

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

    Tags.of(this).add('Project', 'cdk-status-api');
    Tags.of(this).add('Environment', 'learning');

    const statusApi = new StatusApi(this, 'StatusApi');

    new CfnOutput(this, 'ApiEndpoint', {
      value: statusApi.apiEndpoint,
    });
  }
}

What does the stack add?

  • The StatusApi import makes the reusable construct available to the stack.
  • The Project and Environment tags label supported resources with consistent metadata.
  • The StatusApi instance adds the log group and Lambda-backed HTTP API to this stack.
  • The ApiEndpoint output makes the generated URL available after deployment.
  • Save lib/status-api-stack.ts.

Before you run the test, consider whether the new stack should satisfy every assertion now.

  • Test the completed infrastructure by running this command:
npm test

What does this test prove?

The test compiles the TypeScript app before inspecting its synthesized template. Its assertions require one Lambda function with the chosen runtime settings.

The same test requires one HTTP API and exactly one GET /health route. A pass proves that the reusable construct resolves the missing infrastructure from the previous step.

That's the failing specification resolved. You should now see one passing test in the terminal.

Is the infrastructure test still failing?

  • Check that StatusApiStack creates new StatusApi(this, 'StatusApi').
  • Check that the function uses lambda.Runtime.NODEJS_24_X and lambda.LoggingFormat.JSON.
  • Check that the route path is /health with the GET method.
  • Still stuck? Help me compare my CDK construct with the failing assertions.

✔️ Awesome, I've got everything!

Your stack now creates the reusable construct. It also applies both tags and exposes the endpoint output.

ⓧ I'd like to double check the full code

  • Compare lib/status-api-stack.ts with this complete version:
import { CfnOutput, Stack, StackProps, Tags } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StatusApi } from './status-api';

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

    Tags.of(this).add('Project', 'cdk-status-api');
    Tags.of(this).add('Environment', 'learning');

    const statusApi = new StatusApi(this, 'StatusApi');

    new CfnOutput(this, 'ApiEndpoint', {
      value: statusApi.apiEndpoint,
    });
  }
}
Prove the template passes and synthesizes

A passing assertion verifies the expected resource shape. Synthesis now turns that tested design into the cloud assembly that the next step can review.

Before you run the complete check, consider whether synthesis can succeed after the test passes.

  • Run the test and synthesis sequence with this command:
npm test && npm run synth

What does this sequence produce?

The shell runs synthesis only after the test succeeds. This prevents a failing infrastructure design from moving into the deployment assembly.

The synth script creates cdk.out for StatusApiStack. That folder contains the generated CloudFormation assembly.

You should see one passing test followed by the synthesized template output. The cdk.out directory is now available in your workspace.

  • Refresh the cdk-status-api folder in the Explorer sidebar from earlier.
  • Expand the cdk.out directory.

You should see the synthesized assembly files for StatusApiStack. This confirms that your tested infrastructure is ready for change review.

Did synthesis stop or miss cdk.out?

  • Check the test output first because synthesis runs only after a passing test.
  • Check that cdk.json still points to dist/bin/status-api.js.
  • Check that bin/status-api.ts still instantiates StatusApiStack.
  • Still stuck? Help me troubleshoot my CDK synthesis.

Strong work. Your failing test now guards a reusable serverless API construct that compiles and synthesizes into a deployable assembly.

Review, Deploy, and Call the Live API

Your AWS CDK construct now passes its infrastructure test. It also synthesizes a deployable template.

A local pass proves the template shape. You still need to review the AWS CloudFormation changes before testing the real endpoint.

In this step, get ready to:
  • Prepare the selected AWS environment for deployment.
  • Review the CloudFormation changes for expected resources.
  • Prove the deployed API responds only on its named health route.
Confirm shared deployment support

Bootstrapping supplies the shared roles and asset storage that CDK needs for deployment. These resources live in the selected AWS account and Region under the CDKToolkit stack.

This action can take a few minutes while CloudFormation creates or updates the support stack. It changes shared deployment infrastructure in your active AWS environment.

  • Create or update CDKToolkit in the active account and Region by running:
npm run bootstrap

What does this bootstrap do?

The bootstrap script creates or updates CDKToolkit. That shared stack provides deployment roles and asset storage for CDK projects.

The application still lives in its own StatusApiStack. This separation lets future CDK projects reuse the same deployment foundation.

You should see the bootstrap operation complete for the selected environment. That shared foundation is now ready to support your deployment.

Bootstrap targeting the wrong environment?

  • Stop if the bootstrap output names an unexpected account or Region.
  • Correct the active AWS profile before continuing.
  • Still stuck? Help me verify the AWS account and Region used by my CDK bootstrap.

The bootstrap stack prepares the environment. A fresh synthesis makes sure the local cloud assembly reflects the exact code you are about to deploy.

  • Refresh the synthesized cloud assembly by running:
npm run synth

What does synthesis refresh?

The synthesis script rebuilds the TypeScript app and writes the CloudFormation assembly to cdk.out. The next review compares this current assembly with the deployed environment.

You should see synthesis complete for StatusApiStack. Your local deployment assembly is now current.

Synthesis failing after bootstrap?

  • Confirm the terminal remains inside the cdk-status-api workspace.
  • Compare the synthesis failure with the successful run from the previous step.
  • Still stuck? Help me diagnose why my previously working CDK synthesis now fails after bootstrap.
Inspect the CloudFormation diff

A CDK diff compares your synthesized stack with the current deployed state. The review acts as a gate between tested code and live infrastructure.

Before you run this check, do you expect every addition to map to the status API? Hold that prediction.

  • Generate the deployment diff by running:
npm run diff

What should the diff contain?

The diff should contain only resources required by the tested status API:

  • One AWS Lambda function.
  • One execution role for the function.
  • One invoke permission scoped to GET /health.
  • One Amazon API Gateway HTTP API.
  • One default API stage.
  • One GET /health route.
  • One Lambda integration for the health route.
  • One Amazon CloudWatch Logs log group.
  • The ApiEndpoint stack output.
  • Compare every addition with the expected list above.
  • Stop before deployment if the diff names unrelated resources.

A matching diff gives you a reviewed change set. Every planned resource now traces back to the construct and its passing test.

Deploy and verify the route boundary

This deployment creates live usage-based AWS resources. Tutorial-scale calls keep that exposure small.

The stack can take several minutes to deploy through CloudFormation. The command may pause for an IAM-related approval because the Lambda function needs an execution role.

  • Deploy StatusApiStack by running this command:
npm run deploy

What does this deployment do?

The deployment script sends the reviewed assembly through CloudFormation. It also writes the deployed stack outputs to outputs.json.

The output file groups the endpoint under StatusApiStack.ApiEndpoint. Your verification command reads that value directly.

  • Approve the IAM-related change if prompted.
  • Wait for StatusApiStack to finish deploying.

You should see the deployment complete with the ApiEndpoint output. Your tested stack is now live in AWS.

Deployment not completing?

  • Confirm the deployment uses the same account and Region that you bootstrapped.
  • Check that the diff contained only the expected status API resources.
  • Still stuck? Help me troubleshoot my StatusApiStack deployment.

The generated outputs.json file gives your local tools the deployed endpoint. The health call now tests the complete path from your terminal to the live Lambda response.

Before you call the endpoint, do you expect the response body to match the local handler result? Hold that prediction.

  • Call the deployed health route by running:
node -e "fetch(require('./outputs.json').StatusApiStack.ApiEndpoint + '/health').then(r => r.json()).then(console.log)"

How does this live check work?

  • The require('./outputs.json').StatusApiStack.ApiEndpoint expression reads the deployed URL from the output file.
  • The stable Node.js fetch() function sends a request to /health.
  • The r.json() call parses the response body as JSON before printing it.

You should see { service: 'cdk-status-api', status: 'ok' }. That response proves the public health route reaches your deployed Lambda handler.

Health call not returning the expected JSON?

  • Confirm outputs.json exists inside the cdk-status-api workspace.
  • Confirm the file contains StatusApiStack.ApiEndpoint.
  • Still stuck? Help me troubleshoot the live health endpoint response.

The endpoint root provides a boundary check for the named route. A different response confirms that the health handler does not act as a catch-all route.

  • Press the Up Arrow key in the terminal to recall the health fetch command.
  • Delete + '/health' from the recalled command.

Before you press Enter, do you expect the endpoint root to return the same health object? Hold that prediction.

  • Press Enter to call the endpoint root.

You should see a missing-route response instead of the health response. This proves the API exposes the named health route without a catch-all health route.

Still seeing the health response at the root?

  • Recall the command again if the health response repeats.
  • Confirm + '/health' is absent before pressing Enter.
  • Still stuck? Help me verify that my HTTP API exposes only the named health route.

That closes the deployment loop. Your tested construct is now a reviewed and narrowly routed live API.

Secret mission

Add a Version Route Through the Safe Change Loop

Extend your deployed status service with a tested `GET /version` route. You will update the Lambda behavior, expand the infrastructure assertions, review the CloudFormation changes, and redeploy the API without replacing it.

Clean Up Your Resources

Clean Up Your Resources

Your deployed AWS resources can continue to create usage-based costs while they remain online. Decide whether to keep everything running, pause requests, or delete the learning stack and local workspace.

Cost warning

Amazon API Gateway HTTP API requests are usage-based. AWS Lambda execution is also usage-based.

Amazon CloudWatch Logs can charge for ingestion and storage. Bootstrap asset storage can persist after application deletion.

Pausing prevents new tutorial traffic. Deleting removes the application stack and its retained logs.

Resources you used:

  • The deployed StatusApiStack application stack. This includes the Lambda function, execution role, HTTP API, default stage, route integrations, scoped permissions, log group, and deployment output.
  • The local cdk-status-api workspace. This includes the source files plus generated files such as node_modules, package-lock.json, dist, cdk.out, and outputs.json.

Why CDKToolkit Remains

The CDKToolkit stack provides shared deployment roles and asset storage for future AWS CDK projects in this account and Region.

AWS recommends keeping this bootstrap infrastructure available for future updates. This cleanup leaves it in place.

Keep everything running

No action needed. Choose this if you plan to keep testing the live status API or extend it further.

  • Keep StatusApiStack deployed so /health and /version remain callable.
  • Keep the cdk-status-api workspace available for future code changes.
  • Leave CDKToolkit available for future CDK deployments.

Pause - I'll come back to this later

Stop sending traffic to the public endpoint while keeping the deployment ready for later. Serverless request activity stops while the endpoint is idle, but stored logs and bootstrap assets remain.

  • Stop calling the /health endpoint.
  • Stop calling the /version endpoint.
  • Stop sharing the endpoint stored in outputs.json.
  • Keep StatusApiStack deployed for your return.
  • Keep the cdk-status-api workspace on your Linux machine.

Delete - I don't want to use this again

This option permanently removes the deployed API and the local workspace. The shared CDKToolkit stack stays available for future CDK projects.

  • Copy any project files you want to keep to another location.
  • Destroy StatusApiStack by running this command:
  • Confirm the deletion when AWS CDK asks.
npm run destroy

What Does This Command Remove?

  • The project script runs the local cdk destroy workflow.
  • AWS CloudFormation removes StatusApiStack with its Lambda function, HTTP API, routes, permissions, log group, and output.
  • The shared CDKToolkit stack remains in the selected account and Region.

The stack deletion is complete when AWS CloudFormation can no longer find StatusApiStack.

  • Verify the stack deletion by running this command:
aws cloudformation describe-stacks --stack-name StatusApiStack

What Should I See?

After deletion completes, the AWS CLI returns a ValidationError. That result confirms the named stack no longer exists.

Good work. StatusApiStack is gone from the selected AWS account and Region.

Removing the workspace is permanent on this machine. The next commands first leave cdk-status-api so Linux can safely delete the directory.

  • Delete the cdk-status-api workspace by running these commands:
cd ..
rm -rf cdk-status-api

What Happens to the Local Files?

  • The first command moves your shell into the directory that contains cdk-status-api.
  • The second command removes the source files and generated files inside cdk-status-api.
  • Use your Linux file manager to view the folder that previously contained cdk-status-api.

You should no longer see the cdk-status-api directory.

Nice Work!

Nice Work!

You made it! Your tested TypeScript status API now runs on AWS with live health and version routes.

You've learned how to:

  • Prove local application behavior through a direct AWS Lambda handler invocation that exposes the proxy response.
  • Turn a deliberately failing infrastructure test into a reusable AWS CDK construct whose assertions enforce the Lambda runtime plus exact route keys.
  • Move tested infrastructure through a reviewed deployment workflow that checks AWS CloudFormation changes before publishing a live Amazon API Gateway HTTP API.
  • Complete the optional Secret Mission by adding a tested GET /version route while preserving GET /health.

Ready to quiz yourself?