API with Hono, Lambda and AWS CDK

Last updated on
API with Hono, Lambda and AWS CDK

Why Hono?

Hono is a new framework compatible with almost any runtime that exists. Compared to traditional ExpressJS, it is easier to build applications for runtimes like Lambda or Cloudflare Workers using Hono. Hono provides adapters to ease this process. The syntax is quite similar to what we're used to in ExpressJS.

AWS CDK

In this tutorial, we will be using AWS CDK as our Infrastructure as a Service (IaaS) solution for building with AWS. While we could use a manual build and deploy process for Lambda, with CDK we can automate everything using code.

AWS CDK is a toolkit that allows you to define your AWS infrastructure using just code. It's similar to Terraform but not limited to a single coding language. It supports TypeScript, Java, C#, Python, and Go. You can choose the language you're most familiar with. For this tutorial, we will be using TypeScript. Enough intro, let's start. I'll explain the caveats part in the process.

1. Initialize AWS CDK project

bash
mkdir hello-cdk && cd hello-cdk

Make sure you have AWS CDK installed in your machine, if not then follow this

bash
cdk init app --language typescript

Your project will initialize like this

.
├── bin
│   └── hello-cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── hello-cdk-stack.ts
├── package.json
├── README.md
├── test
│   └── hello-cdk.test.ts
└── tsconfig.json

Now we switch our package manager to pnpm(my preference) and reinstall the dependency. You will see some default code generated. We leave it behind first.

2. Hono setup

Run this to create a hono app. Make sure to add .(dot) for creating inside current folder. You will be prompted to choose adapters. Choose AWS Lambda - without edge.

pnpm create hono@latest .
pnpm create hono@latest .          
create-hono version 0.18.0
 Using target directory .
 Which template do you want to use? aws-lambda
 Directory not empty. Continue? Yes
 Do you want to install project dependencies? Yes
? Which package manager do you want to use? (Use arrow keys)
  npm
  bun
  deno
 pnpm
  yarn

Hono files has been created under src folder

.
├── bin
│   └── hello-cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── hello-cdk-stack.ts
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   └── index.ts
├── test
│   └── hello-cdk.test.ts
└── tsconfig.json

Inside index.ts, hono has scaffold api for lambda, now we separate into app.local.ts and app.ts for Hono stuffs and into index.ts for the application logic stuff.

For local testing, create this. Install @hono/node-server

src/app.local.ts
import app from "./index";
import { serve } from "@hono/node-server";

serve(app);
console.log("Server running on port 3000")

Change index.ts to this. Export the app instance for lambda and local usage.

index.ts
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

export default app;

Create lambda.ts inside lib folder

lib/lambda.ts
import app from "../src/index";
import { handle } from "hono/aws-lambda";

export const handler = handle(app);

Now test the local. Run pnpm dev. Use tsx or bun for running typescript. curl or go to localhost:3000 to make sure the route is working. You will see Hello Hono! on your browser(or console).

AWS CDK

1. As our project initialize, we need to configure our CDK Stacks. This stack consist of infrastructure that we will use to run our lambda function.

app-stack.ts
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Architecture, Runtime } from "aws-cdk-lib/aws-lambda";
import { LambdaRestApi } from "aws-cdk-lib/aws-apigateway";
import { NodejsFunction, SourceMapMode } from "aws-cdk-lib/aws-lambda-nodejs";
import { Certificate } from "aws-cdk-lib/aws-certificatemanager";

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

    const myLambdaFunction = new NodejsFunction(this, "my-lambda-function", {
      functionName: "myLambdaFunctionName",
      architecture: Architecture.ARM_64,
      runtime: Runtime.NODEJS_20_X,
      timeout: Duration.seconds(10),
      memorySize: 256,
      entry: "src/lambda.ts", // our lambda entrypoint
      handler: "handler", // the function name
      bundling: {
        minify: true,
        sourceMap: true,
        sourceMapMode: SourceMapMode.INLINE,
        sourcesContent: false,
        target: "esnext",
      },
      environment: {
        REGION: "ap-southeast-5" // leave if you have configured on the AWS CLI
      },
    });

    const certificate = Certificate.fromCertificateArn(
      this,
      "your-aws-cert-name",
      process.env.CERTIFICATE_ARN!
    );

    new LambdaRestApi(this, "my-api-gateway", {
      restApiName: "my-api",
      handler: myLambdaFunction,
      proxy: true,
      deployOptions: {
        stageName: "prod",
      },
      domainName: {
        domainName: "my-custom-domain.com",
        certificate,
      },
    });
  }
}

We create myLambdaFunction for our handler using NodejsFunction construct. Next attach the function to api gateway using LambdaRestApi construct. If you have custom domain, take the domain certificate ARN, create a cert and attach to the custom domain.

2. Install esbuild as our bundler for the lambda and cdk. pnpm add esbuild -D

3. Add config for esbuild in lib/config/config.json

config-esbuild.json
{
  "sourceMap": true,
  "sourcesContent": true,
  "target": "es2020",
  "keepNames": true,
  "stack": {
    "dev": {
      "minify": false
    },
    "prod": {
      "minify": true
    }
  }
}
Back to blog