メインコンテンツまでスキップ

APIs — Local / REST / GraphQL の 3 層

この章のゴール

  • Local / REST / GraphQL の 3 つの API レイヤの関係を 1 つの図で掴む
  • 「Server Components からは Local、外部クライアントからは REST/GraphQL」という分岐の理由を理解する
  • 自動生成型 payload-types.ts の使いどころが分かる
  • Where 条件・depth・locale・pagination の基本 syntax を 3 つの API で書ける

3 層の関係

ポイントは:

  • Local API は Node プロセス内で直接 PayloadCMS を呼ぶ。HTTP を介さない
  • REST / GraphQL は Local API のラッパー。HTTP リクエストを受けて内部で Local API を実行している
  • Hooks はどの経路でも同じように走ります。一方 Access Control の扱いは経路で異なり、REST / GraphQL では常に適用、Local API ではデフォルトでバイパスされます (詳細は後述の Local API と Access Control)

つまり 3 層は競合関係でなく、呼び出し元の所在によって使い分けるレイヤです。

使い分けの判断軸

呼び出し元第一選択理由
Next.js Server Components / Server ActionsLocal APIHTTP コスト 0、認証ヘッダ不要、型自動補完
Next.js Client Components / 別 framework SPAREST学習コスト低、fetch でそのまま
複雑なネスト取得を 1 回で済ませたいGraphQLover-fetch / under-fetch 回避、宣言的
モバイル / 第三者統合 (外部から)REST or GraphQL認証 + HTTP で標準的
CMS の中で他 Collection を触る (Hooks 内など)Local API (req.payload)サーバ内処理は HTTP を経由する意味がない

Local API

取得方法

Next.js 同居構成では getPayload({ config: configPromise }) で取得します (Local API Overview、公式 website template のパターン)。

import { getPayload } from 'payload'
import configPromise from '@payload-config'

const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
where: { _status: { equals: 'published' } },
limit: 10,
depth: 2,
})

主要メソッド

メソッド用途
payload.find({ collection, where, limit, page, sort, depth, locale })一覧取得
payload.findByID({ collection, id, depth, locale })単一取得
payload.count({ collection, where })件数取得
payload.create({ collection, data })作成
payload.update({ collection, id, data })更新
payload.delete({ collection, id })削除
payload.findGlobal({ slug, depth, locale })Global 取得
payload.updateGlobal({ slug, data })Global 更新
payload.login({ collection, data: { email, password } })プログラム的ログイン

Local API と Access Control (overrideAccess)

Local API には REST / GraphQL と決定的に違う点が 1 つあります。Access Control がデフォルトで適用されません。Local API の overrideAccess オプションはデフォルト true で、04 章 で定義したような access 関数をすべてスキップし、admin 相当の権限で実行します (Local API Overview)。

紛らわしいのは、user を渡しただけでは Access Control が効かないことです:

// ❌ user を渡しても Access Control はバイパスされたまま
const posts = await payload.find({
collection: 'posts',
user: currentUser,
// overrideAccess がデフォルト true のため admin 相当の全件が返る
})

// ✅ overrideAccess: false で初めて user の権限が適用される
const posts = await payload.find({
collection: 'posts',
user: currentUser,
overrideAccess: false,
})

使い分けの基準:

  • overrideAccess: true (デフォルト) のまま: cron / migration / seed など信頼済みのサーバ内部処理。全データに触れる前提の経路
  • overrideAccess: false + user: エンドユーザーの代理で実行する処理 (Server Action / カスタムエンドポイント / webhook)。そのユーザーの権限に絞る
  • overrideAccess: false のみ (user なし): 未認証の公開アクセス相当に絞る。公開サイトの Server Component で「published だけ返したい」ケース (06 章の実装例 がこのパターン)

04 章の access 関数は Admin UI / REST / GraphQL では常に働きますが、Local API では overrideAccess: false を指定した呼び出しでしか働きません。06 章の Server Component パターンで「access 関数が draft を隠してくれるはず」と考えると、実際には全件取得になります。

