Hono でお問い合わせ API を作ろう!
この記事は Hono Advent Calendar 2023 の 23日目の記事です。
サイトを作成するうえでお問い合わせフォームからは逃れられないものです。
デザインを凝りたい、コンバージョンを計測したいなどを加味すると Google Sheets・CRM・MAツールのフォームを埋め込むパターンだと要望に答えることが難しくなります。
SaaS を導入したはいいものの、要望が膨らんでくると SaaS では対応できなくなり、 API を作ることになるのはあるあるだと思います。
API を作るのに今何が一番良さそうなんだろう…そこで Hono!
今回は Hono で API (API Gateway + Lambda) を作って 楽だったこと、便利だったことを共有したいと思います!
※掲載したコードはサンプルになります。バージョンは v3.11.9 時点のものになります。うまく動かない場合ご了承ください。
Valibot にも対応しているので楽!
Hono では middleware として Valibot を利用できます。
今回はバリデーションエラーになった内容もログに残しておきたい、バリデーションが成功・失敗した際に Slack に通知するようにしたいという要望があったので自分で上のものを参考にバリデーターを作って対応しました。
ほぼ Zod なのと、フロントで使う際には Valibot のほうが軽量なので Zod で対応できる複雑なものが必要ないのであればこれでいいのではないかなと思っています。
React Hook Form の例
import { SubmitHandler, useForm } from 'react-hook-form';
import { valibotResolver } from '@hookform/resolvers/valibot';
// React Hook Form
const send: SubmitHandler<スキーマの型> = useCallback(
async (data) => {
// 送信処理
},
[],
);
const {
register,
handleSubmit,
formState: { errors: formatError, isSubmitting },
} = useForm<スキーマの型>({
resolver: valibotResolver(スキーマ),
});
// tsx
return (
<>
<form method={'post'} onSubmit={handleSubmit(send)}>
{/* フォームの内容 */}
</form>
</>
)
個人的にスキーマを共有するだけなのでめちゃくちゃ楽だと思っています!
app.use の '*' が便利!
'*' ですべての処理の前に何かしら噛ませることができます。
不正な origin からのアクセスがあった場合エラーにしたい
app.use('*', async (c, next) => {
const origin = c.req.header('origin');
// list は origin の配列
const notAllowed = !origin || !list.some((l) => origin.includes(l));
if (notAllowed && env !== 'test') {
const error = new Error(`origin is not allowed : ${origin}`);
captureException(error);
throw error;
}
await next();
});
ログにとりあえずIPアドレスを出したい(AWS)
app.use('*', async (c, next) => {
const path = c.req.path;
// AWS用
if (c.env?.requestContext) {
log.info('start', env, path, c.env.requestContext.identity.sourceIp, c.env.requestContext.identity.userAgent);
} else {
log.info('start', env, path);
}
await next();
if (c.error) {
captureException(c.error);
return;
}
log.info('complete', env, path);
})
テストが楽!
Hono では 以下のような感じでテストが書けます。
test('POST /posts', async () => {
const res = await app.request('/posts', {
method: 'POST',
})
expect(res.status).toBe(201)
expect(res.headers.get('X-Custom')).toBe('Thank you')
expect(await res.json()).toEqual({
message: 'Created',
})
})
テストコードの例
mail、Salesforce、Slack に送信するものをそれぞれモックしたかったので以下のようなものを作成し、注入するようにしました。
export const createApp = ({
mailClient,
salesforceClient,
slackClient,
}: {
mailClient: MailClientInterface;
slackClient: SlackClientInterface;
salesforceClient: SalesforceClientInterface;
}) => {
const env = getEnv();
const app = new Hono<{ Bindings: Bindings }>();
// app.post など
return app
};
テストコードで呼び出し、モックを注入する
describe('catch errors', () => {
const mailFailureApp = createApp({
mailClient: mailClients.failure,
salesforceClient: salesforceClients.failure,
slackClient: slackClients.failure,
});
const salesforceFailureApp = createApp({
mailClient: mailClients.success,
salesforceClient: salesforceClients.failure,
slackClient: slackClients.failure,
});
it('Catch SES error', async () => {
const res = await mailFailureApp.request(お問い合わせのエンドポイント, {
method: 'POST',
body: JSON.stringify({
// 内容
}),
});
expect(res.status).toBe(500);
expect((await res.json()).errorName).toBe('SesUnknownError');
});
it('Catch Salesforce error', async () => {
const res = await salesforceFailureApp.request(, {
method: 'POST',
body: JSON.stringify({
// 内容
}),
});
expect(res.status).toBe(500);
expect((await res.json()).errorName).toBe('SalesforceUnknownError');
});
}
おわりに
お問い合わせフォームでよくある問題として、ユーザーがフォームを送信したしてないがあると思います。
今のところ幸いなことに何も問題が起こってはいませんが、フォームを送信した際に Slack に通知、ログを集計、エラーがあったら Sentry で通知するなどの対応を入れたことにより、何かしら問題が発生した場合にも対応しやすくなり安全な状況になったかなと思います。
あまり時間が掛けられない中、個人的にはスピーディーに実装できたのは Hono の API がわかりやすいのと、ドキュメントが充実していること、コミュニティが活発(X で分からないところを解決していただいた watany さんありがとうございます!)なことかなと思っています。
個人開発で TS を用いて API 作る場合には Hono + Cloudflare Workers が今のところいい感じなのでこれからも Hono を使っていきたいなと思っています。 V4 では island 対応も視野に入れているらしいので今から楽しみです!
🔥Hono に感謝🔥
Discussion