Next.jsとServerless Functions、blastengineを使ってお問い合わせフォームを作る

Next.jsはReactをフロントエンドに利用したアプリケーションフレームワークです。Vercel向けにアプリケーションを開発する際によく使われています。

今回はそんなNext.jsとServerless Functions、blastengineを使ってお問い合わせフォームを実装します。

## コード

今回のコードは https://github.com/blastengineMania/nextjs-contact にアップしてあります。実装時の参考にしてください。

## 実装について

### フロントエンド

フロントエンドはNext.jsで実装します。入力項目は以下の通りです。

| 項目 | 型 | 変数名 |
| --- | --- | --- |
| 会社名 | テキスト | company |
| 名前 | テキスト | name |
| メールアドレス | テキスト | mailaddress |
| 問い合わせ種別 | 選択 | type |
| 問い合わせ内容 | テキスト | body |

コードは以下のようになります。

### ステートの準備

今回は以下のステートを用意しています。

```tsx
// フォーム入力データの型
type formData = {
  company: string;
  name: string;
  email: string;
  type: string;
  message: string;
};

// フォームの選択肢の型
type SelectOption = {
  label: string;
  value: string;
};

// フォームの初期値
const defaultValue: formData = {
  company: '',
  name: '',
  email: '',
  type: '',
  message: '',
};

const options: SelectOption[] = [
  { label: 'お問い合わせ', value: 'お問い合わせ' },
  { label: 'ご意見', value: 'ご意見' },
  { label: 'その他', value: 'その他' },
];

const [form, setForm] = useState(defaultValue);
```

### 選択項目の表示

選択項目は `options` で、動的に生成しています。

```html
<div className="block type">
	<label htmlFor="frm-type">お問い合わせ種別</label>
	<select
		onChange={(e) =>
			setForm({ ...form, ...{ type: e.target.value } })
		}
	>
		{options.map((option) => (
			<option value={option.value}>{option.label}</option>
		))}
	</select>
</div>

```

### フォームについて

フォームは以下のようになります。入力されたら、その値をフォームに反映しています。ボタンやフォームの送信時には `send` 関数を呼んでいます。

```html
<>
	<div className={styles.container}>
		<Head>
			<title>お問い合わせ</title>
		</Head>

		<main className={styles.main}>
			<h3 className={styles.title}>お問い合わせ</h3>
			<form className="container" onSubmit={send}>
				<div className="company block">
					<label htmlFor="frm-company">会社名</label>
					<input
						id="frm-company"
						type="text"
						value={form.company}
						autoComplete="company"
						onChange={(e) =>
							setForm({ ...form, ...{ company: e.target.value } })
						}
						required
					/>
				</div>
				<div className="account block">
					<label htmlFor="frm-name">名前</label>
					<input
						id="frm-name"
						type="text"
						value={form.name}
						autoComplete="name"
						onChange={(e) =>
							setForm({ ...form, ...{ name: e.target.value } })
						}
						required
					/>
				</div>
				<div className="email block">
					<label htmlFor="frm-email">メールアドレス</label>
					<input
						id="frm-email"
						type="email"
						value={form.email}
						autoComplete="email"
						onChange={(e) =>
							setForm({ ...form, ...{ email: e.target.value } })
						}
						required
					/>
				</div>
				<div className="block type">
					<label htmlFor="frm-type">お問い合わせ種別</label>
					<select
						onChange={(e) =>
							setForm({ ...form, ...{ type: e.target.value } })
						}
					>
						{options.map((option) => (
							<option value={option.value}>{option.label}</option>
						))}
					</select>
				</div>
				<div className="message block">
					<label htmlFor="frm-message">Message</label>
					<textarea
						id="frm-message"
						rows={6}
						value={form.message}
						onChange={(e) =>
							setForm({ ...form, ...{ message: e.target.value } })
						}
					>
					</textarea>
				</div>
				<button onClick={send}>送信</button>
			</form>
		</main>
	</div>
</>

```

また、CSS（global.css）は以下のようになります。

