2026年7月13日

2026年7月13日

WordPressのREST APIにVanilla JSのfetch()でアクセスする方法

jQuery やビルドツールを使わずに WordPress の REST API を呼び出すなら、ブラウザー標準の fetch() で十分です。公開データの取得は URL を組み立てて GET するだけで、ログイン中のユーザーとして書き込む場合は、PHP から渡した nonce を X-WP-Nonce ヘッダーに付けます。この記事では、エンドポイントの URL を安全に JavaScript へ渡す方法、投稿一覧の取得とページ送り、独自エンドポイントへの POST、エラー処理と、つまずきやすい認証・キャッシュの問題までを順に説明します。

全体の流れ

フロントエンドの JavaScript から REST API を使う手順は、大きく三つです。

  1. PHP 側でスクリプトを読み込み、エンドポイントの URL と(必要なら)nonce を JavaScript に渡す
  2. JavaScript で fetch() を呼び、レスポンスの状態を確かめてから JSON を読む
  3. 書き込みが必要なら、権限チェック付きの独自エンドポイントを用意し、nonce 付きで POST する

コードは子テーマか独自プラグインに置き、ステージング環境で動作を確かめてから本番に反映してください。

URL と nonce を PHP から渡す

REST API のルート URL はサイトごとに異なります。/wp-json/ と決め打ちにすると、サブディレクトリにインストールしたサイトや、パーマリンクが「基本」設定で ?rest_route= 形式になるサイトで動きません。PHP の rest_url() で生成した URL を渡すのが安全な方法です。

add_action( 'wp_enqueue_scripts', function () {
	wp_enqueue_script(
		'my-rest-fetch',
		get_stylesheet_directory_uri() . '/js/my-rest-fetch.js',
		array(),
		'1.0.0',
		array(
			'in_footer' => true,
			'strategy'  => 'defer',
		)
	);

	$settings = array(
		'postsUrl'    => esc_url_raw( rest_url( 'wp/v2/posts' ) ),
		'bookmarkUrl' => esc_url_raw( rest_url( 'myplugin/v1/bookmark' ) ),
		'nonce'       => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
	);
	wp_add_inline_script(
		'my-rest-fetch',
		'window.myRestFetch = ' . wp_json_encode( $settings ) . ';',
		'before'
	);
} );
  • **wp_add_inline_script の before**:本体のスクリプトより前に設定オブジェクトを出力します。wp_json_encode() を通すことで、文字列に含まれる記号も JavaScript として安全な形になります
  • **strategy 引数**:WordPress 6.3 で追加された引数で、defer を指定すると描画を妨げずに読み込めます
  • **nonce のアクション名は wp_rest**:REST API のクッキー認証は、このアクション名で作った nonce を検証します。ログインしていない訪問者には不要なので空にしています

公開データを GET で取得する

投稿一覧のような公開データは、認証なしで取得できます。

( function () {
	const settings = window.myRestFetch;
	const list = document.querySelector( '#latest-posts' );
	if ( ! settings || ! list ) {
		return;
	}

	// title.rendered などに含まれる文字参照(’ など)を、HTMLとして解釈せずに文字へ戻す
	function decodeText( html ) {
		return new DOMParser().parseFromString( html, 'text/html' ).body.textContent || '';
	}

	async function loadPosts( page = 1 ) {
		const url = new URL( settings.postsUrl );
		url.searchParams.set( 'per_page', '5' );
		url.searchParams.set( 'page', String( page ) );
		url.searchParams.set( '_fields', 'id,link,title,date' );

		const controller = new AbortController();
		const timer = setTimeout( () => controller.abort(), 10000 ); // 10秒で打ち切り

		try {
			const res = await fetch( url, { signal: controller.signal } );
			if ( ! res.ok ) {
				throw new Error( 'HTTP ' + res.status );
			}
			const posts = await res.json();
			const totalPages = Number( res.headers.get( 'X-WP-TotalPages' ) || 1 );

			list.replaceChildren(
				...posts.map( ( post ) => {
					const li = document.createElement( 'li' );
					const a = document.createElement( 'a' );
					a.href = post.link;
					a.textContent = decodeText( post.title.rendered );
					li.append( a );
					return li;
				} )
			);
			return totalPages;
		} catch ( error ) {
			list.textContent = '記事を読み込めませんでした。';
			console.error( error );
			return 0;
		} finally {
			clearTimeout( timer );
		}
	}

	loadPosts();
} )();

この例で意識しているポイントです。

URL は URL オブジェクトで組み立てる。 searchParams.set() を使うと、値のエンコードを任せられます。rest_url() が ?rest_route= 形式の URL を返す環境でも、既存のクエリを壊さずにパラメーターを追加できます。

**_fields で必要な項目だけを受け取る。** 投稿のレスポンスには本文や多数のメタ情報が含まれます。表示に使う項目だけを指定すると、転送量とサーバーの処理を減らせます。アイキャッチ画像や投稿者名も必要なら _embed パラメーターを使う方法がありますが、レスポンスが大きくなる点に注意します。

**res.ok を先に確かめる。** fetch() は 404 や 500 が返っても例外を投げず、ネットワーク自体が失敗したときだけ例外になります。ステータスを確かめずに res.json() を呼ぶと、エラーの原因が分かりにくくなります。

ページ送りはレスポンスヘッダーを使う。 総件数は X-WP-Total、総ページ数は X-WP-TotalPages ヘッダーで返されます。1 回に取得できる件数(per_page)の上限は 100 です。

