【Netlify】Functionsを使ってNotion APIと安全に連携する方法 - セットアップから実装まで
Notion をデータソースとして Web アプリを構築する場合、API トークンの扱いが問題になります。フロントエンドから直接 Notion API を呼ぶとトークンがブラウザに露出してしまうため、サーバー側にプロキシが必要です。
Netlify Functions を使えば、別途サーバーを用意せずにこの問題を解決できます。この記事では、Notion インテグレーションの作成から Netlify Functions による安全な API プロキシの構築までを解説します。
全体のアーキテクチャ
ブラウザからは /api/fetchNotion を叩くだけで、Notion のトークンを知る必要がありません。トークンは Netlify Functions のサーバー側でのみ使用されます。
なぜ直接呼び出してはいけないのか
- トークンはサーバー側の環境変数にのみ存在
- ブラウザの開発者ツールからトークンが見えない
- APIの呼び出し制御が可能
- トークンがJavaScriptに埋め込まれる
- 開発者ツールのネットワークタブから丸見え
- 第三者にデータベースを操作されるリスク
Vite プロジェクトで VITE_NOTION_TOKEN のように VITE_ プレフィックスを付けると、トークンがフロントエンドのバンドルに含まれてしまいます。Notion トークンの環境変数名には VITE_ を付けず、NOTION_TOKEN としてください。
Notion 側の準備
インテグレーションの作成
https://www.notion.so/profile/integrations にアクセスし、「新しいインテグレーション」を作成します。
作成後に表示される ntn_xxx... 形式のトークンを控えます。このトークンが Notion API の認証に使われます。
使用する Notion データベースを開き、右上の「…」→「コネクト」から作成したインテグレーションを追加します。
インテグレーションをデータベースに接続しないと、APIからアクセスできません。403エラーが返る場合はこの設定を確認してください。
2024年9月以降に作成されたインテグレーションのトークンは ntn_ プレフィックスで始まります。それ以前に作成されたトークン(secret_ プレフィックス)も引き続き使用できます。
データベースIDの取得
Notion データベースの URL からIDを取得します。
https://www.notion.so/ワークスペース名/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?v=...
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
この部分がデータベースID
データベースのプロパティ設計
Notion データベースには、アプリで必要なプロパティを設定します。例えばブログ用途の場合は以下のようになります。
| プロパティ名 | 型 | 用途 |
|---|---|---|
| Content | Title | 記事タイトル |
| Image | Files | サムネイル画像 |
| Date | Date | 公開日 |
| Tags | Multi-select | タグ分類 |
| Categories | Multi-select | カテゴリ分類 |
| Slug | Rich text | URLスラッグ |
Notion API のレスポンスでは properties.Content、properties.Tags のようにプロパティ名がそのままキーとして返されます。スペースや日本語も使えますが、コードでの扱いやすさを考慮して英語の命名がおすすめです。
プロジェクトのセットアップ
ディレクトリ構成
project-root/
├── netlify/
│ └── functions/
│ └── fetchNotion.ts ← Notion API プロキシ
├── netlify.toml
├── src/
│ └── services/
│ └── NotionData.ts ← フロントエンドからの呼び出し
├── .env ← 環境変数(gitignore対象)
└── package.json
netlify.toml の設定
/api/* へのリクエストを Netlify Functions にルーティングします。
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[dev]
targetPort = 5173
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
環境変数の設定
Notion 連携に必要な環境変数は2つです。
| 変数名 | 説明 | 例 |
|---|---|---|
NOTION_TOKEN | インテグレーショントークン | ntn_xxxxxxxxxxxxxxxx |
NOTION_DATABASE_ID | データベースID | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
ローカル開発用(.env ファイル)
プロジェクトルートに .env ファイルを作成します。
NOTION_TOKEN=ntn_xxxxxxxxxxxxxxxxxxxxxxxx
NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
.env ファイルは必ず .gitignore に含めてください。トークンがリポジトリにコミットされると、誰でもデータベースにアクセスできてしまいます。
環境変数名に VITE_ プレフィックスを付けないでください。VITE_ 付きの変数はビルド時にフロントエンドのコードにインライン展開され、ブラウザからトークンが見えてしまいます。Netlify Functions はサーバー側で実行されるため、VITE_ なしの変数名で安全にアクセスできます。
本番環境(Netlify ダッシュボード)
対象サイトの管理画面を開きます。
Site configuration → Environment variables に移動します。
NOTION_TOKEN と NOTION_DATABASE_ID を登録します。値はローカルの .env と同じものを設定します。
ローカルでも本番でも process.env.NOTION_TOKEN で参照できます。Netlify が環境に応じて .env またはダッシュボードの値を自動的に読み込みます。
Netlify Functions の実装
データベース取得関数
Notion API からデータベースの内容を取得するプロキシ関数を実装します。
// netlify/functions/fetchNotion.ts
export async function handler(event, context) {
const token = process.env.NOTION_TOKEN;
const databaseId = process.env.NOTION_DATABASE_ID;
if (!token || !databaseId) {
return {
statusCode: 500,
body: JSON.stringify({ error: "環境変数が設定されていません" }),
};
}
try {
const response = await fetch(
`https://api.notion.com/v1/databases/${databaseId}/query`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
const errorData = await response.json();
return {
statusCode: response.status,
body: JSON.stringify(errorData),
};
}
const data = await response.json();
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: "Notion APIリクエストに失敗しました" }),
};
}
}
Notion API のレスポンス構造
API から返されるデータは以下のような構造です。各プロパティの型に応じて値の取り出し方が異なります。
// レスポンスの results 配列の各要素
{
id: "page-id",
properties: {
Content: {
type: "title",
title: [{ plain_text: "記事タイトル" }]
},
Tags: {
type: "multi_select",
multi_select: [{ name: "JavaScript" }, { name: "React" }]
},
Date: {
type: "date",
date: { start: "2026-02-07" }
},
Slug: {
type: "rich_text",
rich_text: [{ plain_text: "my-article-slug" }]
}
}
}
プロパティの値を取り出すヘルパー
// レスポンスデータの整形例
function formatResults(results) {
return results.map((page) => ({
id: page.id,
title: page.properties.Content.title[0]?.plain_text || "",
tags: page.properties.Tags.multi_select.map((t) => t.name),
date: page.properties.Date.date?.start || "",
slug: page.properties.Slug.rich_text[0]?.plain_text || "",
image: page.properties.Image.files[0]?.file?.url || "",
}));
}
Notion API から返される画像URLには有効期限(約1時間)があります。長期間キャッシュする場合は、画像を別のストレージにコピーする必要があります。
フロントエンドからの呼び出し
フロントエンドからは Notion の存在を意識せず、自分のAPIとして呼び出すだけです。
// src/services/NotionData.ts
export async function fetchArticles() {
const response = await fetch("/api/fetchNotion");
if (!response.ok) {
throw new Error("データの取得に失敗しました");
}
const data = await response.json();
return data.results;
}
フロントエンドのコードにはトークンもデータベースIDも一切含まれません。
ローカル開発
Netlify CLI でのテスト
Notion 連携を含めたローカル開発には netlify dev を使います。
npx netlify dev
このコマンドで、フロントエンドの開発サーバーと Netlify Functions が同時に起動します。
| 対象 | URL |
|---|---|
| フロントエンド | http://localhost:8888 |
| Notion API プロキシ | http://localhost:8888/api/fetchNotion |
npm run dev はフロントエンドのみ起動するため、/api/fetchNotion にアクセスできません。Notion 連携をテストする場合は必ず npx netlify dev を使ってください。
動作確認
ブラウザで http://localhost:8888/api/fetchNotion に直接アクセスすると、Notion データベースの JSON レスポンスが確認できます。
Functions の拡張
Notion に対する操作を増やす場合は、netlify/functions/ にファイルを追加するだけです。
netlify/functions/
├── fetchNotion.ts → GET /api/fetchNotion(一覧取得)
├── getNotionPage.ts → GET /api/getNotionPage?id=xxx(個別取得)
└── searchNotion.ts → POST /api/searchNotion(検索)
個別ページの取得例を示します。
// netlify/functions/getNotionPage.ts
export async function handler(event, context) {
const pageId = event.queryStringParameters?.id;
if (!pageId) {
return {
statusCode: 400,
body: JSON.stringify({ error: "idパラメータが必要です" }),
};
}
const token = process.env.NOTION_TOKEN;
const response = await fetch(
`https://api.notion.com/v1/pages/${pageId}`,
{
headers: {
Authorization: `Bearer ${token}`,
"Notion-Version": "2022-06-28",
},
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
Notion API のレート制限
Notion API にはリクエストレート制限があります。
| 制限 | 値 |
|---|---|
| リクエスト上限 | 平均 3 リクエスト/秒 |
| レスポンスヘッダ | Retry-After で待機秒数を通知 |
429 エラー(Too Many Requests)が返された場合は、Retry-After ヘッダの値を待ってからリトライします。
// リトライ処理の例
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After")) || 1;
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
return response;
}
throw new Error("リトライ回数の上限に達しました");
}
無料枠の制限
Netlify の無料プランでの上限です。
| リソース | 月間上限 |
|---|---|
| Functions 呼び出し | 125,000 回 |
| Edge Functions 呼び出し | 1,000,000 回 |
| 帯域幅 | 100 GB |
| ビルド時間 | 300 分 |
月125,000回 ÷ 30日 = 1日あたり約4,000回。ページ表示ごとに1回の API 呼び出しなら、個人サイトには十分な量です。
無料枠を超えるとサイト全体が月末まで停止されます。従量課金ではないため、トラフィックが増える見込みがある場合は有料プラン(Starter: $8.50/月〜)を検討してください。
デプロイ
Netlify と Git リポジトリを連携していれば、git push するだけで自動的にデプロイされます。Functions も含めてまとめてビルドされるため、特別な設定は不要です。
デプロイ前に Netlify ダッシュボードの Environment variables に NOTION_TOKEN と NOTION_DATABASE_ID が設定済みであることを確認してください。
まとめ
- Notion 準備 インテグレーション作成 → データベースに接続 → トークン(ntn_xxx)とIDを取得する
- 環境変数 NOTION_TOKEN と NOTION_DATABASE_ID をローカルは .env、本番はダッシュボードに設定する
- Functions 実装 netlify/functions/ にプロキシ関数を作成し、トークンをサーバー側に隠す
- フロントエンド /api/fetchNotion を叩くだけでNotionのデータを取得できる
- セキュリティ トークンは絶対にフロントエンドに含めない。VITE_ プレフィックスは付けない。.env は .gitignore に含める