Hooks 内では req.payload

Hook の引数 req には payload インスタンスが入っているので、Hook 内から他 Collection を触る場合はそちらを使います。このとき書き込み操作にはオプションとして req を渡します:

hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-logs',
data: { type: 'post.changed', postId: doc.id },
req, // 同一 transaction に載せる
})
},
],
}

req を渡すと req.transactionID が引き継がれ、この audit-logs への書き込みは元のリクエスト全体が成功したときだけ commit されます (元の操作が失敗すれば一緒にロールバック)。注意点が 1 つ: await しない fire-and-forget の呼び出しには req を渡さないこと。transaction だけ引き継いで await しないと、ロールバックされた変更に対して成功レスポンスが返る不整合が起きます (Transactions (公式 doc))。

REST API

Base URL

公式 doc (REST API Overview) によれば、デフォルトの base URL は /apiroutes.api で変更可能です。Collection は slug を URL に使います。

メソッドパス用途
GET/api/{collection}一覧取得
GET/api/{collection}/{id}単一取得
GET/api/{collection}/count件数取得
POST/api/{collection}作成
PATCH/api/{collection}/{id}更新
DELETE/api/{collection}/{id}削除
GET/api/globals/{global}Global 取得
POST/api/globals/{global}Global 更新

クエリパラメータ

パラメータ役割
whereフィルタ条件 (nested syntax)
depthリレーション展開深さ
limit / pageページネーション
sortソートキー (-createdAt で降順)
locale / fallback-localeロケール指定
select / populateフィールド絞り込み
draftdraft 含めるか (Versions + Drafts 時)

Where 条件の書き方

REST では nested クエリパラメータで書きます。

# 公開済みかつタイトルに "Next" を含む投稿、新しい順 20 件
GET /api/posts?where[_status][equals]=published&where[title][contains]=Next&sort=-createdAt&limit=20
演算子意味
equals / not_equals等価 / 非等価
in / not_in配列内 / 配列外
greater_than / greater_than_equal / less_than / less_than_equal大小比較
contains / like部分一致
existsフィールド有無
near地理座標近傍 (point 型)

Auth エンドポイント

auth: true の Collection は追加で以下が現れます (前章でも触れた表):

POST /api/users/login
POST /api/users/logout
GET /api/users/me
POST /api/users/refresh-token
POST /api/users/forgot-password

fetch 例

// クライアント側からの取得
const res = await fetch('/api/posts?where[_status][equals]=published&limit=10')
const json = await res.json()
console.log(json.docs) // ドキュメント配列

GraphQL API

Endpoint

公式 doc (GraphQL Overview) によれば:

  • GraphQL: /api/graphql
  • Playground: /api/graphql-playground (dev デフォルト有効、本番デフォルト無効)
  • routes 設定で変更可能

命名規則

対象クエリ / ミューテーション
単一取得Post(id: "...") (singular)
一覧Posts(where: {...}, limit: 10) (plural)
件数countPosts(where: {...})
作成createPost(data: {...})
更新updatePost(id: "...", data: {...})
削除deletePost(id: "...")
ログインloginUser(email: "...", password: "...")

Collection slug から型名が自動生成されます (特殊文字・スペースは除去)。Singular / plural ラベルを labels で明示できます。

クエリ例

query PublishedPosts {
Posts(
where: { _status: { equals: published } }
sort: "-createdAt"
limit: 10
) {
docs {
id
title
slug
author {
id
name
}
}
totalDocs
}
}

無効化

// payload.config.ts
export default buildConfig({
// ...
graphQL: {
disable: true,
},
})

GraphQL を使わないプロジェクトは disable: true で完全に外せます。Playground だけ本番で隠したい場合は disablePlaygroundInProduction: true も。

自動生成型 (payload-types.ts)

生成コマンド

公式 doc (Generating Types) によれば:

# config が src/ にある場合
PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types

package.json の script にしておくのが標準です:

{
"scripts": {
"generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types"
}
}