innerHTML に入れない。 API から受け取った文字列を innerHTML に入れると、内容によってはスクリプトの埋め込みにつながります。textContent と createElement で組み立てるのが安全です。

ログイン中のユーザーとして POST する

書き込みには、権限チェックと入力の検証を備えた独自エンドポイントを用意します。例として、ログイン中のユーザーが記事をブックマークできるエンドポイントを作ります。

add_action( 'rest_api_init', function () {
	register_rest_route( 'myplugin/v1', '/bookmark', array(
		'methods'             => WP_REST_Server::CREATABLE, // POST
		'callback'            => 'myplugin_save_bookmark',
		'permission_callback' => function () {
			return is_user_logged_in();
		},
		'args'                => array(
			'post_id' => array(
				'required'          => true,
				'type'              => 'integer',
				'minimum'           => 1,
				'validate_callback' => 'rest_validate_request_arg',
				'sanitize_callback' => 'absint',
			),
		),
	) );
} );

function myplugin_save_bookmark( WP_REST_Request $request ) {
	$post_id = $request->get_param( 'post_id' );
	if ( 'publish' !== get_post_status( $post_id ) ) {
		return new WP_Error( 'myplugin_invalid_post', '対象の記事が見つかりません。', array( 'status' => 404 ) );
	}

	$user_id   = get_current_user_id();
	$bookmarks = get_user_meta( $user_id, 'myplugin_bookmarks', true );
	$bookmarks = is_array( $bookmarks ) ? $bookmarks : array();
	$bookmarks = array_values( array_unique( array_merge( $bookmarks, array( $post_id ) ) ) );
	update_user_meta( $user_id, 'myplugin_bookmarks', $bookmarks );

	return rest_ensure_response( array( 'bookmarks' => $bookmarks ) );
}

permission_callback は省略せずに書きます。WordPress 5.5 以降、省略するとデバッグ時に注意が出るうえ、誰でも呼べるエンドポイントになってしまいます。誰でも呼べてよい読み取り専用のエンドポイントなら、意図を明示するために __return_true を指定します。

JavaScript 側は次のように呼び出します。

async function saveBookmark( postId ) {
	const settings = window.myRestFetch;
	const res = await fetch( settings.bookmarkUrl, {
		method: 'POST',
		credentials: 'same-origin',
		headers: {
			'Content-Type': 'application/json',
			'X-WP-Nonce': settings.nonce,
		},
		body: JSON.stringify( { post_id: postId } ),
	} );

	const data = await res.json().catch( () => null );
	if ( ! res.ok ) {
		// WordPress のエラーは { code, message, data: { status } } の形で返る
		throw new Error( data && data.message ? data.message : 'HTTP ' + res.status );
	}
	return data;
}

なぜ nonce が要るのか。 ログイン状態はクッキーで送られますが、REST API はクッキーだけでは本人の操作と見なしません。nonce がないリクエストは未ログインとして扱われるため、上の permission_callback で弾かれて 401 になります。他サイトからの不正なリクエスト(CSRF)を防ぐための仕組みです。

**Content-Type: application/json を付ける。** このヘッダーがあると、WordPress は本文を JSON として解析し、get_param() で値を取り出せるようになります。

つまずきやすいポイント

  • **403 と rest_cookie_invalid_nonce が返る**:nonce の期限切れか、ページキャッシュに古い nonce が残っている可能性があります。nonce の有効期間は既定で最長 1 日程度です。キャッシュ系プラグインの多くはログイン中のページをキャッシュしませんが、設定によっては対象になるので確認します
  • **401 と rest_forbidden が返る**:nonce を送っていないか、X-WP-Nonce のヘッダー名を間違えている可能性があります。開発者ツールのネットワークタブで、リクエストヘッダーに nonce が載っているか確かめます
  • **404 と rest_no_route が返る**:ルートの綴りか、register_rest_route() が rest_api_init で実行されているかを確認します。ルート一覧は REST API のルート URL を開くと JSON で確認できます
  • 未ログインだと API 全体が使えない:セキュリティ系プラグインの設定で、未ログインユーザーの REST API を制限していることがあります。項目名はプラグインやバージョンによって異なるため、設定画面かドキュメントで確かめてください
  • 別ドメインから呼びたい:クッキー認証は同じサイト内での利用が前提です。外部のアプリやサーバーから呼ぶ場合は、WordPress 5.6 で追加されたアプリケーションパスワードなどの別の認証方式を使い、パスワードをブラウザー側の JavaScript に埋め込まないようにします

ここで挙げた挙動やエラーコードは、WordPress 6.x 系で見られる一般的なものです(2026年10月現在)。

まとめ

  • エンドポイントの URL は rest_url() で生成し、wp_add_inline_script() と wp_json_encode() で JavaScript に渡す
  • GET では res.ok を確かめてから JSON を読み、_fields で取得項目を絞り、ページ送りは X-WP-TotalPages ヘッダーを使う
  • 受け取った文字列は textContent で表示し、innerHTML に直接入れない
  • 書き込みは permission_callback と引数の検証を備えた独自エンドポイントにし、wp_rest の nonce を X-WP-Nonce ヘッダーで送る
  • 403・401・404 はエラーコードから原因を切り分け、nonce の期限とページキャッシュも疑う

お気軽にご相談ください

お見積りへ お問い合わせへ
▲