2026-10-12 訂正:旧OAuth例のstate保存漏れ・Math.random・browserのclient_secret/refresh token、GitHub説明の無加工HTML挿入、無限retryと構文エラー、本文を覆わないtimeoutを修正しました。完成済みのAPI統合として扱わず、危険な例を取り下げ、検証範囲を限定した読取例へ整理しています。
通信方式と認可を混同しない
REST・SOAP・GraphQL・WebSocket・gRPCは同じ選定軸だけで比較できません。operation、schema、transport、stream、互換性・運用の要件から候補を比べます。HTTPのPOSTが常に作成というわけでもなく、個別API仕様を確認します。
GraphQLの例としてGitHub GraphQL APIは挙げられますが、名前の似たFacebook Graph APIをそのままGraphQLの例にした旧説明を取り下げます。この訂正ではMetaの個別仕様を一次確認できていません。GraphQLも必要fieldを選べるだけで、query cost・N+1・権限・paginationが自動解決するわけではありません。
OAuthは主に認可のframeworkです。login/本人認証を行う場合はOIDC等の仕様とID token検証を別に確認します。OAuthの現行security指針RFC 9700を参照し、authorization codeとPKCE、transactionに結び付いたCSRF対策、redirect URIの一致を設計します。旧例のようにMath.randomでstateを作り、保存せず比較する実装を使いません。
browserのpublic clientにclient_secretを置いて秘密を保てるとは扱いません。BFF等を選ぶならtokenをserver側に保持し、session cookie・CSRF・期限と失効も設計します。refresh tokenをlocalStorageへ常設する例は取り下げました。OAuth/API keyの新規設定・認可操作・外部接続は今回実施していません。
読取の試行回数・本文・中断を一つの期限で扱う
旧retry例では5xx時に例外を出さず回数上限を確認できず、4xxもcatchでretryしていました。以下はNode.js 22以降のGET専用の小例です。設定済みoriginだけを許可し、502/503/504だけ最大三回、redirectを拒否、本文64KiBと全体deadlineを設けます。example.comは架空の接続先で、実providerには接続していません。
import {setTimeout as wait} from 'node:timers/promises';
// Node.js 22+; the example origin must be replaced by approved server configuration.
export async function readJson(url, {signal, timeoutMs=5000, maxAttempts=3, fetchImpl=fetch}={}) {
const target=new URL(url);
if(target.origin!=='https://api.example.com'||target.username||target.password)throw new TypeError('origin not allowed');
if(!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>30000||!Number.isInteger(maxAttempts)||maxAttempts<1||maxAttempts>3)throw new TypeError('invalid limits');
const deadline=new AbortController();
const combined=signal?AbortSignal.any([signal,deadline.signal]):deadline.signal;
const timer=setTimeout(()=>deadline.abort(new DOMException('Deadline exceeded','TimeoutError')),timeoutMs);
try {
for(let attempt=1;attempt<=maxAttempts;attempt++){
combined.throwIfAborted();
const response=await fetchImpl(target.href,{method:'GET',redirect:'error',signal:combined});
combined.throwIfAborted();
if([502,503,504].includes(response.status)&&attempt<maxAttempts){
await response.body?.cancel();
await wait(100*attempt,undefined,{signal:combined});
continue;
}
if(!response.ok){await response.body?.cancel();throw new Error('HTTP '+response.status);}
if(!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type')??'')){await response.body?.cancel();throw new TypeError('JSON expected');}
const reader=response.body?.getReader();
if(!reader)throw new TypeError('body missing');
const chunks=[];let bytes=0;
try {
while(true){
combined.throwIfAborted();
const {done,value}=await reader.read();
combined.throwIfAborted();
if(done)break;
bytes+=value.byteLength;
if(bytes>65536)throw new RangeError('body too large');
chunks.push(value);
}
}catch(error){await reader.cancel().catch(()=>{});throw error;}
finally{reader.releaseLock();}
const data=JSON.parse(Buffer.concat(chunks).toString('utf8'));
combined.throwIfAborted();
return data;
}
}finally{clearTimeout(timer);}
}
export function validateUser(data){
if(!data||typeof data!=='object'||Array.isArray(data)||!Number.isSafeInteger(data.id)||data.id<=0||typeof data.name!=='string'||!data.name.trim()||data.name.length>100||typeof data.email!=='string'||data.email.length>254||! /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email))throw new TypeError('invalid user');
return {id:data.id,name:data.name.trim(),email:data.email};
}
外部signalを置き換えず、deadlineと合わせます。header取得後も本文を読む間まで期限を残し、成功・失敗ともtimerを解放します。404・429・JSON不正・Content-Type不一致・network errorはこの例では自動retryしません。429では実providerのRetry-After、全体budget、quotaを確認して別に再開判断します。中断前、await後、返却前の確認は、それぞれ次の副作用や結果採用を防ぐためです。
この例の限界は、固定originからの読取専用で、認証・秘密・pagination・cache・schema全般を備えたclientではないことです。実装されたfetchがsignalを守ることを前提にしています。DNS/egress制御・全endpointの許可・concurrency・監視等は環境側で確認する必要があります。POSTの決済・送信には転用しません。timeoutでも既に行われた外部操作を取消したことにはならず、結果照合と二重実行防止が別に必要です。
Nodeで20項目を、Response/ReadableStreamのローカルmockだけで確認しました。成功、5xx三回停止、404一回、JSON/Content-Type/サイズ不正、origin/上限不正、開始前とheader後の中断、本文待ち中のdeadline、user dataの型・ID・name・emailを含みます。実APIの障害復旧・課金・成果を確認したtestではありません。
外部dataを表示する境界
旧GitHub例はname・description等をtemplate文字列でinnerHTMLへ渡していました。この表示codeを取り下げます。外部文字列はtextContent等で文字として挿入し、linkはHTTPSと許可hostnameを検証してからDOMへ設定します。HTMLを許す用途なら専用のsanitizationとpolicyを別に設計します。
GitHubのwatchers_countをwatch通知購読者数として表示しません。このfieldはstar数との互換fieldです。starはstargazers_count、購読者なら対象APIのsubscribers_count等の定義・取得方法を照合します。公式repository APIを参照してください。十件取得を全repositoryと扱わず、pagination・rate limit・0件・権限と404を区別します。
旧email regex末尾のescaped dollarはリテラル$に一致していました。上の末尾anchorへ直しました。ただし、この簡易regexは全email仕様の検証・本人確認ではありません。local partを勝手に小文字化しません。数値IDもparseIntで途中まで読んで受理せず、正のsafe integerというこの例の契約で検証します。
testの版と、実clientの存在を確認する
旧ApiClientにはgetUserがなく、その呼出しで実API連携をtestしたとは言えません。定義のないclient/matcherの実行例を取り下げ、実装した上の読取・validationに対するlocal testへ限定しました。
toMatchSchemaはJest標準matcherではありません。必要な追加matcherを明示するか、導入済みのschema validatorで検証して通常のassertionへ渡します。testをskipしたことを成功と報告しません。
MSWのrestはv1形式です。v2はhttp/HttpResponse、v3移行資料では両者のimport元はmsw/httpです。利用版とNode/browserのsetupを照合し、unhandled requestを外部へ通さないtestを用意します。この訂正ではMSWをinstall・実行していません。
OpenAPIは有効な形式で記述する
旧code blockはjsonと表示しながらインデントの崩れたYAML風記述で、escaped $refも不正でした。以下は架空の仕様案を正しいJSONとして表した例です。
{
"openapi": "3.0.3",
"info": {
"title": "架空ユーザーAPI",
"version": "1.0.0"
},
"paths": {
"/users/{id}": {
"get": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"minimum": 1
}
}
],
"responses": {
"200": {
"description": "成功",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "対象なし"
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"required": [
"id",
"name",
"email"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"maxLength": 100
},
"email": {
"type": "string",
"maxLength": 254
}
}
}
}
}
}
JSONをparseし、path parameterがrequiredであることとref先が存在することを確認しました。OpenAPI 3.0.3仕様の全体schema検証、生成client、実endpointとのcontract testは未実施です。emailの全形式条件や認証・error body等もこの仕様案で網羅したものではありません。
直接連携・BFF・gateway・eventを選ぶ前提
秘密を扱うならserver側の責任を決めます。BFFを置くだけで安全にはなりません。認証・認可、許可endpoint、入力・response上限、rate limit、ログと秘密のmask、cacheのuser/tenant境界、通信失敗を確認します。CORSはbrowserの読取制御であり、APIへの認証・認可の代替ではありません。
複数APIをeventでつなぐ場合も、重複・順序・部分成功・補償・人間確認を設計します。旧稿の決済・在庫・SNS連携の図は一般的な設計例で、今回実装した顧客案件や送信成果ではありません。
確認範囲
公式security・API・MSW・OpenAPI資料、20項目のローカルtest、JSON構文とrefを確認しました。OAuth flow、DOM表示、BFF、実provider、MSW、生成client、本番障害・費用・成果は未検証です。
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。