WEBサイトやアプリケーションを構築していると、必ずと言っていいほど「お問い合わせフォーム」なるものがございます。
ユーザーが入力した内容を、運用している自社(自分の)メールサーバーに飛ばす…っていう、超オーソドックスなあれです。
ここ最近リリースしたWEBサイトにも、例のごとく「お問い合わせフォーム」は実装しているのですが。
メールサーバー、契約したくなかったんですよね…ケチだから…。
何か慣れ親しんだツールで、かつ、無料で問い合わせ内容をキャッチできないだろうか?
ついでに、業務でも活用できる方法がいいな、ということで。
我がWEBサイトのフォームは、全部Slackが受け止めてくれているのです。
全体の構成
フォームの内容をSlackに流し込む「受け皿」を作ることにしたわけですが、簡単に構成を紹介します。
各サイトのお問い合わせフォーム
│ fetch で POST
▼
自分で立てた APIサーバー(Node.js + Express)
│ Slack Webhook へ転送
▼
Slack のチャンネルに通知が届く
ポイントは、フォームが直接Slackを叩くのではなく、あいだにAPIサーバーを一枚かませているってところです。
なぜAPIサーバーを挟むのか?
URLの秘匿
Slackに送るためには「Webhook URL」という、いわば鍵のようなものが必要になります。
これをフロント側(=ブラウザで誰でも見られる場所)に書いてしまうと、URLを盗まれて悪用され兼ねません。
そこで、URLはサーバー側に隠しておき、フォームからはそのサーバーにだけ送信する―――この形に落ち着きました。
問い合わせの一元管理
そしてこの構成の何がいいって、複数サイトの問い合わせを一つのアプリでまとめて受けられるってところなんですよね。
Slackならチャンネルで切り分けることもできるし、通知のオン・オフもしやすい。
結局Slackが有能ってだけの話なんだよな…。
メールサーバーで代用する場合
実際、これをメールでやろうとすると地味~に手間なんです。
送信ソフト(Postfix等)を入れて設定し、VPSで塞がれがちな25番ポートの解除を申請し、さらに迷惑メール対策(SPF / DKIM / DMARC)まで整える……。
こういった一連の作業が必要で、「借りれば勝手に使える」類のものではありません。
その煩わしさをまるっと飛ばせるのが、Slackを採用するいちばんのうまみでしたね。
事前準備
さて、構築に際して私が準備しておいたアプリケーションやインフラまわりはこちらです。
Slack側でWebhook URLを用意する
Webhook URLの発行については、以下の記事でわかりやすく解説しております!
メッセージを飛ばしたいチャンネルを作ったら、こちらの手順でURLを取得しましょう。

ディレクトリ・設定ファイルの作成
作業用のフォルダを作って、必要なものを入れます。
今回はapi-serverというディレクトリで作業しました。
mkdir api-server
cd api-server
npm init -y
npm install express dotenv
package.json を用意する
npm init -y を実行することで、プロジェクトの「設計図」にあたる package.json が自動で作成されます。
また、後続の npm install express dotenv で、ひな型となる package.json に dependencies の欄が追加されるのです。
ここまではコマンド実行で勝手に書き込んでくれるのですが、以降は自分で編集しておきましょう。
最終的に、package.json はこうなりました。
{
"name": "api-server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"dotenv": "^16.0.0",
"express": "^4.18.0"
}
}
ポイントだけ補足しておきます。
.env に隠したい値をまとめる
APIサーバーが使う値は、.env に隠しておきましょう。
先ほど発行したWebhook URLもここに書きます。
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXX/YYYY/ZZZZ
PORT=3001
.env ファイルというと数十行あるイメージをしてしまいますが、今回は2行でOK。server.js の冒頭にある import 'dotenv/config' が、この .env を読み込んでくれます。
PORTを指定している理由ですが、正直、server.js で process.env.PORT || 3001 としているので、今思えば書く必要はなかったのかな~と思いました。
まぁいつか突然ポートを変えたい日が来るかもしれませんしね、そのハンドリングのためとこじつけて次に行きましょう。
前編のまとめ
これでSlack側の準備と、開発環境の用意ができました。
後編ではいよいよ、APIサーバー本体 server.js を構築していきます。
「今すぐ教えろ!」って方向けに、先んじて全体像を載せておきますので、コピペ&適宜変更してご自由に使ってくださいませ。
ブロックごとに何をしているのかは、後編で詳しく解説いたします。
import 'dotenv/config'
import express from 'express'
const app = express()
const PORT = process.env.PORT || 3001
app.use(express.json())
const ALLOWED_ORIGINS = [
'https://gradient-generator.hibitsuiki.com',
'http://localhost:5173',
'https://extension-quiz.hibitsuiki.com',
'http://localhost:8080',
]
app.use((req, res, next) => {
const origin = req.headers.origin
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
}
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
if (req.method === 'OPTIONS') return res.sendStatus(204)
next()
})
async function notifyError(text) {
try {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
blocks: [{
type: 'section',
text: { type: 'mrkdwn', text: `🚨 *APIサーバーエラー*\n\`\`\`${text}\`\`\`` },
}],
}),
})
} catch { /* Slack自体が落ちている場合は無視 */ }
}
process.on('uncaughtException', async (err) => { await notifyError(`uncaughtException: ${err.message}`); process.exit(1) })
process.on('unhandledRejection', async (reason) => { await notifyError(`unhandledRejection: ${reason}`) })
app.post('/form', async (req, res) => {
const { source, name, email, type, message } = req.body
if (!message) return res.status(400).json({ error: 'message is required' })
const blocks = [
{
type: 'section',
text: { type: 'mrkdwn', text: `📬 *【お問い合わせ】${source ?? ''}*` },
},
{ type: 'divider' },
{
type: 'section',
text: {
type: 'mrkdwn',
text: [
`📂 カテゴリ:${type || '(未選択)'}`,
`👤 お名前:${name ?? '(未入力)'}`,
`✉️ メールアドレス:${email ?? '(未入力)'}`,
].join('\n'),
},
},
{
type: 'section',
text: { type: 'mrkdwn', text: `🗨️ お問い合わせ内容\n${message}` },
},
]
try {
const slackRes = await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blocks }),
})
if (!slackRes.ok) {
const errText = await slackRes.text()
console.error('Slack error:', errText)
return res.status(502).json({ error: 'slack error' })
}
res.json({ ok: true })
} catch (err) {
console.error(err)
res.status(500).json({ error: 'internal error' })
}
})
app.listen(PORT, () => {
console.log(`API server running on port ${PORT}`)
})
ではまた次回!


