Contents
Pexels API の取得と安全なキー管理
Pexels の画像検索機能を自プロジェクトに組み込む際は、まず公式サイトで API キーを取得し、安全に保管することが最重要です。キーが漏洩すると不正利用やレートリミット超過の原因になるため、環境変数やシークレット管理サービスを活用して「コードに埋め込まない」設計を徹底しましょう。
アカウント登録手順
公式サイト(https://www.pexels.com/api/) の指示に沿ってアカウントと API キーを取得します。以下の流れで作業できます。
- サインアップ – メールまたは Google アカウントで無料会員登録
- ダッシュボードへ移動 – ログイン後、左側メニューの「API」セクションを開く
- キー発行 –
Create New API Keyをクリックし、表示された文字列をコピー
⚠️ 取得したキーは機密情報です。リポジトリやフロントエンドコードに直接埋め込まないよう必ず注意してください。
環境変数での保存方法
Node.js / Next.js のプロジェクトでは、.env.local にキーを記述し .gitignore で除外します。これだけでローカルと本番環境両方で同一コードが利用できます。
|
1 2 3 |
# .env.local(プロジェクトルート) PEXELS_API_KEY=YOUR_GENERATED_KEY_HERE |
アプリ側では process.env.PEXELS_API_KEY を参照すれば OK です。
シークレット管理サービスの活用例
CI/CD パイプラインやサーバーレス環境では、以下のマネージドサービスでシークレットを保管すると安全性が向上します。設定画面から「Environment Variables」または「Secrets」に PEXELS_API_KEY を登録してください。
| サービス | 設定手順の概要 |
|---|---|
| Vercel | Project Settings → Environment Variables にキーを追加 |
| Netlify | Site settings → Build & deploy → Environment → Add variable |
| AWS Secrets Manager | aws secretsmanager create-secret で作成し、Lambda の環境変数へ注入 |
いずれも「コードにキーを書かない」原則を守れるため、実務での採用が推奨されます。
認証ヘッダーとリクエスト構築
Pexels API は Authorization: Bearer {API_KEY} ヘッダーで認証します。ここではヘッダー生成関数を共通化し、fetch と axios の両方で利用できる実装例をご紹介します。
Authorization ヘッダー取得関数
ヘッダー作成ロジックをユーティリティに切り出すことで再利用性が向上します。以下は TypeScript/JavaScript 共通のシンプルな実装です。
|
1 2 3 4 5 |
// utils/pexelsClient.js export const getAuthHeader = () => ({ Authorization: `Bearer ${process.env.PEXELS_API_KEY}`, }); |
fetch と axios の実装例
fetch を使う場合(Node ≥18 またはブラウザ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { getAuthHeader } from './utils/pexelsClient'; export const searchImagesFetch = async (query, page = 1, perPage = 15) => { const url = new URL('https://api.pexels.com/v1/search'); url.searchParams.append('query', query); url.searchParams.append('page', page); url.searchParams.append('per_page', perPage); const response = await fetch(url, { headers: getAuthHeader() }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }; |
axios を使う場合(Node またはブラウザ)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import axios from 'axios'; import { getAuthHeader } from './utils/pexelsClient'; export const searchImagesAxios = (query, page = 1, perPage = 15) => { return axios .get('https://api.pexels.com/v1/search', { headers: getAuthHeader(), params: { query, page, per_page: perPage }, }) .then(res => res.data) .catch(err => { if (err.response) throw new Error(`HTTP ${err.response.status}`); throw err; }); }; |
Next.js API Route でのサーバー側呼び出し
API Route にリクエストを集約すれば、フロントエンドからはキーが完全に隠蔽されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// pages/api/pexels-search.js import { searchImagesAxios } from '../../utils/pexelsClient'; export default async function handler(req, res) { const { query, page = '1', per_page = '15' } = req.query; try { const data = await searchImagesAxios(query, Number(page), Number(per_page)); res.status(200).json(data); } catch (e) { console.error(e); res.status(502).json({ error: e.message }); } } |
フロントエンドは fetch('/api/pexels-search?...') と呼び出すだけで、キー漏洩リスクを回避できます。
画像検索エンドポイントとフロントエンド連携
Pexels の画像検索は /v1/search エンドポイントで提供されます。本章ではパラメータの意味を整理し、Next.js API Route と React フック・コンポーネントの組み合わせで安全にデータ取得する手順を示します。
クエリパラメータ概要
検索時に指定できる主なオプションです。公式ドキュメント(https://www.pexels.com/api/) を参照してください。
| パラメータ | 説明 | 例 |
|---|---|---|
query |
検索キーワード(必須) | nature |
per_page |
1 リクエストあたり取得件数(最大 80) | 30 |
page |
ページ番号(1 始まり) | 2 |
orientation |
landscape, portrait, square のいずれか |
portrait |
size |
large, medium, small |
large |
Next.js API Route の利用例
先ほど作成した /api/pexels-search をフロントエンドから呼び出すだけで、検索結果が取得できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// hooks/usePexelsSearch.js import { useState, useEffect } from 'react'; export const usePexelsSearch = (term, page = 1) => { const [photos, setPhotos] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!term) return; setLoading(true); fetch( `/api/pexels-search?query=${encodeURIComponent(term)}&page=${page}&per_page=15` ) .then(res => res.json()) .then(data => { setPhotos(data.photos); setError(null); }) .catch(err => setError(err.message)) .finally(() => setLoading(false)); }, [term, page]); return { photos, loading, error }; }; |
コンポーネントでの検索 UI
以下は検索入力と結果一覧を表示するシンプルなコンポーネントです。ページングボタンも備えています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// components/ImageSearch.tsx import { useState } from 'react'; import { usePexelsSearch } from '../hooks/usePexelsSearch'; export const ImageSearch = () => { const [term, setTerm] = useState(''); const [page, setPage] = useState(1); const { photos, loading, error } = usePexelsSearch(term, page); const handleSubmit = e => { e.preventDefault(); setPage(1); // 新規検索時はページリセット }; return ( <div> <form onSubmit={handleSubmit}> <input placeholder="キーワードを入力" value={term} onChange={e => setTerm(e.target.value)} /> <button type="submit" disabled={loading}>検索</button> </form> {error && <p className="error">エラー: {error}</p>} <div className="grid"> {photos.map(p => ( <a key={p.id} href={p.url} target="_blank" rel="noopener noreferrer" > <img src={p.src.medium} alt={p.alt || 'Pexels image'} /> </a> ))} </div> {photos.length > 0 && ( <button onClick={() => setPage(prev => prev + 1)} disabled={loading}> 次のページ </button> )} </div> ); }; |
この構成により、フロントエンドは API Route 経由でキーが隠蔽された状態で画像検索を利用できます。
レスポンス解析と UI 活用
Pexels が返す JSON は階層化されており、必要な情報だけを抽出して UI に組み込むことがポイントです。ここでは主要フィールドの概要と、React で扱いやすい形への変換例をご紹介します。
JSON 構造と重要フィールド
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{ "total_results": 10000, "page": 1, "per_page": 15, "photos": [ { "id": 2014422, "width": 3024, "height": 4032, "url": "https://www.pexels.com/photo/...", "photographer": "John Doe", "photographer_url": "https://www.pexels.com/@johndoe", "src": { "original": ".../original.jpg", "large2x": ".../large2x.jpg", "large": ".../large.jpg", "medium": ".../medium.jpg", "small": ".../small.jpg", "portrait": ".../portrait.jpg", "landscape": ".../landscape.jpg", "tiny": ".../tiny.jpg" }, "alt": "A beautiful forest" } ], "next_page": "https://api.pexels.com/v1/search?...&page=2" } |
photos配列が画像情報本体srcオブジェクトにサイズ別 URL が格納されているので、表示用途に合わせて選択可能
必要情報だけを抽出する方法
React コンポーネントで扱うデータは以下の形に整形するとシンプルです。
|
1 2 3 4 5 6 7 8 9 |
const formatted = photos.map(p => ({ id: p.id, thumbnail: p.src.medium, // カード表示用 fullSize: p.src.large2x, // 詳細ビュー用 photographer: p.photographer, photographerUrl: p.photographer_url, alt: p.alt ?? '', })); |
ギャラリーコンポーネント例
サムネイル一覧とクリック時にモーダルで拡大表示する最小構成です。アクセシビリティを意識し、alt 属性は必ず設定しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { useState } from 'react'; export const PhotoGallery = ({ photos }) => { const [selected, setSelected] = useState(null); return ( <> <div className="grid"> {photos.map(p => ( <img key={p.id} src={p.thumbnail} alt={p.alt} onClick={() => setSelected(p)} className="cursor-pointer" /> ))} </div> {selected && ( <div className="modal" onClick={() => setSelected(null)}> <img src={selected.fullSize} alt={selected.alt} /> <p> Photo by{' '} <a href={selected.photographerUrl} target="_blank" rel="noopener noreferrer" > {selected.photographer} </a>{' '} on{' '} <a href="https://www.pexels.com" target="_blank" rel="noopener noreferrer"> Pexels </a> </p> </div> )} </> ); }; |
データ抽出と UI 表示を分離すれば、将来的に新しいサイズや追加フィールドが増えても容易に拡張できます。
レートリミット・ページング・エラーハンドリング
実運用で安定させるためには、公式ドキュメントに記載されているリクエスト上限と、超過時の対策をしっかり設計する必要があります。
公式のリクエスト上限
2024 年 6 月時点の Pexels API 公式ページ(https://www.pexels.com/api/) によると、無料プランは月間 20,000 リクエスト が上限です。1 時間あたりの上限は 200 リクエストで、超過すると 429 Too Many Requests が返ります(有料プランに切り替えると上限が大幅に緩和されます)。
ページング実装パターン
無限スクロールや「次へ」ボタンでページ番号を管理する際は、取得済み件数と総件数を比較しつつリクエスト回数を抑える工夫が必要です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// hooks/useInfinitePexels.js import { useState, useEffect } from 'react'; import axios from 'axios'; export const useInfinitePexels = term => { const [photos, setPhotos] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const fetchPage = async p => { setLoading(true); try { const { data } = await axios.get('/api/pexels-search', { params: { query: term, page: p, per_page: 30 }, }); setPhotos(prev => [...prev, ...data.photos]); setHasMore(Boolean(data.next_page)); } finally { setLoading(false); } }; useEffect(() => { if (!term) return; // 新しい検索語ならリセット setPhotos([]); setPage(1); fetchPage(1); }, [term]); const loadMore = () => { if (loading || !hasMore) return; const next = page + 1; setPage(next); fetchPage(next); }; return { photos, loading, hasMore, loadMore }; }; |
指数バックオフと再試行ロジック
429 が返った場合は待機時間を伸ばす「指数バックオフ」を実装すると、短期間のリクエスト集中による失敗を緩和できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const MAX_RETRIES = 5; async function fetchWithBackoff(url, opts, attempt = 0) { try { const res = await fetch(url, opts); if (!res.ok && res.status === 429) throw new Error('Rate limit'); return res; } catch (err) { if (attempt >= MAX_RETRIES) throw err; const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s … await new Promise(r => setTimeout(r, delay)); return fetchWithBackoff(url, opts, attempt + 1); } } |
axios インターセプタで統一的エラー処理
全リクエストに対して共通のハンドリングを入れれば、個別 try/catch の記述が減り、ユーザーには「再試行中」や「しばらく待ってください」といったフィードバックが提供できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import axios from 'axios'; const api = axios.create({ baseURL: '/api', timeout: 8000 }); api.interceptors.response.use( res => res, async err => { const { config, response } = err; if (!config || !response) return Promise.reject(err); // 429 または 5xx 系エラーはリトライ対象 if (response.status === 429 || (response.status >= 500 && response.status < 600)) { config.__retryCount = config.__retryCount ?? 0; if (config.__retryCount >= 3) return Promise.reject(err); const delay = Math.pow(2, config.__retryCount) * 1000; await new Promise(r => setTimeout(r, delay)); config.__retryCount += 1; return api(config); } // 4xx 系はそのまま上位へ伝搬 return Promise.reject(err); } ); export default api; |
このインターセプタをプロジェクト全体で使うことで、レートリミットや一時的なサーバ障害への耐性が格段に向上します。
利用規約・クレジット表記と有料プランの判断基準
Pexels の画像は商用利用も可能ですが、撮影者名と Pexels へのリンク が必須です。ここでは正しいクレジット表記方法と、有料プランへ移行すべきシーンを整理します。
必須クレジット表記
公式ドキュメント(https://www.pexels.com/api/) に基づくと、最低でも以下の情報が必要です。
|
1 2 |
Photo by {photographer} on Pexels |
HTML 実装例:
|
1 2 3 4 5 |
<p> Photo by <a href="https://www.pexels.com/@johndoe" target="_blank">John Doe</a> on <a href="https://www.pexels.com" target="_blank">Pexels</a> </p> |
画像カードやモーダルのフッターに必ず埋め込むようにしてください。
商用利用時の注意点
| 項目 | 内容 |
|---|---|
| 再配布禁止 | 取得した画像をそのまま販売・再配布しないこと |
| 加工は許可 | サイズ変更や色調整は自由だが、クレジット表記は残す必要あり |
| 商標利用制限 | 「Pexels」ロゴの使用はガイドラインに従うこと |
無料枠を超える場合のプラン比較
無料プラン(月間 20,000 リクエスト)で足りないケースでは、以下の指標で有料プランへの移行を検討してください。
| 判定ポイント | 無料プランでの課題 | 有料プランのメリット |
|---|---|---|
| リクエスト数 | 20k/Month 超過で 429 エラー | 上限解除(例:200k/Month) |
| 画像解像度 | 大サイズ取得が制限されることあり | オリジナルフル解像度へのアクセス |
| サポート | コミュニティベースのみ | SLA と専用サポート窓口 |
| 優先キュー | 高トラフィック時に遅延が発生 | 優先処理で低レイテンシ |
代替サービスとの比較
プロジェクトの要件次第で他社画像 API の併用も検討できます。
| サービス | 無料リクエスト上限 | クレジット必須 | 主な特徴 |
|---|---|---|---|
| Pexels | 20,000 / 月 | 必要 | 高品質写真が豊富、API がシンプル |
| Unsplash | 5,000 / 月 | 推奨(明示的に要求なし) | デザイナー向けビジュアルが多い |
| Pixabay | 5,000 / 日 | 不要 | イラスト・ベクターも多数、商用利用が自由 |
代替サービスはライセンスやリミットが異なるため、プロジェクトのスケールと画像ジャンルに合わせて最適な組み合わせを選びましょう。
まとめ
- キー取得 & 安全管理:公式サイト(https://www.pexels.com/api/)でキーを発行し、
.envや Vercel / Netlify のシークレットに保管。コードへの埋め込みは絶対に避ける。 - 認証ヘッダー:
Authorization: Bearer {KEY}をユーティリティ関数で統一し、fetchとaxios両方で利用可能にする。Next.js API Route でサーバー側に隠蔽すればフロントエンドは安全に呼び出せる。 - 検索エンドポイント:クエリパラメータを理解し、React フックとコンポーネントでシンプルかつ再利用可能な UI を構築。ページングや無限スクロールも同様のフローで実装できる。
- レスポンス解析:
photos[i].src.{size}から必要サイズだけを抽出し、クレジット情報と共にカード・モーダル UI に組み込む。データ整形はコンポーネントから分離して保守性を確保。 - レートリミット対策:無料プランは月間 20,000 リクエスト、1 時間あたり 200 リクエストが上限。指数バックオフと axios インターセプタで 429 / 5xx エラーに自動リトライし、ユーザー体験を損なわない設計を行う。
- 利用規約遵守:必ず撮影者名と Pexels へのリンクをクレジット表記。商用利用時は再配布禁止・加工許可のルールを守る。上限超過や高解像度が必要な場合は有料プラン、または Unsplash / Pixabay といった代替サービスを比較検討する。
これらのベストプラクティスに沿って実装すれば、Pexels API を安全・効率的にプロダクション環境へ組み込むことができます。ぜひ本稿を参考に、品質とセキュリティを両立した画像検索体験を提供してください。