- 技術 💻
マッシュアップとは?Web APIを組み合わせる仕組みと作り方を実演
結論
マッシュアップとは、複数のWebサービスやAPIが返すデータ・機能を組み合わせて、1つの新しいサービスを作る手法です。地図に店舗や天気を重ねて表示するサービスが代表例です。もとは音楽で「複数の曲を重ねて1曲にする」手法を指す言葉です。
この記事でわかること
- マッシュアップの意味(Web・音楽の場面別)と、3つの型(表示・データ・処理の組み合わせ)
- 天気APIとイベントAPIの自作モックを組み合わせる小さなアプリの実物と、直列・並列・キャッシュの実測
- APIの利用規約・レート制限への配慮、片方が止まったときの作り方、エラー原文と判断表
この記事で実際に動かしたもの:127.0.0.1 に立てた自作の「天気API」と「イベントAPI」(どちらもモック)を、curl と jq、Node.js のサーバー、Python、Chromium のページから組み合わせ、出力・時間・エラー原文を記録しました。外部のAPIには接続していません。
マッシュアップの全体像を先に図にします。利用者が見るのは1つの画面ですが、裏では別々の提供元のAPIを呼び、結果を1つにまとめています。
flowchart TD
U[利用者の画面] --> M[マッシュアップ<br>アプリ]
M --> A[天気API<br>提供元A]
M --> E[イベントAPI<br>提供元B]
A --> J[結果を突き合わせる<br>都市コードで対応]
E --> J
J --> R[雨なら屋内を先に<br>1つの画面で表示]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| マッシュアップとは | 複数のWebサービス・APIを組み合わせて新しいサービスを作る手法 | マッシュアップとは |
| マッシュアップの意味(音楽) | 複数の曲を重ねて1曲に仕立てる制作手法。既存曲を使うなら権利の確認が要る | マッシュアップとは |
| マッシュアップの例 | 地図×飲食店、地図×天気、天気×イベントなど | マッシュアップの例 |
| マッシュアップの種類 | 表示・データ・処理(ロジック)の組み合わせの3つで説明されることが多い | マッシュアップの3つの型 |
| マッシュアップの作り方 | APIを2つ呼び、共通の鍵(都市コード等)で突き合わせて1つのJSONにする | 最初の5分・作ってみる |
| ブラウザだけで作れる? | APIがCORSを許可していれば可能。APIキーが要るならサーバー側で組む | ブラウザかサーバーか |
| 注意点は? | 利用規約・レート制限・帰属表示・API停止や仕様変更への備え | 利用規約とレート制限 |
| Webhook・ノーコードとの違い | Webhookは通知の仕組み、ノーコードは組み立ての道具。マッシュアップは作るものの考え方 | 違い |
確認環境:Ubuntu 24.04.5 LTS(2 CPU・メモリ 8GB)。Node.js 22.23.4 の標準機能(http・fetch・AbortSignal.timeout・Promise.allSettled)だけで、天気API(127.0.0.1:18215。APIキー必須・10秒に5回まで・応答に300ms)とイベントAPI(127.0.0.1:18216。キー不要・応答に200ms・CORSヘッダーなし)のモックと、2つを組み合わせるマッシュアップのサーバー(127.0.0.1:18217)を作りました。curl 8.5.0 と jq 1.7、Python 3.13.18(標準ライブラリのみ)、Playwright 1.63.0 から起動した Chromium 153 でも同じAPIを呼んでいます。時間は同じマシンで21回測った中央値です。外部のAPIには接続しておらず、APIの利用規約・CORS・Retry-After の記述は各公式ドキュメントで確かめた範囲です。図は mermaid 11 で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「マッシュアップとは?Web APIを組み合わせる仕組みと作り方を実演」
マッシュアップとは(Web・音楽での意味)
この節では、マッシュアップという言葉の意味を、ふだんの意味とITでの意味に分けて整理できます。
マッシュアップ(mashup)は、英語の mash up(すりつぶして混ぜ合わせる)から来た言葉で、IT では「複数のWebサービス・ソフトウェア・データベースなどを組み合わせて、新しいサービスを作る手法」を指します。 Webの提供者が外部から呼び出せるように公開しているデータや機能(Web API)を、別の提供者のものと組み合わせるのが典型です。
もとは音楽の言葉で、2つ以上の曲を重ねて再生し、1曲に仕立てる制作手法を指します。2000年代半ばに「Web 2.0」という言葉とともに、Webサービスを組み合わせる意味でも広く使われるようになりました。
| 場面 | マッシュアップの意味 | 例 |
|---|---|---|
| ふだんの英語 | すりつぶす・混ぜ合わせる(mash up) | mashed potatoes(マッシュポテト)と同じ mash |
| 音楽 | 複数の曲の歌とオケなどを重ねて1曲に仕立てる手法 | A曲のボーカルにB曲の伴奏を重ねる |
| Web・IT | 複数のWebサービス・APIを組み合わせて新しいサービスを作る手法 | 地図に飲食店の情報を重ねて表示する |
| 動画・画像 | 複数の素材を組み合わせて1つの作品にすること | 複数の映画の場面をつなぐ予告編風の動画 |
| 情報処理の試験 | 公開されたWebサービスを組み合わせてコンテンツを作る手法として出題 | 大学の案内ページに地図サービスを組み込む |
音楽のマッシュアップで既存の曲や音源を使う場合は、著作権法の複製権(第21条)・翻案権(第27条。編曲も含む)、音源を作ったレコード製作者の権利(第96条ほか)に関わります。公開する前に、権利者の許諾や配信サービスの規約を確かめます(この記事は法的な助言ではありません)。以下はWeb・ITでの意味に絞って説明します。
自前で全部作る
地図・天気・店舗のデータを自分で集めて管理する
自由に作れるが、時間と費用がかかる
データの更新も自分の責任
マッシュアップ
各分野の提供者が公開するAPIを呼んで組み合わせる
少ないコードで早く作れる
APIの停止・仕様変更・規約の影響を受ける
最初の5分:2つのAPIを curl と jq でマッシュアップする
この節では、プログラムを書く前に、コマンド2つでマッシュアップの最小形を体験できます。
まず、天気APIとイベントAPIをそれぞれ呼んだ結果です。2つのAPIは提供元が違うという想定なので、都市の表し方(tokyo と TYO)も、時刻の形式(ISO 8601 の文字列と UNIX 時間)も揃っていません。
$ curl -s --noproxy '*' -i -H 'X-API-Key: demo-key-weather' \
'http://127.0.0.1:18215/v1/weather?city=tokyo'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=60
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4
{"city":"tokyo","name":"東京","forecast":"rain","temp_c":17,"pop":80,"updated_at":"2026-10-29T09:00:00+09:00"}
$ curl -s --noproxy '*' 'http://127.0.0.1:18216/events?area=TYO'
{"count":3,"items":[{"id":"e1","area_code":"TYO","title":"屋外マルシェ","indoor":false,"start":1793235600},{"id":"e2","area_code":"TYO","title":"プラネタリウム特別上映","indoor":true,"start":1793239200},{"id":"e3","area_code":"TYO","title":"美術館の夜間開館","indoor":true,"start":1793260800}]}
2つの結果を jq で1つにまとめます。降水確率(pop)が50%以上なら屋内のイベントだけを残す、という小さな決まりを入れました。
$ W=$(curl -s --noproxy '*' -H 'X-API-Key: demo-key-weather' 'http://127.0.0.1:18215/v1/weather?city=tokyo')
$ E=$(curl -s --noproxy '*' 'http://127.0.0.1:18216/events?area=TYO')
$ jq -n --argjson w "$W" --argjson e "$E" \
'{city:$w.name, forecast:$w.forecast, pop:$w.pop,
picks:[$e.items[] | select(if $w.pop>=50 then .indoor else true end) | .title]}'
{
"city": "東京",
"forecast": "rain",
"pop": 80,
"picks": [
"プラネタリウム特別上映",
"美術館の夜間開館"
]
}
天気APIだけでもイベントAPIだけでも出せなかった「雨の東京で行ける屋内イベント」という答えが、2つを組み合わせて初めて出ました。 これがマッシュアップの芯です。以降の節では、これをアプリにしたときに必要になること(速さ・キー・制限・障害)を1つずつ確かめます。
使うAPIを選ぶ
何と何を組み合わせると新しい価値が出るかを決め、各APIのドキュメントと利用規約を読む。
1つずつ単体で呼ぶ
curl で呼び、応答の形・ステータス・ヘッダー(制限の残り回数など)を確かめる。
突き合わせる鍵を決める
都市・住所・商品コードなど、2つのAPIの結果をつなぐ項目と、その対応表を用意する。
組み合わせて1つの形に
単位・時刻・名前を揃え、利用者に見せる1つのJSONや画面にまとめる。
失敗に備える
タイムアウト・キャッシュ・片方だけ失敗したときの表示を入れる。
マッシュアップの3つの型(表示・データ・ロジック)
この節では、マッシュアップを「何を組み合わせるか」で3つに分ける考え方と、それぞれの実物が分かります。
マッシュアップは、組み合わせるものによって次の3つに分けて説明されることがあります。情報処理技術者試験(基本情報技術者試験など)では「ロジックマッシュアップ」の例を選ぶ問題も出ています。ただし、この分類は主に試験の文脈で使われる言葉で、開発の会話で毎日聞くものではありません。
| 型 | 組み合わせるもの | この記事での実物 |
|---|---|---|
| プレゼンテーション(表示)マッシュアップ | 画面の表示。地図や動画の部品を自分のページに埋め込む | 1つのページに天気とイベント一覧を並べて表示 |
| データマッシュアップ | 複数のAPIのデータを鍵で突き合わせて1つにする | 都市コードで天気とイベントを1つのJSONに |
| ロジックマッシュアップ | あるAPIの出力を、別のAPIの入力に渡して処理をつなぐ | 天気の降水確率から indoor=true を決めてイベントAPIに渡す |
ロジックマッシュアップの実物です。天気APIの出力(降水確率80%)から「屋内」という条件を作り、イベントAPIへの問い合わせに使いました。最初の5分の例(両方を取ってから絞る)と違い、イベントAPIは最初から屋内の2件だけを返します。
$ W=$(curl -s --noproxy '*' -H 'X-API-Key: demo-key-weather' 'http://127.0.0.1:18215/v1/weather?city=tokyo')
$ IN=$(echo "$W" | jq -r 'if .pop>=50 then "true" else "false" end'); echo "indoor=$IN"
indoor=true
$ curl -s --noproxy '*' "http://127.0.0.1:18216/events?area=TYO&indoor=$IN" | jq -c '[.items[].title]'
["プラネタリウム特別上映","美術館の夜間開館"]
表示を組み合わせる
地図・動画・SNSの埋め込み部品をページに並べる。
いちばん手軽
データを組み合わせる
共通の鍵で突き合わせ、1つの答えにまとめる。
鍵の対応表が要る
処理をつなぐ
Aの出力をBの入力に渡す。呼ぶ順番が決まる。
並列にできない
ロジックマッシュアップは、Bの呼び出しがAの結果を待つため、後で測る「並列化」ができません。速さと取ってくるデータの量のどちらを優先するかで、データマッシュアップと使い分けます。
マッシュアップの例と、広まった経緯
この節では、マッシュアップのよく知られた例と、言葉が広まった流れが分かります。
マッシュアップの説明で必ず出てくるのは、地図のAPIに別のデータを重ねる例です。地図は自前で用意するのが特に大変なため、2005年に Google マップの API が公開されてから、地図に店舗・物件・天気・写真を重ねるサービスが次々に作られました。
| 組み合わせ | できること | 使われる考え方 |
|---|---|---|
| 地図 × 飲食店データ | 現在地の近くの店を地図上に表示 | データ・表示 |
| 地図 × 天気予報 | 地図の上に各地の天気を重ねる | データ・表示 |
| 天気 × イベント(この記事) | 雨なら屋内イベントを先に出す | データ・ロジック |
| 郵便番号 × 住所の入力欄 | 郵便番号から住所を自動入力 | データ |
| 検索 × 商品 × ブックマーク数 | 検索結果に関連商品と人気度を並べる | データ・表示 |
| 写真共有 × 位置情報 | 撮影地点ごとに写真を地図に並べる | 表示 |
2006〜2008年ごろには、Yahoo! Pipes や Microsoft Popfly のように、部品をつなぐだけでマッシュアップを作れる道具も登場しました(いずれも現在は提供されていません)。今は「マッシュアップ」という言葉を前面に出すサービスは減りましたが、APIを組み合わせてサービスを作るやり方そのものは、Webアプリの標準的な作り方になりました。 決済・地図・認証・メール送信・生成AIを外部のAPIで済ませるアプリは、どれも広い意味のマッシュアップです。
関連記事「REST APIの設計|URL・ステータスコード・エラー形式の決め方」で扱っています。
作ってみる:天気APIとイベントAPIのマッシュアップ(Node.js)
この節では、モックAPI2つと、それを組み合わせるサーバーの全コードと動かした結果が分かります。
外部のAPIには接続しないため、提供元の違う2つのAPIを自分で用意しました。天気APIは「APIキー必須・10秒に5回まで」、イベントAPIは「キー不要・CORSヘッダーなし」と、性格を変えています。どちらも Node.js の標準モジュールだけで書いています。
// 天気API(モック): 127.0.0.1:18215 APIキー必須・10秒に5回まで・300ms遅延
import http from 'node:http';
const KEY = 'demo-key-weather';
const DATA = {
tokyo: { city: 'tokyo', name: '東京', forecast: 'rain', temp_c: 17, pop: 80 },
osaka: { city: 'osaka', name: '大阪', forecast: 'sunny', temp_c: 20, pop: 10 },
sapporo:{ city: 'sapporo',name: '札幌', forecast: 'cloudy',temp_c: 9, pop: 30 },
};
const LIMIT = Number(process.env.LIMIT ?? 5), WINDOW = 10_000; let hits = [];
let served = 0;
http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const send = (code, body, h = {}) => { res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', ...h }); res.end(JSON.stringify(body)); };
if (u.pathname === '/stats') return send(200, { served });
if (u.pathname !== '/v1/weather') return send(404, { error: 'not_found' });
if (req.headers['x-api-key'] !== KEY) return send(401, { error: 'invalid_api_key' });
const now = Date.now(); hits = hits.filter(t => now - t < WINDOW);
if (hits.length >= LIMIT) {
const wait = Math.ceil((WINDOW - (now - hits[0])) / 1000);
return send(429, { error: 'rate_limited' }, { 'Retry-After': String(wait), 'X-RateLimit-Limit': String(LIMIT), 'X-RateLimit-Remaining': '0' });
}
hits.push(now); served++;
const city = (u.searchParams.get('city') || '').toLowerCase();
const d = DATA[city];
setTimeout(() => {
if (!d) return send(404, { error: 'unknown_city', city });
send(200, { ...d, updated_at: '2026-10-29T09:00:00+09:00' }, {
'Cache-Control': 'max-age=60', 'X-RateLimit-Limit': String(LIMIT), 'X-RateLimit-Remaining': String(LIMIT - hits.length) });
}, Number(process.env.DELAY ?? 300));
}).listen(18215, '127.0.0.1', () => console.log('weather-api on 127.0.0.1:18215 pid', process.pid));
// イベントAPI(モック): 127.0.0.1:18216 キー不要・200ms遅延・CORSヘッダーなし・形式が天気APIと違う
import http from 'node:http';
const EVENTS = [
{ id: 'e1', area_code: 'TYO', title: '屋外マルシェ', indoor: false, start: 1793235600 },
{ id: 'e2', area_code: 'TYO', title: 'プラネタリウム特別上映', indoor: true, start: 1793239200 },
{ id: 'e3', area_code: 'TYO', title: '美術館の夜間開館', indoor: true, start: 1793260800 },
{ id: 'e4', area_code: 'OSA', title: '河川敷フェス', indoor: false, start: 1793235600 },
{ id: 'e5', area_code: 'OSA', title: '水族館ナイト', indoor: true, start: 1793257200 },
{ id: 'e6', area_code: 'SPK', title: '屋内スケート開放', indoor: true, start: 1793242800 },
];
let served = 0;
http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
if (u.pathname === '/stats') { res.writeHead(200, {'Content-Type':'application/json'}); return res.end(JSON.stringify({ served })); }
if (process.env.BROKEN === 'html') { res.writeHead(502, {'Content-Type':'text/html'}); return res.end('<html><body><h1>502 Bad Gateway</h1></body></html>'); }
if (u.pathname !== '/events') { res.writeHead(404); return res.end(); }
served++;
const area = u.searchParams.get('area');
const indoor = u.searchParams.get('indoor');
let items = EVENTS.filter(e => e.area_code === area);
if (indoor !== null) items = items.filter(e => String(e.indoor) === indoor);
if (process.env.RENAME === '1') items = items.map(({ title, ...r }) => ({ ...r, name: title }));
setTimeout(() => { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ count: items.length, items })); },
Number(process.env.DELAY ?? 200));
}).listen(18216, '127.0.0.1', () => console.log('event-api on 127.0.0.1:18216 pid', process.pid));
2つのAPIの違いを表にしました。実際の外部APIでも、提供元が違えばこの程度の違いは普通にあり、マッシュアップのコードはこの差を吸収する役目を負います。
| 項目 | 天気API | イベントAPI | マッシュアップ側の対応 |
|---|---|---|---|
| 都市の表し方 | tokyo(英小文字) |
TYO(3文字コード) |
対応表 AREA で変換 |
| 時刻の形式 | ISO 8601 の文字列(+09:00) | UNIX 時間(秒) | 日本時間の「11:00」に揃える |
| 認証 | X-API-Key ヘッダー必須 | 不要 | キーはサーバーの環境変数に置く |
| 回数制限 | 10秒に5回。超えると429 | なし | キャッシュと Retry-After |
| 応答時間 | 300ms | 200ms | 並列に呼び、1.5秒で打ち切る |
| CORS | なし(ブラウザから呼ばない前提) | なし | サーバー側で組む |
都市の対応表やタイムゾーンは、間違えてもエラーにならず、違う都市・違う時刻の結果が静かに混ざるのが厄介な点です。対応表に無い値は先に弾く、時刻は必ずタイムゾーンを指定して変換する、という2つを決めておきます。
マッシュアップの本体です。2つのAPIを同時に呼び(Promise.allSettled)、都市名の対応表で結果を突き合わせ、天気で並べ順を決めます。APIキーは環境変数から読み、ブラウザには渡しません。
// マッシュアップ(BFF): 127.0.0.1:18217 天気API+イベントAPI を組み合わせて「今日の過ごし方」を返す
import http from 'node:http';
import { readFileSync } from 'node:fs';
const WEATHER = 'http://127.0.0.1:18215/v1/weather';
const EVENTS = 'http://127.0.0.1:18216/events';
const KEY = process.env.WEATHER_API_KEY; // キーはサーバー側だけに置く
const AREA = { tokyo: 'TYO', osaka: 'OSA', sapporo: 'SPK' }; // 2つのAPIの「都市の表し方」の対応表
const TTL = Number(process.env.TTL ?? 60_000);
const MODE = process.env.MODE ?? 'parallel'; // parallel | serial
const cache = new Map(); let blockedUntil = 0;
async function getJSON(url, opt = {}) {
const r = await fetch(url, { ...opt, signal: AbortSignal.timeout(Number(process.env.TIMEOUT ?? 1500)) });
const text = await r.text();
if (!r.ok) { const e = new Error(`HTTP ${r.status} ${url.split('?')[0]}`); e.status = r.status; e.retryAfter = r.headers.get('retry-after'); throw e; }
return JSON.parse(text);
}
async function weather(city) {
const hit = cache.get(city);
if (hit && Date.now() - hit.at < TTL) return { ...hit.v, cached: true };
if (Date.now() < blockedUntil) { // Retry-After の間は呼ばない
if (hit) return { ...hit.v, cached: true, stale: true };
throw Object.assign(new Error('rate limited (waiting Retry-After)'), { status: 429 });
}
try {
const v = await getJSON(`${WEATHER}?city=${encodeURIComponent(city)}`, { headers: { 'X-API-Key': KEY } });
cache.set(city, { at: Date.now(), v }); return { ...v, cached: false };
} catch (e) {
if (e.status === 429) blockedUntil = Date.now() + Number(e.retryAfter ?? 10) * 1000;
if (hit) return { ...hit.v, cached: true, stale: true }; // 期限切れでも手元の値で返す
throw e;
}
}
const events = (city) => getJSON(`${EVENTS}?area=${AREA[city]}`);
const fmt = (unix) => new Date(unix * 1000).toLocaleTimeString('ja-JP', { timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit' });
async function plan(city) {
const t0 = performance.now();
let rw, re;
if (MODE === 'serial') {
rw = await weather(city).then(v => ({ status: 'fulfilled', value: v }), e => ({ status: 'rejected', reason: e }));
re = await events(city).then(v => ({ status: 'fulfilled', value: v }), e => ({ status: 'rejected', reason: e }));
} else {
[rw, re] = await Promise.allSettled([weather(city), events(city)]);
}
const warnings = [];
const w = rw.status === 'fulfilled' ? rw.value : (warnings.push(`weather: ${rw.reason.message}`), null);
const ev = re.status === 'fulfilled' ? re.value : (warnings.push(`events: ${re.reason.message}`), null);
if (ev?.items?.some(e => typeof e.title !== 'string' || typeof e.start !== 'number'))
warnings.push('events: unexpected shape (title/start missing)'); // 形の確認
const rainy = w ? (w.forecast === 'rain' || w.pop >= 50) : null;
const items = ev?.items?.map(e => ({ title: e.title ?? '(名称なし)', indoor: e.indoor, time: fmt(e.start) }))
.sort((a, b) => rainy === null ? 0 : (rainy ? b.indoor - a.indoor : a.indoor - b.indoor)) ?? null;
return {
city, weather: w && { name: w.name, forecast: w.forecast, temp_c: w.temp_c, pop: w.pop, cached: w.cached, stale: w.stale },
advice: rainy === null ? '天気が取れないため並べ替えなし' : rainy ? '雨の予報:屋内のイベントを先に表示' : '晴れ・くもり:屋外のイベントを先に表示',
events: items, warnings, took_ms: Math.round(performance.now() - t0),
};
}
http.createServer(async (req, res) => {
const u = new URL(req.url, 'http://x');
if (u.pathname === '/') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); return res.end(readFileSync(new URL('./index.html', import.meta.url))); }
if (u.pathname === '/api/plan') {
const city = u.searchParams.get('city') ?? 'tokyo';
if (!AREA[city]) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'unknown city' })); }
const body = await plan(city);
const code = body.weather || body.events ? 200 : 502;
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' }); return res.end(JSON.stringify(body, null, 1));
}
res.writeHead(404); res.end();
}).listen(18217, '127.0.0.1', () => console.log(`mashup on 127.0.0.1:18217 mode=${MODE} pid`, process.pid));
3つを起動して、マッシュアップのAPIを呼んだ結果です。
$ node weather-api.mjs &
weather-api on 127.0.0.1:18215 pid 25212
$ node event-api.mjs &
event-api on 127.0.0.1:18216 pid 25213
$ WEATHER_API_KEY=demo-key-weather node mashup.mjs &
mashup on 127.0.0.1:18217 mode=parallel pid 25214
$ curl -s --noproxy '*' 'http://127.0.0.1:18217/api/plan?city=tokyo'
{
"city": "tokyo",
"weather": {
"name": "東京",
"forecast": "rain",
"temp_c": 17,
"pop": 80,
"cached": false
},
"advice": "雨の予報:屋内のイベントを先に表示",
"events": [
{ "title": "プラネタリウム特別上映", "indoor": true, "time": "11:00" },
{ "title": "美術館の夜間開館", "indoor": true, "time": "17:00" },
{ "title": "屋外マルシェ", "indoor": false, "time": "10:00" }
],
"warnings": [],
"took_ms": 364
}
$ curl -s --noproxy '*' -i 'http://127.0.0.1:18217/api/plan?city=nagoya' | head -1
HTTP/1.1 400 Bad Request
UNIX 時間の 1793239200 が日本時間の「11:00」に、TYO と tokyo が同じ都市として扱われています。マッシュアップのコードの多くは、こうした「形の違いを揃える」処理です。 対応表に無い都市(nagoya)は、上流のAPIを呼ぶ前に 400 で返しています。
同じ処理を Python の標準ライブラリだけで書くと次のようになります。2つのAPIはスレッドで同時に呼んでいます。
import json, os, time, urllib.request
from concurrent.futures import ThreadPoolExecutor
KEY = os.environ["WEATHER_API_KEY"]
AREA = {"tokyo": "TYO", "osaka": "OSA", "sapporo": "SPK"}
def get_json(url, headers=None, timeout=1.5):
req = urllib.request.Request(url, headers=headers or {})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r)
def plan(city):
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as ex:
fw = ex.submit(get_json, f"http://127.0.0.1:18215/v1/weather?city={city}", {"X-API-Key": KEY})
fe = ex.submit(get_json, f"http://127.0.0.1:18216/events?area={AREA[city]}")
w, e = fw.result(), fe.result()
rainy = w["forecast"] == "rain" or w["pop"] >= 50
items = sorted(e["items"], key=lambda x: x["indoor"], reverse=rainy)
return {"city": w["name"], "rainy": rainy, "events": [x["title"] for x in items],
"took_ms": round((time.perf_counter() - t0) * 1000)}
print(json.dumps(plan("tokyo"), ensure_ascii=False))
$ WEATHER_API_KEY=demo-key-weather python3 mashup.py
{"city": "東京", "rainy": true, "events": ["プラネタリウム特別上映", "美術館の夜間開館", "屋外マルシェ"], "took_ms": 351}
$ python3 mashup.py # キーを渡し忘れた場合
KeyError: 'WEATHER_API_KEY'
マッシュアップはブラウザで組むか、サーバーで組むか(CORSとAPIキー)
この節では、ブラウザのJavaScriptから直接2つのAPIを呼ぶ方法と、サーバーで組み合わせる方法の違いが、Chromium での実物で分かります。
マッシュアップのサーバー(127.0.0.1:18217)が返すページに、2つのボタンを置きました。「サーバー経由で取得」は同じオリジンの /api/plan を呼び、「ブラウザから直接」はイベントAPI(127.0.0.1:18216)を直接呼びます。
<!doctype html><meta charset="utf-8"><title>今日の過ごし方(マッシュアップ)</title>
<h1>今日の過ごし方</h1><select id="c"><option>tokyo<option>osaka<option>sapporo</select>
<button id="go">サーバー経由で取得</button><button id="direct">ブラウザから直接2つのAPIを呼ぶ</button>
<p id="w"></p><ul id="ev"></ul><pre id="log"></pre>
<script>
const $ = (s) => document.querySelector(s);
$('#go').onclick = async () => {
const r = await fetch('/api/plan?city=' + $('#c').value); const d = await r.json();
$('#w').textContent = d.weather ? `${d.weather.name}:${d.weather.forecast} ${d.weather.temp_c}℃ 降水確率${d.weather.pop}% / ${d.advice}` : d.advice;
$('#ev').innerHTML = ''; for (const e of d.events ?? []) { const li = document.createElement('li'); li.textContent = `${e.time} ${e.title}${e.indoor ? '(屋内)' : '(屋外)'}`; $('#ev').append(li); }
$('#log').textContent = 'took_ms=' + d.took_ms + ' warnings=' + JSON.stringify(d.warnings);
};
$('#direct').onclick = async () => {
try { const r = await fetch('http://127.0.0.1:18216/events?area=TYO'); $('#log').textContent = await r.text(); }
catch (e) { $('#log').textContent = 'events: ' + e.name + ': ' + e.message; }
};
</script>
Playwright から Chromium でページを開き、両方のボタンを押した結果です。
--- tokyo(サーバー経由)
東京:rain 17℃ 降水確率80% / 雨の予報:屋内のイベントを先に表示
11:00 プラネタリウム特別上映(屋内)
17:00 美術館の夜間開館(屋内)
10:00 屋外マルシェ(屋外)
took_ms=454 warnings=[]
--- osaka(サーバー経由)
大阪:sunny 20℃ 降水確率10% / 晴れ・くもり:屋外のイベントを先に表示
10:00 河川敷フェス(屋外)
16:00 水族館ナイト(屋内)
took_ms=340 warnings=[]
--- ブラウザから直接
events: TypeError: Failed to fetch
[console.error] Access to fetch at 'http://127.0.0.1:18216/events?area=TYO' from origin 'http://127.0.0.1:18217' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
[console.error] Failed to load resource: net::ERR_FAILED
ページのソースに APIキーが含まれるか: false
ポート番号が違うだけでもオリジンは別になります。イベントAPIは Access-Control-Allow-Origin を返さないため、ブラウザはレスポンスをJavaScriptに渡さず、コードに届くのは「Failed to fetch」だけでした。 詳しい理由はコンソールにだけ出ます。一方で、イベントAPI側の受付回数を数えると、このときもリクエストは1回届いていました。CORS は「ブラウザが読ませるかどうか」の仕組みで、サーバーへの到達を止めるものではありません。
APIキーが必要なAPIを組み合わせるとき
ブラウザ向けに発行された公開用キー(参照元の制限つき)だけは、提供元の説明どおりブラウザで使う
| 組む場所 | 向いている場合 | 気をつけること |
|---|---|---|
| ブラウザ(クライアント) | キー不要で、CORS を許可しているAPI。地図などの埋め込み部品 | CORS で読めないAPIがある。キーが見える。利用者ごとに上流を呼ぶ |
| 自分のサーバー(BFF) | キーが要る・CORS が無い・結果をキャッシュしたい | サーバーの運用が増える。サーバーが落ちると全部止まる |
| ノーコードの道具 | 定期実行や通知など、画面の無い組み合わせ | 道具の料金・実行回数の上限・データの置き場所 |
関連記事「CORSエラーの原因と解決方法|エラーメッセージ別に解説」で扱っています。
関連記事「BFF(Backend for Frontend)とは|構成図とNode.js実装」で扱っています。
マッシュアップの速さ:直列・並列・キャッシュを実測
この節では、2つのAPIを順番に呼ぶか同時に呼ぶか、結果を手元に置くかで、応答時間と上流への呼び出し回数がどれだけ変わるかが分かります。
マッシュアップのサーバーを3つの設定で起動し、/api/plan?city=tokyo を21回ずつ呼んで、サーバー内の処理時間の中央値と、天気APIへの呼び出し回数を数えました(この測定のときだけ、天気APIの回数制限を外しています)。天気APIは300ms、イベントAPIは200msかかる設定です。
serial ttl=0 server_median=507ms min=504 max=545 client_median=0.506545s
weather calls: 42
parallel ttl=0 server_median=304ms min=302 max=344 client_median=0.304018s
weather calls: 42
parallel ttl=60000 server_median=204ms min=202 max=334 client_median=0.203313s
weather calls: 1
| 設定 | 応答時間(中央値) | 天気APIの呼び出し | 読み方 |
|---|---|---|---|
| 直列・キャッシュなし | 507ms | 42回/42リクエスト | APIの数だけ遅くなる |
| 並列・キャッシュなし | 304ms | 42回/42リクエスト | いちばん遅いAPIで決まる |
| 並列・キャッシュ60秒 | 204ms | 1回/42リクエスト | イベントAPIの200msだけが残る |
互いに依存しない呼び出しは並列にするだけで約4割速くなり、変わりにくいデータ(天気は60秒ごとに更新という想定)をキャッシュすると上流への呼び出しがほぼ無くなりました。 上流のAPIの回数制限や従量課金を考えると、速さ以上にキャッシュの効果が大きい場面が多くあります。ただし、キャッシュしてよいか・何秒までよいかはAPIの規約で決まっていることがあるため、次の節で確かめます。
利用者が増えたときの上流への呼び出し回数
キャッシュなしのマッシュアップでは、上流への呼び出し回数は「自分のサービスへのリクエスト数 × 組み合わせるAPIの数」になります。今回の42リクエストでも、天気APIに42回・イベントAPIに42回の計84回が流れました。1分に100人が開くページなら、2つのAPIに毎分200回です。天気APIの「10秒に5回」は1分にすると30回までなので、キャッシュが無ければ毎分70回が429になる計算です。
並列化は待ち時間を縮めますが、呼び出し回数は減らしません。回数を減らすのは、キャッシュ、同じ問い合わせをまとめる工夫(同時に来た同じ都市のリクエストを1回の呼び出しで済ませる)、必要な項目だけを取る問い合わせ方です。どれを使えるかは、APIの規約と、データがどのくらいの頻度で変わるかで決まります。
関連記事「JavaScriptのasync/awaitの使い方|エラー処理と並列実行」で扱っています。
マッシュアップで守るAPIの利用規約とレート制限
この節では、他社のAPIを組み合わせるときに確かめる規約の項目と、回数制限(レート制限)に当たったときの実物が分かります。
マッシュアップは他人のサービスの上に作るため、技術より先に規約を読みます。たとえば OpenStreetMap の標準タイルサーバーの利用ポリシー(Tile Usage Policy)では、地図上の帰属表示を隠さないこと、アプリ固有の User-Agent を送ること、サーバーのキャッシュ指示に従うこと、表示しない範囲の一括ダウンロードやオフライン保存をしないことが定められ、サービスはベストエフォートで保証が無いと書かれています。項目はAPIごとに違いますが、確かめる観点はほぼ共通です。規約のほかに、APIの版(v1・v2)と廃止予定の告知、障害情報を出すステータスページの有無も、長く使えるAPIかを見分ける材料になります。
- 1商用利用・用途の制限無料枠は個人・非商用に限る、特定の用途を禁止する、などの条件がある
- 2回数・料金の上限1秒・1分・1日あたりの回数、従量課金の単価、上限を超えたときの扱い
- 3保存・キャッシュの可否と期間結果を保存してよいか、何秒(何日)までか、Cache-Control に従うか
- 4帰属表示(クレジット)提供元の名前やロゴを画面のどこに出すか。隠してはいけない場合がある
- 5他のAPIのデータとの組み合わせ別の提供元の地図や表示と混ぜることを禁じる規約もある
- 6APIキーの扱いと停止・変更の告知キーの公開禁止、仕様変更や提供終了の告知方法と期間
回数制限に当たると、多くのAPIは HTTP 429 Too Many Requests を返します。天気APIを直接7回続けて呼んだ結果です。
$ for i in 1 2 3 4 5 6 7; do curl -s --noproxy '*' -o /dev/null -w "%{http_code} " \
-H 'X-API-Key: demo-key-weather' 'http://127.0.0.1:18215/v1/weather?city=osaka'; done
200 200 200 200 200 429 429
$ curl -s --noproxy '*' -i -H 'X-API-Key: demo-key-weather' 'http://127.0.0.1:18215/v1/weather?city=osaka'
HTTP/1.1 429 Too Many Requests
Content-Type: application/json; charset=utf-8
Retry-After: 9
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
{"error":"rate_limited"}
Retry-After: 9 は「9秒待ってから送ってよい」という意味です(日時で書く形式もあります)。マッシュアップのサーバーは、429 を受けたら Retry-After の間は天気APIを呼ばず、手元に前の値があればそれを返すようにしました。キャッシュを切った(TTL=0)状態で8回続けて呼んだ結果です。
{"n":1,"cached":false,"stale":null,"warnings":[],"took_ms":351}
{"n":2,"cached":false,"stale":null,"warnings":[],"took_ms":317}
{"n":3,"cached":false,"stale":null,"warnings":[],"took_ms":311}
{"n":4,"cached":false,"stale":null,"warnings":[],"took_ms":303}
{"n":5,"cached":false,"stale":null,"warnings":[],"took_ms":306}
{"n":6,"cached":true,"stale":true,"warnings":[],"took_ms":208}
{"n":7,"cached":true,"stale":true,"warnings":[],"took_ms":203}
{"n":8,"cached":true,"stale":true,"warnings":[],"took_ms":203}
# まだ一度も取っていない札幌は、天気なし・イベントだけで返る
{"weather":null,"warnings":["weather: rate limited (waiting Retry-After)"],"events":1}
6回目以降は「少し前の値(stale)」で答え、天気APIには送っていません。最初に書いた版では、Retry-After の待ち時間に入った直後、手元の値を見る前に例外を投げていたため、7回目だけ天気が空になりました。待っている間の分岐でも手元の値を先に見るように直しています。60秒のキャッシュを有効にして100回呼んだときは、100回とも 200 で、天気APIへの呼び出しは1回でした。
関連記事「レート制限の実装アルゴリズム|6方式を同じ条件で動かして比較」で扱っています。
片方のAPIが止まったときのマッシュアップの作り方
この節では、組み合わせたAPIの1つが止まる・遅い・壊れた応答を返すときに、マッシュアップ全体を止めない作り方が、実際の応答で分かります。
マッシュアップの最大の弱点は、自分では直せない他人のAPIに依存することです。イベントAPIを止めたり、わざと遅くしたりして、マッシュアップのサーバーの応答を記録しました。
# 1. イベントAPIを止めた
{"weather":"rain","advice":"雨の予報:屋内のイベントを先に表示","events":null,"warnings":["events: fetch failed"],"took_ms":324}
# 2. イベントAPIが3秒かかる(タイムアウトは1.5秒)
{"events":null,"warnings":["events: The operation was aborted due to timeout"],"took_ms":1503}
# 3. イベントAPIがHTMLのエラーページ(502)を返す
{"events":null,"warnings":["events: HTTP 502 http://127.0.0.1:18216/events"]}
# 4. イベントAPIが項目名を title から name に変えた
{"events":[{"title":"(名称なし)","indoor":true,"time":"11:00"},{"title":"(名称なし)","indoor":true,"time":"17:00"},{"title":"(名称なし)","indoor":false,"time":"10:00"}],"warnings":[]}
# 5. 天気APIのキーが違う
{"weather":null,"advice":"天気が取れないため並べ替えなし","warnings":["weather: HTTP 401 http://127.0.0.1:18215/v1/weather"]}
# 6. 両方とも止めた
{"city":"osaka","weather":null,"advice":"天気が取れないため並べ替えなし","events":null,
"warnings":["weather: fetch failed","events: fetch failed"],"took_ms":3}
HTTP 502
Promise.allSettled は、片方が失敗しても両方の結果(成功なら値、失敗なら理由)を待って返します。Promise.all だと1つの失敗で全体が失敗になり、天気が取れているのに画面が空になります。1〜3と5では、取れたほうだけで答え、取れなかった理由を warnings に残せました。 タイムアウトを付けないと、2のような遅いAPIに引きずられて、利用者は3秒以上待たされます。
注意したいのは4です。項目名が変わっても、エラーにならず「(名称なし)」が黙って並びました。warnings も空です。仕様変更は止まるより気づきにくいため、受け取ったデータに必要な項目があるかを確かめる2行を足しました(上の mashup.mjs に入っている「形の確認」の行です)。
if (ev?.items?.some(e => typeof e.title !== 'string' || typeof e.start !== 'number'))
warnings.push('events: unexpected shape (title/start missing)'); // 形の確認
# 項目名が name に変わったイベントAPIで、もう一度呼んだ
{"events":[{"title":"(名称なし)","indoor":false,"time":"10:00"},{"title":"(名称なし)","indoor":true,"time":"16:00"}],
"warnings":["events: unexpected shape (title/start missing)"]}
# 元のイベントAPIに戻した
{"warnings":[]}
今度は warnings に理由が出ました。実際のサービスでは、この警告をログに残して通知し、画面には「イベント名を取得できませんでした」のように出します。項目の数が多いAPIでは、JSON Schema や Zod などの検証ライブラリで受け取る形を決めておくと、確認の漏れが減ります。
- 1タイムアウトを付けるAbortSignal.timeout(1500) など。待ち時間の上限を自分で決める
- 2ステータスを見てからJSONとして読むr.ok を確かめる。HTMLのエラーページを JSON.parse しない
- 3片方の失敗で全体を止めないallSettled で取れたほうだけ返し、warnings に理由を残す
- 4キャッシュと前の値を使う429 や障害のときは少し前の値で答え、古さを表示する
- 5データの形を確かめる必要な項目が無ければ警告する。仕様変更に早く気づく
マッシュアップのよくあるエラーとつまずき
この節では、APIを組み合わせるときに出るエラーの原文と、原因・直し方が一覧で分かります。
| エラー原文 | 出た場面 | 原因 | 直し方 |
|---|---|---|---|
TypeError: fetch failed(cause: connect ECONNREFUSED 127.0.0.1:18216) |
Node.js の fetch | 相手のAPIが止まっている・ポートやURLが違う | error.cause.code を見る。URLを確かめ、取れたほうだけで返す |
TimeoutError: The operation was aborted due to timeout |
AbortSignal.timeout を付けた fetch | 相手が指定時間内に応答しない | 時間を決めて打ち切り、キャッシュや部分的な結果で答える |
SyntaxError: Unexpected token '<', "<html><bod"... is not valid JSON |
r.json() で読んだとき | 502などのHTMLのエラーページをJSONとして読んだ | r.ok とContent-Typeを先に確かめる |
JSONDecodeError: Expecting value: line 1 column 1 (char 0) |
Python の json.loads | 同じく、JSON以外の本文を読んだ | ステータスを確かめてから読む |
HTTPError: HTTP Error 502: Bad Gateway |
Python の urllib | 相手側の障害。urllib は4xx・5xxで例外にする | 例外を捕まえ、取れたほうだけで返す |
HTTP 401 {"error":"invalid_api_key"} |
キー必須のAPI | キーが無い・違う・期限切れ | 環境変数の名前と値、キーの有効範囲を確かめる |
HTTP 429 Retry-After: 9 |
短時間に何度も呼んだ | 回数制限を超えた | Retry-After の間は呼ばない。キャッシュを入れる |
No 'Access-Control-Allow-Origin' header is present on the requested resource. |
ブラウザから別オリジンのAPIを直接呼んだ | APIが自分のオリジンを許可していない | 自分のサーバー経由で呼ぶ(BFF) |
KeyError: 'WEATHER_API_KEY' |
Python で環境変数を読んだ | キーを環境変数で渡し忘れた | 起動時に渡す。無ければ分かる文言で止める |
| エラーは出ず「(名称なし)」が並ぶ | 相手が項目名を変えた | 仕様変更をコードが知らない | 必要な項目の有無を確かめ、無ければ警告する |
1fetch failed は理由が奥にある
message は「fetch failed」だけで、ECONNREFUSED などの本当の理由は error.cause に入っていた。ログには cause も出す。
2ブラウザでは理由が読めない
CORS で止まったとき、JavaScript に届くのは「Failed to fetch」だけ。理由は開発者ツールのコンソールで見る。
3いちばん怖いのは静かな変化
止まる・遅いは警告に出たが、項目名の変更はエラーにならなかった。形の確認を入れる。
マッシュアップとWebhook・REST API・BFF・ノーコードの違い
この節では、マッシュアップと一緒に出てくる言葉との関係と、どの記事で何を扱っているかが分かります。
マッシュアップは「複数のサービスを組み合わせて新しいものを作る」という考え方の名前で、特定の技術の名前ではありません。周りの言葉は、その部品や道具にあたります。
| 言葉 | 何か | マッシュアップとの関係 |
|---|---|---|
| Web API・REST API | 外部からデータや機能を呼び出す窓口とその設計の流儀 | 組み合わせる材料。マッシュアップはAPIを呼ぶ側 |
| Webhook | 出来事が起きたときに相手から自分へ届く通知 | 「呼びに行く」でなく「知らせてもらう」組み合わせ方 |
| BFF | 画面ごとに用意する、APIをまとめる中継サーバー | サーバー側で組むマッシュアップの置き場所 |
| ノーコード・iPaaS(n8n・Zapier・Make など) | コードを書かずにサービス同士をつなぐ道具 | マッシュアップを組み立てる道具の1つ |
| API連携・システム連携 | システム同士でデータをやり取りすること | ほぼ同じ意味で使われる。新しいサービスを作る意図が強いとマッシュアップと呼ぶ |
ノーコードの道具は、「毎朝天気を取って、雨ならチャットに屋内イベントを送る」のような画面の無い組み合わせを、ブロックをつなぐだけで作れます。今回のように画面に出す・細かい並べ替えや障害時の扱いを決めたい場合はコードで、定期実行や通知が中心なら道具で、と使い分けるのが一般的です。道具を使う場合も、APIの規約・回数制限・キーの置き場所を確かめる点は変わりません。
Webhook は、マッシュアップと組み合わせると呼び出し回数を減らす役にも立ちます。今回の例なら、イベントAPIが「イベントが追加・変更された」ことを Webhook で知らせてくれる場合、マッシュアップ側はイベント一覧を長めにキャッシュしておき、通知が来たときだけ捨てて取り直せます。毎回問い合わせる(ポーリング)より上流の負担が小さく、変更も早く反映されます。通知を受ける側の作り方(署名の検証・再送・二重処理の防止)は、次の記事で扱っています。
関連記事「Webhookとは?実装までわかる|署名検証・リトライ・冪等性を実行ログで解説」で扱っています。
マッシュアップのメリット・デメリットと判断表
この節では、マッシュアップで作るか、自前で作るかを決めるときの材料が分かります。
| 観点 | メリット | デメリット |
|---|---|---|
| 作る速さ・費用 | 地図や天気など大きなデータを自前で用意せずに済む | 従量課金のAPIは利用者が増えるほど費用が増える |
| 品質 | 各分野の専門の提供者のデータを使える | データの正しさ・更新頻度を自分では決められない |
| 変更 | 部品を差し替える・足すのが比較的簡単 | 相手の仕様変更・値上げ・提供終了に合わせて直す必要がある |
| 可用性 | 相手が冗長化していれば自前より止まりにくいことも | どれか1つが止まると機能が欠ける。今回は片方停止で部分表示 |
| 権利・規約 | 規約の範囲で正式に使える | 保存禁止・表示義務・組み合わせの禁止などの制約を受ける |
何を組み合わせるか決まったら、次の表で作り方を選びます。
| 状況 | 選ぶ方法 | 理由 |
|---|---|---|
| キー不要・CORS許可のAPIを画面に出すだけ | ブラウザで組む | サーバーが要らず最も手軽 |
| APIキーが要る・CORS が無い | 自分のサーバー(BFF)で組む | キーを隠せる。CORS の制約を受けない |
| 互いに依存しない複数のAPI | 並列に呼ぶ(allSettled) | 実測で507ms→304ms。片方の失敗で止まらない |
| Aの結果でBへの問い合わせが変わる | 順番に呼ぶ(ロジックマッシュアップ) | 並列にはできない。Bの取得量は減る |
| 同じ問い合わせが多い・回数制限が厳しい | 規約の範囲でキャッシュ | 実測で呼び出しが42回→1回 |
| 定期実行や通知が中心で画面が無い | ノーコードの道具も候補 | 作る量が少ない。料金と上限を確かめる |
| そのAPIが止まると業務が止まる | 代わりのAPIか自前のデータも用意 | 依存先を1つにしない |
マッシュアップのよくある質問
この節では、マッシュアップについてよく聞かれる質問への答えが分かります。
Q. マッシュアップを一言でいうと何ですか?
複数のWebサービスやAPIを組み合わせて、新しいサービスを作ることです。もとは複数の曲を重ねて1曲にする音楽の手法の名前です。
Q. マッシュアップの具体例は?
地図に飲食店や天気を重ねて表示するサービスが代表例です。この記事では、天気APIとイベントAPIを組み合わせ、雨なら屋内イベントを先に出すアプリを作りました。
Q. マッシュアップとAPI連携は同じですか?
ほぼ同じ場面で使われます。API連携はシステム同士のやり取り全般を指し、マッシュアップは組み合わせて新しいサービスを作ることに重きを置いた言い方です。
Q. プログラミングができなくてもマッシュアップは作れますか?
地図などの埋め込み部品をページに貼る、ノーコードの道具でサービスをつなぐ、といった方法なら作れます。画面の細かい動きや障害時の扱いまで決めたい場合はコードが向いています。
Q. ブラウザのJavaScriptだけで作れますか?
APIが CORS を許可していて、キーが不要(またはブラウザ用の公開キー)なら作れます。今回のイベントAPIのように CORS ヘッダーが無いと、リクエストは届いてもブラウザが結果を読ませません。
Q. APIキーはどこに置けばよいですか?
自分のサーバーの環境変数などに置き、ブラウザには組み合わせた結果だけを返します。Git に入れない運用もあわせて決めます。
Q. 429 Too Many Requests が出たらどうしますか?
Retry-After の秒数(または日時)まで待ってから送ります。同じ問い合わせが多いなら、規約の範囲でキャッシュを入れると呼び出し自体が減ります。今回は42回が1回になりました。
Q. APIの結果を保存(キャッシュ)してよいですか?
APIごとの規約で決まります。保存を禁じる、期間を限る、Cache-Control に従うよう求める、などの条件があるため、使う前に確かめます。
Q. マッシュアップしたページが遅いときは、どこから見ますか?
まず各APIを単体で curl して時間を測り、順番に呼んでいないかを確かめます。今回は直列の507msが並列で304ms、キャッシュありで204msになりました。残った時間は、いちばん遅いAPIの応答時間です。
Q. 組み合わせたAPIが提供終了したら?
その機能は使えなくなります。差し替えやすいよう、APIを呼ぶ部分を1か所にまとめ、代わりの提供元を調べておくと影響を小さくできます。
Q. 音楽のマッシュアップを動画サイトに上げてもよいですか?
既存の曲や音源を使う場合、著作権や音源の権利に関わります。権利者の許諾や、投稿先のサービスの規約を確かめてから判断します。
Q. 試験ではどう問われますか?
ITパスポート試験や基本情報技術者試験で、「公開されているWebサービスを組み合わせて新しいサービスを作る手法」を選ぶ問題や、ロジックマッシュアップの例を選ぶ問題が出ています。
まとめ
- マッシュアップとは、複数のWebサービス・APIを組み合わせて新しいサービスを作る手法。もとは複数の曲を重ねる音楽の言葉で、既存曲を使う音楽のマッシュアップには権利の確認が要る。
- 組み合わせ方は表示・データ・ロジックの3つで説明される。データの組み合わせでは、都市コードや時刻の形式を揃える処理がコードの中心になった。
- APIキーが要る・CORS が無いAPIはサーバー側で組む。ブラウザから直接呼ぶと、リクエストは届いても「Failed to fetch」で結果を読めなかった。
- 並列化で507ms→304ms、キャッシュで上流の呼び出しが42回→1回。規約の範囲で呼び出しを減らすことが、速さと回数制限の両方に効く。
- 他人のAPIは止まる・遅れる・形が変わる。タイムアウト、ステータスの確認、allSettled での部分表示、データの形の確認を最初から入れる。
参考・出典
確認日はいずれも 2026年10月29日です。
- MDN「Cross-Origin Resource Sharing (CORS)」:https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- MDN「Retry-After header」:https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After
- MDN「Promise.allSettled()」:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
- MDN「AbortSignal: timeout() static method」:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
- OpenStreetMap Foundation「Tile Usage Policy」:https://operations.osmfoundation.org/policies/tiles/
- 日本法令外国語訳データベースシステム「著作権法(昭和四十五年法律第四十八号)」:https://www.japaneselawtranslation.go.jp/ja/laws/view/4207
- IT用語辞典 e-Words「マッシュアップ」(情報処理技術者試験の出題履歴):https://e-words.jp/w/マッシュアップ.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「マッシュアップとは?Web APIを組み合わせる仕組みと作り方を実演」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。