not/onの公開記事をより、止まらせず(NOT DONE)、自走する(ALWAYS ON)ものにするため、ダイナミックな記事生成のパーツを検討する。その第2弾。
NotionデータベースのデータをAWS Lambda(Python)でプレーンなJSONに変換し、前段の Amazon CloudFront でキャッシュすることで、Notion APIのレートリミットを回避し、高速かつ安全にフロントエンドから取得可能にする汎用アセット。
利用者が事前に「管理用API」へNotionの接続情報を登録し、安全な「アクセスキー」を発行。
not/onの記事が、Notionデータベースと連動して自走する、
これを実現するためのAPIと呼び出しライブラリを実装。
尚、not/onのための実装だが、アクセスキー認証とCORSフリーの設計により、誰でも、どのAPPでも安全に使えます。
NotionClient 開発ドキュメント
🚀 コンセプト
安全なマルチテナント対応
事前に管理用API経由でNotionのインテグレーションシークレットとデータベースIDを登録し、代わりにフロントエンド公開用の「アクセスキー」を発行。生のAPIキーがブラウザ側に漏洩するリスクを完全に排除。
高パフォーマンス・低コスト
ユーザーのブラウザは CloudFront エッジキャッシュにアクセスするため、Lambda の起動回数および Notion API の呼び出し回数を劇的に削減。CloudFront側でアクセスキーをキャッシュキーに含める設定を行うことで、複数ユーザーによるマルチテナント環境でも個別のキャッシュ配信とNotionのレートリミット(平均3リクエスト/秒)回避を両立。
Notionには迷惑をかけない。
完全な汎用性
Notion側で列(プロパティ)の追加・削除を行っても、APIやJSのコード修正は不要。
初期化の簡素化
NotionClient.js 内部に CloudFront のエンドポイントを隠蔽。利用側は発行されたアクセスキーを渡すだけでインスタンス化可能。
描画ロジックの分離: クライアントライブラリはデータ取得(Fetch)に特化。
アーキテクチャ
1. 接続情報の登録(とアクセスキーの発行)
[利用者] ──(Notionキー + DB ID)──> [管理用API] ──(暗号化)──> [Amazon DynamoDB] │ (アクセスキー発行)
2. クライアントからのデータ閲覧(通常時)
[閲覧者ブラウザ] ──(アクセスキー)──> [Amazon CloudFront] ──(キー毎キャッシュ)──> [データ取得API (Lambda)] ──> [Notion DB]
使い方(フロントエンド)
NotionClient.js をお使いのフロントエンドに配置してインポート。
📖 API 応答フォーマット(JSON)
CloudFront エッジから、以下のように型が平坦化(String, Number, Boolean, Array)されたオブジェクトの配列が高速返却される。
[
{
"id": "cbc3013d-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"タイトル": "Notion記事サンプル",
"タグ": ["技術", "AWS"],
"公開日": "2026-07-05",
"ステータス": "公開中",
"閲覧数": 1250,
"有効化": true
}
]
💻 クライアントライブラリ(JS)詳細リファレンス
クラス: NotionClient
async register(notionApiKey, databaseId)
Notionの接続情報を管理用APIへ登録し、新しいアクセスキーを発行してインスタンス内に自動セットする。
- 引数:
notionApiKey{string}(必須): NotionのインテグレーションシークレットdatabaseId{string}(必須): 対象NotionデータベースのID
- 戻り値:
Promise<Object>: 発行されたaccess_keyを含むオブジェクト{ "access_key": "notion_ack_xxxxxxxxxxxxxxxx" }
constructor(accessKey)
内部リソースとして固定された CloudFront エンドポイントの初期化、および固有のアクセスキーの設定。
引数: accessKey {string}(必須): 管理画面から発行された固有のアクセスキー。
async getRows(options = {})
アクセスキーをクエリパラメータに付与し、CloudFront から整形済みのレコード配列を非同期で取得(キー毎のキャッシュ有効)。
戻り値: Promise<Array>: 平坦化されたオブジェクトの配列。
💡 使い方・実装例
1. NotionのAPIキーとデータベースIDの取得
Notionから「インテグレーションシークレット」と「データベースID」を取得する手順。
インテグレーションシークレット (APIキー) の取得
- Notionのマイインテグレーション画面にアクセス
Notion My Integrations(外部サイト)を開く。 - 新しいインテグレーションの作成
「新しいインテグレーション」ボタンを押下。
任意の名前(例: not-on-proxy)を入力し、対象のワークスペースを選択して「保存」をクリック。 - シークレットのコピー
画面に表示される 「シークレット(内部インテグレーションシークレット)」 を表示し、コピーして控える(これがAPIキーになる)。
データベースID の取得と接続許可
- データベースIDの抽出
対象のNotionデータベースをブラウザで開く。
URLを確認し、以下の規則に従って 32桁の英数字 を抽出する。
通常のURLの場合:
https://www.notion.so/ワークスペース名/【この32桁の英数字】?v=...
ドメインが短い場合:
https://www.notion.so/【この32桁の英数字】?v=...
⚠️ 注意: ?v= より前のスラッシュ(/)に挟まれた32桁の文字列(ハイフンは除外するか、そのまま使用)がデータベースID。
- インテグレーションの接続許可(必須)
Notionデータベースの右上にある 「•••(三点リーダー)」 メニューを開く。
「接続先」(または「接続を追加」)を選択。
先ほど作成したインテグレーション名(例: not-on-proxy)を選択し、アクセスを許可(招待)する。 - 📝 設定完了後のチェック
インテグレーションシークレット: secret_ から始まる文字列
データベースID: 32桁の文字列
接続許可: データベース側でインテグレーションが追加されていること
これらが揃えば、アクセスキーを発行できる。
2. アクセスキーの発行
- 下記の画面(HTML)で「Notion インテグレーションシークレット (APIキー)」と「Notion データベースID」を入力
- 「アクセスキーを発行」ボタンを押下し、表示されたアクセスキーをコピー
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> NotionClient - アクセスキー発行管理ツール</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 2rem auto;
max-width: 600px;
padding: 0 1rem;
background-color: #fafafa;
color: #1f2937;
}
h1 {
font-size: 1.5rem;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.warning {
background-color: #fffbeb;
border: 1px solid #fef3c7;
color: #b45309;
padding: 1rem;
border-radius: 0.375srem;
font-size: 0.875rem;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
label {
display: block;
font-weight: bold;
margin-bottom: 0.375rem;
font-size: 0.875rem;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 0.625rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
box-sizing: border-box;
font-family: monospace;
}
button {
background-color: #2563eb;
color: white;
padding: 0.625rem 1.25rem;
border: none;
border-radius: 0.375rem;
font-weight: bold;
cursor: pointer;
width: 100%;
}
button:hover {
background-color: #1d4ed8;
}
button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.result-box {
margin-top: 1.5rem;
padding: 1rem;
background-color: #ecfdf5;
border: 1px solid #d1fae5;
border-radius: 0.375rem;
display: none;
}
.result-box.error {
background-color: #fef2f2;
border: 1px solid #fee2e2;
color: #991b1b;
}
.key-display {
background-color: #ffffff;
padding: 0.75rem;
border: 1px solid #a7f3d0;
border-radius: 0.25rem;
font-family: monospace;
font-size: 1.125rem;
color: #065f46;
word-break: break-all;
margin-top: 0.5rem;
}
</style>
</head>
<body>
<h1>Notion 接続設定・アクセスキー発行</h1>
<div class="warning">
<strong>⚠️ セキュリティ注意</strong><br>
この画面は生シークレット(Notion APIキー)を取り扱います。一般公開サイトには配置せず、ローカル環境での実行か、関係者のみがアクセスできる安全な環境(認証制限下)でのみ利用してください。
</div>
<div class="form-group">
<label for="notionApiKey">Notion インテグレーションシークレット (APIキー)</label>
<input type="password" id="notionApiKey" placeholder="secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
</div>
<div class="form-group">
<label for="databaseId">Notion データベースID</label>
<input type="text" id="databaseId" placeholder="32桁の英数字(ハイフンなし)">
</div>
<button id="submitBtn">アクセスキーを発行</button>
<div id="resultBox" class="result-box">
<div id="resultMessage"></div>
<div id="keyDisplay" class="key-display"></div>
</div>
<script type="module">
import { NotionClient } from './NotionClient.js';
const notionApiKeyInput = document.getElementById('notionApiKey');
const databaseIdInput = document.getElementById('databaseId');
const submitBtn = document.getElementById('submitBtn');
const resultBox = document.getElementById('resultBox');
const resultMessage = document.getElementById('resultMessage');
const keyDisplay = document.getElementById('keyDisplay');
submitBtn.addEventListener('click', async () => {
const notionApiKey = notionApiKeyInput.value.trim();
const databaseId = databaseIdInput.value.trim();
if (!notionApiKey || !databaseId) {
alert('両方の項目を入力してください。');
return;
}
// UI状態の初期化
submitBtn.disabled = true;
submitBtn.innerText = '発行中...';
resultBox.style.display = 'none';
resultBox.classList.remove('error');
keyDisplay.style.display = 'block';
try {
// 初期化時はアクセスキー指定なしの adminClient を生成
const adminClient = new NotionClient();
// ライブラリの登録関数をコール
const result = await adminClient.register(notionApiKey, databaseId);
// 成功時の描画
resultMessage.innerHTML = '<strong>登録に成功しました。</strong><br>以下のアクセスキーをコピーし、フロントエンド(公開Webサイト側)の初期化コードに埋め込んでください。';
keyDisplay.innerText = result.access_key;
resultBox.style.display = 'block';
// 入力フォームをクリア(セキュリティ保護のため)
notionApiKeyInput.value = '';
databaseIdInput.value = '';
} catch (error) {
// エラー時の描画
resultBox.classList.add('error');
resultMessage.innerHTML = '<strong>エラーが発生しました。</strong>';
keyDisplay.innerText = error.message || error;
resultBox.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.innerText = 'アクセスキーを発行';
}
});
</script>
</body>
</html>
3. データ取得
ご利用のフロントエンドAPPにNotionClientライブラリをインポート、
下記は、データ取得の実装例。
import { NotionClient } from './NotionClient.js'
<script type="module">
import { NotionClient } from './NotionClient.js';
// 事前に発行した「安全なアクセスキー」のみを埋め込む
const client = new NotionClient('notion_ack_abc123_取得したキー');
async function initTable() {
try {
const articles = await client.getRows();
const tbody = document.querySelector('#article-table tbody');
tbody.innerHTML = articles.map(item => `
<tr>
<td>${item.タイトル || '無題'}</td>
<td>${Array.isArray(item.タグ) ? item.タグ.join(', ') : (item.タグ || '-')}</td>
</tr>
`).join('');
} catch (error) {
console.error('データ取得失敗', error);
}
}
document.addEventListener('DOMContentLoaded', initTable);
</script>
setupAndFetch();
ライブラリ情報
OpenAPI 3.0.0 仕様書(JSON)
{
"openapi": "3.0.3",
"info": {
"title": "Generic Notion Proxy & Multi-Tenant API",
"description": "Notionの接続情報を安全に管理し、発行されたアクセスキーを用いて、平坦化(フラット化)されたプレーンなJSONデータを配信するマルチテナント対応プロキシAPI。",
"version": "1.1.0"
},
"servers": [
{
"url": "https://YOUR_CLOUDFRONT_DISTRIBUTION_URL.cloudfront.net",
"description": "Amazon CloudFront ディストリビューションエンドポイント"
}
],
"paths": {
"/register": {
"post": {
"summary": "Notion接続情報の登録とアクセスキーの発行",
"description": "利用者のNotion APIキーとデータベースIDをデータベースに安全に登録し、フロントエンド公開用の固有のアクセスキーを発行します。",
"operationId": "registerTenant",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterRequest"
}
}
}
},
"responses": {
"200": {
"description": "登録成功。アクセスキーが発行されます。",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterResponse"
}
}
}
},
"400": {
"description": "リクエスト不正。必須パラメータの不足など。",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "サーバー内部エラー。データストアへの書き込み失敗など。",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/query": {
"get": {
"summary": "整形済みNotionデータの取得(キャッシュ有効)",
"description": "クエリパラメータで指定されたアクセスキーに紐づくNotionデータベースからレコードをクエリし、データ型を平坦化したオブジェクトの配列を返します。前段のCloudFrontでキー毎にキャッシュされます。",
"operationId": "getNotionRows",
"parameters": [
{
"name": "access_key",
"in": "query",
"description": "管理画面または/registerAPIから発行された固有のアクセスキー",
"required": true,
"schema": {
"type": "string"
},
"example": "notion_ack_cbc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
}
],
"responses": {
"200": {
"description": "正常処理。フラットなオブジェクトの配列が返ります。",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotionRow"
}
},
"example": [
{
"id": "cbc3013d-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"タイトル": "Notion記事サンプル",
"タグ": ["技術", "AWS"],
"公開日": "2026-07-05",
"ステータス": "公開中",
"閲覧数": 1250,
"有効化": true
}
]
}
}
},
"401": {
"description": "認証エラー。無効なアクセスキーが指定された場合。",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"500": {
"description": "サーバー内部エラー。Notion APIとの通信失敗やパースエラー。",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"RegisterRequest": {
"type": "object",
"required": ["notion_api_key", "database_id"],
"properties": {
"notion_api_key": {
"type": "string",
"description": "Notionのインテグレーションシークレットトークン",
"example": "secret_xX0yY1zZ..."
},
"database_id": {
"type": "string",
"description": "対象とするNotionデータベースのID(32桁の英数字)",
"example": "6bc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
}
}
},
"RegisterResponse": {
"type": "object",
"properties": {
"access_key": {
"type": "string",
"description": "フロントエンド公開用に発行された固有のアクセスキー。データ取得APIの認証に利用。",
"example": "notion_ack_6bc3013d1a2b3c4d5e6f7a8b9c0d1e2f"
}
}
},
"NotionRow": {
"type": "object",
"description": "Notionの1行(ページ)データをフラットに変換したオブジェクト。キー名はNotionの列名(プロパティ名)がそのまま動的にマッピングされる。",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "NotionページのユニークID"
}
},
"additionalProperties": {
"oneOf": [
{ "type": "string", "description": "テキスト、セレクト、日付、URLなど" },
{ "type": "number", "description": "数値プロパティ" },
{ "type": "boolean", "description": "チェックボックス" },
{
"type": "array",
"items": { "type": "string" },
"description": "マルチセレクトなど複数値を持つプロパティ"
},
{ "type": "null" }
]
}
},
"ErrorResponse": {
"type": "object",
"required": ["error"],
"properties": {
"error": {
"type": "string",
"description": "エラーメッセージの詳細"
}
}
}
}
}
}