dev/watch 時の自動再生成は明記されていないため、設定変更 → yarn generate:types を手動 (or npm script) がデフォルトのフローです。

出力される型

  • 各 Collection ごとに interface (Post, User, Media など)
  • Global ごとに interface (Header, SiteSettings など)
  • interfaceName を Block / Array / Group に指定すると 再利用可能な top-level interface も生成

フロントでの使い方

import type { Post, User } from '@/payload-types'

const posts: Post[] = await payload.find({ collection: 'posts' }).then((r) => r.docs)

Local API の戻り値も REST レスポンスも、この型で受けられるため フロント全体が型安全になります。Next.js 同居構成だと payload-types.ts をプロジェクト内で直接 import できるので、別 package コピーの手間もありません。

3 つの API で同じことを書く比較

「公開済みの投稿を新しい順に 10 件」を 3 つの API で書くと:

// Local API (Server Component 内など)
const { docs } = await payload.find({
collection: 'posts',
where: { _status: { equals: 'published' } },
sort: '-createdAt',
limit: 10,
depth: 2,
})
# REST
GET /api/posts?where[_status][equals]=published&sort=-createdAt&limit=10&depth=2
# GraphQL
{
Posts(
where: { _status: { equals: published } }
sort: "-createdAt"
limit: 10
) {
docs { id title }
}
}

REST / GraphQL は Access Control を通過した、認可されたデータだけを返します。Local API は overrideAccess: false を明示したときだけ同じ挙動になります (前述)。

実装例 — カスタム REST エンドポイント (endpoints)

自動生成の REST エンドポイントだけで足りない場合、Collection または Config の endpoints 配列に独自の handler を追加できます。公式 doc が示す典型例: 認証チェック → リクエストボディを読む → Local API でドキュメント更新 → JSON 返却、というフローです (REST API Overview)。

// collections/Tracking.ts
import type { CollectionConfig } from 'payload'

export const Tracking: CollectionConfig = {
slug: 'tracking',
endpoints: [
{
path: '/:id/tracking',
method: 'post',
handler: async (req) => {
if (!req.user) {
return Response.json({ error: 'forbidden' }, { status: 403 })
}

const data = await req.json()

await req.payload.update({
collection: 'tracking',
id: req.routeParams.id as string,
data,
})

return Response.json({ message: 'successfully updated tracking info' })
},
},
],
fields: [/* ... */],
}

ポイント:

  • path:idreq.routeParams.id で取れる
  • methodlowercase ('get' / 'post' 等)
  • req.usernull のとき Response.json({ error }, { status: 403 }) で拒否
  • 戻り値は Response.json(...) (Next.js Route Handler と同じ syntax)
  • req.payload でその場から Local API (Local API 節) を呼べる

このエンドポイントは POST /api/tracking/:id/tracking として公開されます (Collection の slug が prefix に付く)。

出典: REST API Overview (公式 doc)

この章のまとめ

  • 3 層は競合でなく 呼び出し元の所在で使い分ける
  • Server Components / Server Actions / Hooks 内 → Local API (HTTP コスト 0、req.payload も同じインスタンス)
  • Local API はデフォルトで Access Control をバイパス (overrideAccess: true)。ユーザー権限で絞るなら overrideAccess: false + user、Hooks 内の書き込みは req を渡して transaction を共有
  • 外部クライアント → REST (シンプル) または GraphQL (宣言的、複雑ネスト)
  • payload generate:typespayload-types.ts が出る → フロント全体を型安全に
  • Where 条件は 3 つとも 同じ演算子 (equals / contains / greater_than …) でシンタックスだけが違う
  • 標準にない API は endpoints 配列でカスタム handler を生やせる (req.user で認証チェック / Response.json で返却)

次に読む

次章 06 Next.js Integration — 同居構成の深掘り は本シリーズの目玉です。app/(payload)/ の route group から getPayload() の使い方、Lexical 描画、Live Preview、Draft Preview まで、Next.js 同居構成を 12 節で集中的に扱います。

参考