```css
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  background: #1e1e1e;
  min-height: 100vh;
  display: flex;
  color: rgb(243, 241, 239);
  justify-content: center;
  align-items: center;
}

.block {
  display: flex;
  flex-direction: column;
}

.name {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

.container {
  font-size: 1.3rem;
  border-radius: 10px;
  width: 85%;
  padding: 50px;
  box-shadow: 0 54px 55px rgb(78 78 78 / 25%), 0 -12px 30px rgb(78 78 78 / 25%),
    0 4px 6px rgb(78 78 78 / 25%), 0 12px 13px rgb(78 78 78 / 25%),
    0 -3px 5px rgb(78 78 78 / 25%);
}

.container input {
  font-size: 1.2rem;
  margin: 10px 0 10px 0px;
  border-color: rgb(31, 28, 28);
  padding: 10px;
  border-radius: 5px;
  background-color: #e8f0fe;
}

.container select {
  font-size: 1.2rem;
  margin: 10px 0 10px 0px;
  border-color: rgb(31, 28, 28);
  padding: 10px;
  border-radius: 5px;
  background-color: #e8f0fe;
}

.container textarea {
  margin: 10px 0 10px 0px;
  padding: 5px;
  border-color: rgb(31, 28, 28);
  border-radius: 5px;
  background-color: #e8f0fe;
  font-size: 20px;
}

.container h1 {
  text-align: center;
  font-weight: 600;
}

.name div {
  display: flex;
  flex-direction: column;
}

.block button {
  padding: 10px;
  font-size: 20px;
  width: 30%;
  border: 3px solid black;
  border-radius: 5px;
}

.button {
  display: flex;
  align-items: center;
}

textarea {
  resize: none;
}

```

![Untitled.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/27e3ea4f-a2a7-97ef-7435-2a8cd1c27ef0.jpeg)


### 送信した際の処理

送信ボタンを押した際の処理として、入力情報を `POST /api/blastengine` に送信します。

```tsx
// フォームの送信処理
const send = async (e: any) => {
  e.preventDefault();
  fetch('/api/blastengine', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(form),
  });
};
```

## Serverless Functions

まず `.env` ファイルを作成して、以下の2つの環境変数を定義します。

| 環境設定名 | 値 |
| --- | --- |
| BLASTENGINE_USER_ID | 先ほど作成したAPIユーザー名 |
| BLASTENGINE_API_KEY | 先ほど作成したAPIキー |

### コードの作成

### ライブラリの追加

プロジェクトへ、blastengine SDKを追加します。

```tsx
npm i blastengine
```

### 基本形

コードは `/pages/api/blastengine.ts` として作成します。基本形は以下の通りです。

```tsx
import type { NextApiRequest, NextApiResponse } from 'next'
import { BlastEngine, Mail } from 'blastengine';

type Data = {
  delivery_id: number
}

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse<Data>
  ) {
	// この中に処理を書く
}
```

### SDKの初期化

読み込んだblastengineのSDKを初期化します。

```tsx
new BlastEngine(process.env.BLASTENGINE_USER_ID!, process.env.BLASTENGINE_API_KEY!);
```

### 件名・本文を用意する

続けてメール送信する内容を準備します。

```tsx
const { body } = req;
const mail = new Mail();
const text = `__USERNAME__様

お問い合わせいただきありがとうございます。内容を確認し、追ってご連絡いたします。

会社名：
__COMPANY__
お名前：
__USERNAME__
お問い合わせ内容：
__MESSAGE__
`;
mail
  .setFrom('info@blastengine.jp', '管理者')
  .setSubject('お問い合わせありがとうございます')
  .addTo(body.email, {
		USERNAME: body.name,
		COMPANY: body.company,
		MESSAGE: body.message
	})
  .setText(text)
  .setEncode('UTF-8');
```

最後にメールを送信します。レスポンスとして、blastengineから返却される配信ID（デリバリーID）を返します。

```tsx
await mail.send();
res.status(200).json({ delivery_id: mail.deliveryId! });
```

スクリプト全体は以下のようになります。

```tsx
import type { NextApiRequest, NextApiResponse } from 'next'
import { BlastEngine, Mail } from 'blastengine';

type Data = {
  delivery_id: number
}

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse<Data>
  ) {
  new BlastEngine(process.env.BLASTENGINE_USER_ID!, process.env.BLASTENGINE_API_KEY!);
  const { body } = req;
  const mail = new Mail();
  const text = `__USERNAME__様

お問い合わせいただきありがとうございます。内容を確認し、追ってご連絡いたします。

会社名：
__COMPANY__
お名前：
__USERNAME__
お問い合わせ内容：
__MESSAGE__
`;
  mail
    .setFrom('info@blastengine.jp', '管理者')
    .setSubject('お問い合わせありがとうございます')
    .addTo(body.email, { USERNAME: body.name, COMPANY: body.company, MESSAGE: body.message })
    .setText(text)
    .setEncode('UTF-8');
  await mail.send();
  res.status(200).json({ delivery_id: mail.deliveryId! });
}
```

![uploading...0]()


## まとめ

今回はNext.jsとServerless Functionsを使って、お問い合わせフォームを作成しました。フロントエンドだけでなく、API実行部分もNext.js内で使えるのが便利です。

[エンジニア向けメール配信システム「ブラストエンジン（blastengine）」](https://blastengine.jp/)