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!
というレスポンスを返却します。
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
も合わせて変更しておきます。
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株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion