0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Notionに問い合わせフォームを繋ごうとして、僕たちがぶつかった「地味だけど深い壁」の話

0
Posted at

Notionに問い合わせフォームを繋ごうとして、僕たちがぶつかった「地味だけど深い壁」の話

Notion、めちゃくちゃ便利ですよね。

社内のドキュメントや議事録はもちろん、プロジェクトのタスク管理も、気づけば全部Notionに集まっている……というチームも多いのではないでしょうか。

そんなふうにNotionを使い倒していると、ある日ふと、誰もがこう思う瞬間があると思うんです。

「……あれ? 外部からの問い合わせも、直接Notionのデータベースに入ってきたら最高なんじゃないか?」

Googleフォームや問い合わせツールの回答をわざわざスプレッドシートからコピペする必要もなくなるし、Notionのカンバンボードで「未対応」「対応中」「完了」とステータスを動かすだけで、そのまま顧客対応フローが完成する。想像しただけでも最高です。

ところが。

これを実際に「日本の業務現場」でまともに動かそうとすると、地味だけど、めちゃくちゃ深い壁にいくつもぶつかるんですよね。

海外製の連携ツールを試してみても日本の住所入力に合わなかったり、APIの仕様変更で急に動かなくなったり、ちょっと同時に回答されただけでエラーを吐いてしまったり……。

今回は、僕たちがNext.jsとTypeScriptで「Notion直結のフォーム基盤」を本気で作ってみた過程で見えてきた、泥臭い落とし穴とその乗り越え方について、備忘録と思考の整理を兼ねてまとめてみたいと思います。

同じように「Notionでフォームを完結させたい!」と試行錯誤している方の、ちょっとしたヒントになれば嬉しいです。


1. まずはAPIの洗礼。「database_id」が消えた日

NotionのAPIを触ったことがある方なら、「データベースIDを指定してページ(レコード)を作る」という流れはお馴染みだと思います。

ところが、最新のNotion API(2025-09-03 バージョン以降)を触り始めた瞬間、いきなり最初の壁にぶつかりました。

なんと、これまで当たり前のように使っていた database_id ではなく、data_source_id という新しい概念への移行 が進んでいたのです。

// packages/notion-client/src/client.ts
import { Client } from "@notionhq/client";

export class NotionIntegrationClient {
  private client: Client;

  constructor(apiKey: string) {
    this.client = new Client({
      auth: apiKey,
      notionVersion: "2025-09-03", // 最新APIバージョン
    });
  }

  async getDataSourceSchema(dataSourceId: string) {
    // 従来の databases ではなく data_sources エンドポイントを参照
    const response = await this.client.request({
      path: `data_sources/${dataSourceId}`,
      method: "GET",
    });
    return response;
  }
}

「えっ、データベース指定じゃないの?」と一瞬戸惑うのですが、Notion全体の進化(データベースの枠を超えた柔軟なデータソース統合)を見据えた設計変更だとわかると、なるほどなと納得します。

とはいえ、ネット上の過去のチュートリアル記事通りに databases.retrieve() を叩いていると急に動かなくなったり型が合わなくなったりするので、まずは公式リファレンスのバージョン指定(notionVersion: "2025-09-03")をしっかり確認することが最初の一歩でした。


2. 「3リクエスト/秒」の壁。同時送信でデータが虚空に消える問題

フォームが完成し、いざテスト送信をしてみると、次にぶつかったのが**Notion APIのレート制限(Rate Limits)**でした。

Notion APIには、公式ドキュメントで**「平均3リクエスト/秒」**というレート制限が設定されています。

通常の個人利用なら1秒に3回もAPIを叩くことはまずありません。しかし「Webフォーム」となると話は別です。
複数の人がたまたま同じタイミングでポチッと送信ボタンを押しただけで、あっという間に HTTP 429 Too Many Requests が返ってきてしまいます。

もしWebサーバーがNotion APIを直接同期フェッチしていたらどうなるでしょうか?
ユーザーの画面には「送信エラー」が表示され、最悪の場合、大切な問い合わせや応募データがどこにも保存されずに虚空へと消え去ってしまいます。これは業務ツールとして絶対に避けたい事態です。

解決策:非同期キューと指数バックオフ

そこで僕たちは、フォーム送信を受け取った瞬間にNotion APIを叩くのをやめ、「非同期キュー」を1枚挟むアーキテクチャに変更しました。

[ユーザーの送信] 
   │
   ▼
[Next.js API Route] ──(200 OKを即時返却)──> ユーザーは待たずに完了画面へ
   │
   ▼ キューへ投入(Upstash QStash / Workers Queue)
[非同期ワーカー]
   │
   ├── (Notion API を呼び出し)
   └── (もし429エラーなら、指数バックオフで数秒後に安全に再試行)

コードのイメージとしてはこのような形です:

