🔥

Hono + Lamba入門

に公開

はじめに

Honoはサーバレス環境で利用できるJavaScriptのバックエンドフレームワークです。
本記事ではHonoをLambdaにデプロイし、基本的な実装を確認します。

およそ30分ほどで実施できる内容となっています。
なお、LambdaデプロイのためにCDKを利用しますが、CDKの説明は本記事には含めておりません。実施時間の多くはCDKのデプロイにかかるため、変更をまとめてデプロイすることを推奨します。

Lambdaにデプロイ

公式ドキュメントに沿って進めていきます。CDK定義によってデプロイしていきます。

フォルダの作成

まずはHonoアプリ用フォルダを作成します。
フォルダ名がCDKのスタック名に反映されるため、すでにMyAppStackが存在する場合は別の名前に行置き換えます。

mkdir my-app
cd my-app

続いて、CDKプロジェクトを作成します。
CDKコマンドの初期設定を行ってない場合、先に実行します。(参考

npm install -g aws-cdk # 設定済みの場合は不要
cdk init app -l typescript

npmライブラリのインストールを行います。

npm i hono
npm i -D esbuild

Lambda関数用フォルダを作成します。

mkdir lambda
touch lambda/index.ts

Honoの定義

lambda/index.tsにHonoのGETメソッドのAPIを追加します。このAPIではHello Hono!というレスポンスを返却します。

lambda/index.ts
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export const handler = handle(app)

AWS CDKの定義

lib/my-app-stack.tsが既に存在しているため、次の内容に書き換え、Lambdaの定義を追加します。
フォルダ名をmy-appから変更した場合、クラス名MyAppStackも合わせて変更しておきます。

lib/my-app-stack.ts
import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'

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

    const fn = new NodejsFunction(this, 'lambda', {
      entry: 'lambda/index.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_22_X,
    })
    const fnUrl = fn.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE,
    })
    new cdk.CfnOutput(this, 'lambdaUrl', {
      value: fnUrl.url!
    })
  }
}

AWS CDKのデプロイ

CDKのデプロイコマンドを実行します。

cdk deploy

デプロイ内容の確認が表示されるため、yと入力します。

Do you wish to deploy these changes (y/n)?

成功すると、下記のようにLambdaデプロイ先のURLが表示されます。

 ✅  MyAppStack

✨  Deployment time: 45.46s

Outputs:
MyAppStack.lambdaUrl = https://{エンドポイントURL}

以降、変更反映のたびにCDKデプロイが必要です。

挙動確認

CDKにてLambda関数URLを設定しているので、curlによって挙動確認できます。

curl https://{エンドポイントURL}
# 結果
Hello Hono!

なお、コンソール上でレスポンスの末尾に%が付く場合がありますが、これはシェルプロンプトが同じ行に表示されていることが原因で、レスポンスには含まれていません。
メッセージの末尾に改行(\n)を追加することで修正することが可能です。

app.get('/', (c) => c.text('Hello Hono!\n'))

以降は、実際の業務でよく使われる要素を中心に進めていきます。

リクエストパラメータの取得

リクエストに含まれるパラメータを参照し、返却します。
以降は先程追加したapp.get('/', (c) => c.text('Hello Hono!'))の下に追加していきます。

ルートパラメータの参照

URLに含まれるパラメータnameを取得して返却します。

app.get('/hello/:name', (c) => {
  const name = c.req.param('name')
  return c.text(`Hello, ${name}!`)
})

挙動確認用のcurlコマンドは次のとおりです。

curl https://{エンドポイントURL}/hello/Taro
# 結果
Hello, Taro!

クエリパラメータの参照

クエリに含まれるパラメータqを取得して返却します。

app.get('/search', (c) => {
  const keyword = c.req.query('q') || 'nothing'
  return c.text(`You searched for: ${keyword}`)
})

挙動確認用のcurlコマンドは次のとおりです。

curl "https://{エンドポイントURL}/search?q=keyword"
# 結果
You searched for: keyword

JSONレスポンスの設定

これまではテキストを返却していましたが、JSONとして返却します。

app.get('/user', (c) => {
  const user = {
    id: 1,
    name: 'Taro',
    email: 'taro@example.com'
  }
  return c.json(user)
})

挙動確認用のcurlコマンドは次のとおりです。

curl https://{エンドポイントURL}/api/user
# 結果
{"id":1,"name":"Taro","email":"taro@example.com"}

GET以外のメソッドの追加

POST

