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 Actions | Local API | HTTP コスト 0、認証ヘッダ不要、型自動補完 |
| Next.js Client Components / 別 framework SPA | REST | 学習コスト低、fetch でそのまま |
| 複雑なネスト取得を 1 回で済ませたい | GraphQL | over-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 は /api、routes.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 | フィールド絞り込み |
draft | draft 含めるか (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の:idはreq.routeParams.idで取れるmethodは lowercase ('get'/'post'等)req.userがnullのとき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 に付く)。
この章のまとめ
- 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:typesで payload-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 節で集中的に扱います。