// apps/web/src/app/api/queue/notion-worker/route.ts
export async function POST(req: NextRequest) {
  const { submissionData, retryCount = 0 } = await req.json();

  try {
    // Notion APIへ安全に転送
    await pushToNotionDataSource(submissionData);
    return NextResponse.json({ success: true });
  } catch (error: any) {
    if (error?.status === 429 && retryCount < 5) {
      // 指数バックオフ(1秒、2秒、4秒…)で再試行をスケジュール
      const delaySec = Math.pow(2, retryCount);
      console.warn(`429検知: ${delaySec}秒後に再試行します (試行回数: ${retryCount + 1})`);
      
      await scheduleRetry(submissionData, retryCount + 1, delaySec);
      return NextResponse.json({ status: "retry_scheduled" });
    }
    throw error;
  }
}

これを入れたことで、瞬間的にアクセスが集中してもキューがバッファとなって順番にNotionへ流し込んでくれるようになり、データの取りこぼしがゼロになりました。


3. 日本の現場で海外製ツールを使うと起きる「住所とフリガナの悲劇」

技術的なAPIの壁をクリアしたあとに待っていたのが、**「日本のWebフォーム特有の作法」**という、UI/UXの深い壁でした。

海外製の素晴らしいNotionフォーム作成ツールは世界中にたくさんあります。しかし、実際に日本のビジネスや現場に導入しようとすると、社内やユーザーから決まってこんな声が上がるんですよね。

「郵便番号を入れたのに住所が自動で出ないの、地味に不便だよね…」
「名前のフリガナ欄がないから、顧客管理で読み仮名が分からない」
「全角数字で入力したらエラーで弾かれた。イライラする…」

日本のユーザーは、普段Amazonや各種ECサイトで「郵便番号7桁を入れたら一瞬で町名まで補完される」という極上の入力体験に慣れ親しんでいます。
そのため、住所を手打ちさせられるだけで離脱率が跳ね上がってしまうのです。

オープンな zipcloud API と自動正規化の組み込み

この問題を解決するため、クライアント側の入力補助として「郵便番号からの住所自動補完」と「全角半角の自動正規化」を組み込みました。

// 郵便番号7桁が入力されたら即座に非同期補完
async function fetchAddressByPostalCode(code: string) {
  const cleanCode = code.replace(/[^\d]/g, ""); // ハイフンや全角を除去
  if (cleanCode.length !== 7) return null;

  try {
    const res = await fetch(`https://zipcloud.ibsnet.co.jp/api/search?zipcode=${cleanCode}`);
    const data = await res.json();
    if (data.results && data.results[0]) {
      const { address1, address2, address3 } = data.results[0];
      return `${address1}${address2}${address3}`;
    }
  } catch (e) {
    console.warn("住所補完に失敗しました:", e);
  }
  return null;
}

Notion側には「都道府県」「市区町村」「番地・建物名」をまとめた綺麗な文字列で流し込みつつ、Formula 2.0でハイフンを正規化する。
たったこれだけのことですが、フォームの送信完了率と「使いやすさ」の印象が劇的に変わりました。


4. セキュリティの葛藤。「個人情報を自前DBに持ちたくない」

もうひとつ、開発中にずっと議論していたのが**「セキュリティとプライバシー」**の設計でした。

フォームに入力されるデータには、名前、メールアドレス、電話番号、相談内容など、大切な個人情報が含まれます。
一般的なWebサービスであれば自社のPostgreSQLやMySQLに保存するところですが、個人開発や小さなチームでそれをやると、

  • 万が一のデータベース漏洩リスクに怯え続けることになる
  • バックアップや暗号化、アクセスログ管理の保守コストが重すぎる
  • サービス終了時にユーザーデータをどう破棄するかという法的責任

という、重たい課題がのしかかってきます。

辿り着いた「ゼロデータ保持(Zero Retention)」という答え

そこで僕たちは、「回答データを自社サーバーのDBに一切永続保持しない」 という方針をとることにしました。

ユーザーから送られてきた回答は、メモリ上で暗号化されたままNotion APIへと直接リレーされ、Notionへの格納が確認された瞬間にサーバー上から消去されます。

データが保管される場所は、「ユーザー自身が契約・管理しているNotionワークスペース」ただ1箇所だけ。
これなら、僕たちのサーバーが万が一攻撃されたとしても流出する回答データ自体が存在しませんし、ユーザー自身も自社のセキュリティポリシーの中でデータを完全にコントロールできます。


5. 終わりに:道具とどう付き合うか

「Notionにフォームを繋ぐ」

言葉にするとたった一行ですが、実際に現場で気持ちよく動くものを作ろうとすると、APIの仕様変更、レート制限の回避、日本の入力作法への配慮、そしてセキュリティの設計思想まで、驚くほどたくさんの学びがありました。

便利なツールをただ組み合わせるだけでなく、**「データを受け取る現場のメンバー」と「入力してくれる顧客や応募者」**の両方がストレスなく幸せになれる境界線を丁寧に探ること。
それが、道具を作る上で一番大切なことなのかもしれません。

もし同じようにNotionを活用して業務改善やフォーム連携に挑戦している方がいれば、この記事が少しでもお役に立てれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?