app.post('/message', async (c) => {
  const body = await c.req.json()
  const message = body.message || 'No message'

  return c.json({
    received: message,
    status: 'ok'
  }, 201)
})

挙動確認用のcurlコマンドは次のとおりです。

curl -X POST https://{エンドポイントURL}/message \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello from client!"}'
# 結果
{"received":"Hello from client!","status":"ok"}

PUT

app.put('/message', async (c) => {
  const body = await c.req.json()
  const message = body.message || 'No message'

  return c.json({
    updated: message,
    status: 'ok'
  }, 200)
})

挙動確認用のcurlコマンドは次のとおりです。

curl -X PUT https://{エンドポイントURL}/message \
  -H "Content-Type: application/json" \
  -d '{"message": "Updated message"}'
# 結果
{"updated":"Updated message","status":"ok"}

DELETE

app.delete('/message', async (c) => {
  const body = await c.req.json()
  const message = body.message || 'No message'

  return c.json({
    deleted: message,
    status: 'ok'
  }, 200)
})

挙動確認用のcurlコマンドは次のとおりです。

curl -X DELETE https://{エンドポイントURL}/message \
  -H "Content-Type: application/json" \
  -d '{"message": "Message to delete"}'
# 結果
{"deleted":"Message to delete","status":"ok"}

ミドルウェアの設定

ミドルウェアを追加することで、APIの共通処理を設定することができます。

エラーハンドリングの追加

エラーが起こったときに500レスポンスを返却する処理を追加します。
app.useの第1引数の*は、すべてのAPIへの適用を指します。

app.use('*', async (c, next) => {
  try {
    await next()
  } catch (err) {
    console.error('Error:', err)
    return c.json({ error: 'Internal Server Error' }, 500)
  }
})

意図的にエラーを起こすAPIを定義します。

app.get('/error', (c) => {
  throw new Error()
})

挙動確認

エラーメッセージが返却されるか確認します。

curl https://{エンドポイントURL}/error
# 結果
Internal Server Error

認証の追加

あらかじめ用意されているBearer認証ミドルウェアを追加します。
これにより、/auth配下のAPIはヘッダーに認証情報を含めることが必要となります。
認証情報は環境変数としての保持が適切ですが、本記事においては直接指定します。

// 中略
import { bearerAuth } from 'hono/bearer-auth'

// 中略
app.use(
  '/auth/*',
  bearerAuth({
    token: 'honoiscool',
  })
)

/auth配下にAPIを追加します。

app.get('/auth', (c) => {
  return c.text('You are authorized')
})

挙動確認

認証情報を追加してcurlでアクセスし、認証が通ることを確認します。

curl https://{エンドポイントURL}/auth \
  -H 'Authorization: Bearer honoiscool'
# 結果
You are authorized

認証情報が未指定の場合はエラーになる挙動も確認します。

curl https://{エンドポイントURL}/auth
# 結果
Unauthorized

バリデーションの追加

Honoが推奨しているZodを使い、リクエストパラメータを検証します。

インストール

npm i zod @hono/zod-validator

API追加

messageパラメータが空の場合はバリデーションエラーとするAPIを追加します。

// 中略
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const messageSchema = z.object({
  message: z.string().min(1)
})

// 中略
app.post(
  '/validation',
  zValidator('json', messageSchema),
  async (c) => {
    const data = c.req.valid('json')
    return c.json({ received: data.message, status: 'ok' }, 201)
  }
)

挙動確認

バリデーションが通るリクエストを送信して、レスポンスを確認します。

curl -X POST https://{エンドポイントURL}/validation \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello from curl"}'
# 結果
{"received":"Hello from curl","status":"ok"}

バリデーションエラーになるリクエストの挙動も確認します。

curl -X POST https://{エンドポイントURL}/validation \
  -H "Content-Type: application/json" \
  -d '{"message": ""}'
# 結果
{"success":false,"error":{"name":"ZodError","message":"[\n  {\n    \"origin\": \"string\",\n    \"code\": \"too_small\",\n    \"minimum\": 1,\n    \"inclusive\": true,\n    \"path\": [\n      \"message\"\n    ],\n    \"message\": \"Too small: expected string to have >=1 characters\"\n  }\n]"}

クリーンアップ

以上でHono + Lamba入門は終了です。最後にクリーンアップをします。

cdk destroy

削除の確認が表示されるため、yと入力したらクリーンアップ完了です。

Are you sure you want to delete: MyAppStack (y/n)?
NCDCエンジニアブログ

Discussion