フルスタックNext.jsの決定版！？ "Payload"で管理画面&WebAPIを自動生成！

# 雑まとめ

Payloadとは 

**Next.jsに
WebAPI&管理画面のコード自動生成機能と
それを支える高機能なORMを加えた最強フルスタックWebフレームワーク**　

触ってみた所感としては……
- ふつうのNext.jsアプリなので好きなインフラにセルフホスティングできるの良い
- コードファースト&コード生成アプローチで黒魔術感が少なく管理しやすそう
- 管理画面やWebAPIのカスタマイズ方法も多彩でたいていの要件満たせそう
- 一括アップロード、ライブプレビュー、ジョブキュー などなど…… 機能盛りだくさん

興味が湧いたら さあ `npm create payload-app`

# はじめに

[TRIAL＆RetailAI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/retail-ai) 12日目の記事です。
昨日は @mossan_hoshi さんの『[わく枠アウトプット術](https://qiita.com/mossan_hoshi/items/1f1279db7d8d19a9db3e)』という記事でした。

私も@mossan_hoshiさんを見習ってアウトプットを増やしていこうと思います……！

--- 

この記事では
2024年11月の`Best of JS`ランキングで2位を獲得した、今注目のフルスタックNext.jsフレームワーク **Payload** を紹介します。

https://bestofjs.org/rankings/monthly/2024/11

https://payloadcms.com/


---



# Payloadの概要とコンセプト


まず、以下のドキュメントを読みながら、自分なりに解説します
https://payloadcms.com/docs/getting-started/what-is-payload
https://payloadcms.com/docs/getting-started/concepts

## `Payload`とは

`Payload`とは、**Next.jsをベースとした、フルスタックTypeScriptアプリフレームワーク**です。

Payloadは次のような機能をNext.jsに追加します。

- RSCを活用した完全にカスタマイズできる管理画面を自動生成
- データ操作のためのREST、GraphQL、ローカルAPIを自動生成
- DBスキーマの自動管理
- 認証方法、アクセス制御を高度にカスタマイズ可能
- ファイルストレージおよびトリミング/フォーカスポイント選択機能を持った画像管理ツールが付属
- ライブプレビュー 
- その他にもたくさん(ジョブキュー、Meta製リッチテキストエディタLexical対応、日本語化などなど）

もともと、`Payload`は`Strapi`や`Ghost`のようなヘッドレスコンテンツ管理システム（CMS)のフレームワークとして始まりました。
2024年11月のPayload v3リリースで、Next.jsベースになり、さまざまなユースケースにフィットするフレームワークへ成長しました。

- ヘッドレスCMS
- ORM、認証認可、WebAPI、スキーママイグレーション、Webhook、管理画面などが必要なシステム開発全般
- ECサイト
- Shopifyのようなヘッドレスコマースシステム
- デジタル資産管理システム

逆に、`Payload`に向いていないユースケースとしては

> If you already have a full database and just need to visualize the data somehow
> すでに完全なデータベースがあり、データを何らかの方法で視覚化する必要がある場合

とのこと。この場合は、Payloadを使ってもWebAPI、管理画面どちらも自動生成できないため、たしかにメリットが薄いです。

## Payloadのコンセプト

理解を助けるためにPayloadのコンセプトを紹介します。

### 型付けされた設定ファイル

Payloadの設定はTypeScriptによって型付けされた`payload.config.ts`ファイルで行います。
IDEやエディタの入力支援を受けられるので、書きやすく、管理もしやすいです。

```ts:payload.config.tsのシンプルな例
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET,
  db: mongooseAdapter({
    url: process.env.DATABASE_URI,
  }),
  collections: [
    {
      slug: 'pages',
      fields: [
        {
          name: 'title',
          type: 'text'
        }
      ]
    }
  ],
})
```

### 特定のデータベースに依存しない

データベースアダプターを変更することで、複数のDBから好きなものを選択できます。
現在データベースアダプターが提供されているDBは MongoDB,PostgreSQL,SQLite,Vercel Postgres です。

### コレクション

スキーマを共有するドキュメントの集まりをコレクションと呼びます。（users,posts,commentsなど）。
コレクションのスキーマを定義すると、そのスキーマをもとにDBへ保存されます(RDBの場合は、テーブルが作成されます）。
また、管理画面やWebAPIもコレクションのスキーマをもとに自動生成されます。

### グローバル

グローバルは、ドキュメントを1つしか持てないコレクションです。
1つしか持てないので、アプリ全体から利用するシングルトンのように使うことができます。
Webサイトのヘッダーやフッターのナビゲーションを管理したい時などに使います。

### フィールド

ドキュメントのスキーマに定義できる情報です。
フィールドをもとに、DBテーブル、管理画面、WebAPIが作られます。

様々なタイプのフィールドがあり、ネストしたフィールドや、他コレクション/グローバルへのリレーションを持たせることもできます。

<details><summary>フィールドタイプの一覧</summary>


| タイプ         | 説明                                                                 |
|--------------------------|----------------------------------------------------------------------|
| Array                   | 繰り返し可能なコンテンツ用で、ネストされたフィールドをサポートします |
| Blocks                  | ブロックベースのコンテンツ用で、ネストされたフィールドをサポートします |
| Checkbox                | 真偽値（true/false）を保存します                                     |
| Code                    | コードエディターインターフェースを提供し、文字列を保存します         |
| Date                    | 日付ピッカーを表示し、タイムスタンプを保存します                     |
| Email                   | 正しい形式のメールアドレスを保証します                              |
| Group                   | フィールドをキー付きオブジェクトとしてネストします                   |
| JSON                    | JSONエディターインターフェースを提供し、JSONオブジェクトを保存します |
| Number                  | 数値を保存します                                                     |
| Point                   | 位置データ用で、地理的な座標を保存します                             |
| Radio                   | ラジオボタングループを表示し、1つの値のみ選択可能です               |
| Relationship            | 他のコレクションとの関係を割り当てます                               |
| Rich Text               | 拡張可能なリッチテキストエディター(デフォルトはLexical)を提供します |
| Select                  | ドロップダウンや選択リスト形式の値セレクターを表示します             |
| Tabs (Named)            | Groupに似ていますが、ネストされたフィールドをタブ形式で表示します     |
| Text                    | シンプルなテキスト入力で文字列を保存します                          |
| Textarea                | Textに似ていますが、複数行の入力が可能です                          |
| Upload                  | ローカルファイルや画像のアップロードを可能にします                 |


以下のフィールドは、DBに保存されませんが、管理画面の見た目に影響します。

| タイプ         | 説明                                                              |
|--------------------------|-------------------------------------------------------------------|
| Collapsible             | 折りたたみ可能なコンポーネント内にフィールドをネストします         |
| Join                    | フィールド間で双方向データバインディングを実現します               |
| Row                     | フィールドを横方向に整列させます                                   |
| Tabs (Unnamed)          | タブ形式のレイアウト内にフィールドをネストします                   |
| UI                      | カスタムUIコンポーネント用の空白のフィールドです                  |

</details>


### フック

アプリ全体、コレクションごと、グローバルごと、フィールドごと それぞれの単位でフックを設定し、任意の処理を実行できます。
これによって、以下のようなさまざまな機能を実現できます。

- 読み込みや更新が行われる前にデータを修正する
- 機密データを暗号化/復号する
- Salesforce のようなサードパーティの CRM と統合する
- アップロードされたファイルのコピーを Amazon S3 などにも送信する
- Stripe のような決済プロバイダーを通じて注文を処理する
- 問い合わせフォームが送信された際にメールを送信する
- データの所有権や変更履歴を追跡する


参考までに、コレクションとフィールドのフックを紹介します。

```ts
export const CollectionWithHooks: CollectionConfig = {
  // ...
  hooks: {
    beforeOperation: [(args) => {...}],
    beforeValidate: [(args) => {...}],
    beforeDelete: [(args) => {...}],
    beforeChange: [(args) => {...}],
    beforeRead: [(args) => {...}],
    afterChange: [(args) => {...}],
    afterRead: [(args) => {...}],
    afterDelete: [(args) => {...}],
    afterOperation: [(args) => {...}],
    afterError: [(args) => {....}],

    // Auth-enabled Hooks
    beforeLogin: [(args) => {...}],
    afterLogin: [(args) => {...}],
    afterLogout: [(args) => {...}],
    afterRefresh: [(args) => {...}],
    afterMe: [(args) => {...}],
    afterForgotPassword: [(args) => {...}],
    refresh: [(args) => {...}],
    me: [(args) => {...}],
  },
}

const FieldWithHooks: Field = {
  name: 'name',
  type: 'text',
  hooks: {
    beforeValidate: [(args) => {...}],
    beforeChange: [(args) => {...}],
    beforeDuplicate: [(args) => {...}],
    afterChange: [(args) => {...}],
    afterRead: [(args) => {...}],
  }
}
```

### 認証

Payloadは認証機能を提供しており、管理画面とWebAPIの両方で利用できます。

認証の方法として、以下の3つをサポートしており、どれか1つ以上を組み合わせることも可能です。

- HTTP-Only Cookies
- JSON Web Tokens (JWT)
- APIキー

これらはPassportパッケージによって実現しています。

また、Passportを使わない、独自の認証機能を組み込むこともできます。

### アクセス制御（認可）

ドキュメントのスキーマにアクセス制御を設定し、ドキュメントに対してできる操作、できない操作をきめ細かく制御できます。

活用例↓

- 投稿への読み取りアクセスを誰にでも許可する
- Statusフィールドが「公開」となっている投稿のみを公開アクセス可能にする
- Roleフィールドが「管理者」となっているユーザーのみに投稿の削除権限を与える
- お問い合わせフォームの送信は誰でも可能にするが、閲覧、更新、削除は管理画面にログインしたユーザーのみに限定する
- ユーザーが自分の注文のみ閲覧可能とし、他人の注文は閲覧できないように制限する
- 特定の組織に属するユーザーへ、その組織のリソースのみへのアクセスを許可する


また、アクセス制限ロジックは `Access`型を満たす関数として柔軟に定義できます。

```ts:アクセス制御ロジックの例
import type { Access } from 'payload'

export const canUpdateUser: Access = ({ req: { user }, id }) => {
  // Adminロールを持つユーザーなら更新を許可
  if (user.roles && user.roles.some((role) => role === 'admin')) {
    return true
  }

  // その他のユーザーは、自分の情報のみ更新可能
  return user.id === id
}
```

```ts:ロジックをコレクションに適用
import type { CollectionConfig } from 'payload'

export const CollectionWithUpdateAccess: CollectionConfig = {
  // ...
  access: {
    update: canUpdateUser,
  },
}
```

### 管理画面の自動生成

Next.jsのAppRoute機能を使った管理画面を自動生成します。
デフォルトで使いやすく、多機能で、カスタマイズ性も高いです。
React Server Componentsにも対応しています。


個人的には、管理画面を自動生成できつつ、カスタマイズも高いのは嬉しいポイントです。
カスタムフィールドや、カスタムコンポーネントを作成して配置できるので、ほとんどの要件を満たせるのではないでしょうか。


### データ取得

PayloadはCRUD、ログイン、ログアウトなどの機能を3つの方法で公開します


- ローカルAPI：サーバーから直接DBにアクセス
- WebAPI
    - REST API
    - GraphQL

"Pages"コレクションを取得する例を見ましょう。

#### ローカルAPI

ローカルAPIを通して、Node.jsサーバーから直接DBを操作できます。

```tsx
import React from 'react'
import config from '@payload-config'
import { getPayload } from 'payload'

const MyServerComponent: React.FC = () => {
  const payload = await getPayload({ config })

  // `findResult` は `PaginatedDocs<Page>` として型付けされ、様々な情報を利用できます。
  const findResult = await payload.find({ collection: 'pages' })

  return (
    <ul>
      {findResult.docs.map((page) => {
        // `page` は Pages コレクションに対応する型情報を持つ
      })}
    </ul>
  )
}

```

#### Web API

REST APIは`/api`パス、GraphQLルートは`/api/graphql`パスで公開されます

```ts
fetch('https://localhost:3000/api/pages') 
  .then((res) => res.json())
  .then((data) => console.log(data))
```

---

### パッケージ構造

Payloadは整理された複数のパッケージから構成されています。
たとえば、`payload`パッケージはNext.jsに依存しないので、好きなフレームワーク(React Router,SveltKitなど)やNode.js環境で利用できます。

| パッケージ | 説明 |
| --- | --- |
| payload | Payloadのコアロジック（find / create / update / deleteなどのオペレーション、アクセスコントロール、Hooks、Validation）と型定義。 |
| @payloadcms/next | 管理画面およびREST API、GraphQL APIなど、PayloadのHTTPレイヤー全般を担う |
| @payloadcms/graphql | GraphQL機能を抽象化したパッケージ。GraphQL機能を使う場合は別途`graphql`が必要。 |
| @payloadcms/ui | Payloadの管理画面で利用されるUIコンポーネント群。RSCとしても利用可能。 |
| @payloadcms/db-postgres, @payloadcms/db-vercel-postgres, @payloadcms/db-mongodb | データベースアダプタ。1つのプロジェクトで1種類のみ選択可能 |
| @payloadcms/richtext-lexical, @payloadcms/richtext-slate | リッチテキスト機能を提供。Payload v3ではデフォルトで`lexical` |

このような構造なので、仮に、将来Next.jsが廃れてしまっても、`@payloadcms/{新フレームワーク}`のようなパッケージを作ることで、現実的な工数で移行できる……かも……。

--- 


# プロジェクト構造を見てみる

create-payload-app コマンドが提供されているので、すぐにプロジェクトを作成できます。
テンプレートは最小限の`blank`と機能山盛りの`website`があります。

## websiteテンプレート

`website`テンプレートには以下の機能が最初から盛り込まれており、実装の参考になります。

- 認証・アクセス制御
- レイアウトビルダー
- 下書きとライブプレビュー機能
- SEO、リダイレクト対応
- TypeScriptとNext.jsを使用したフロントエンド

コード全体はこちら
https://github.com/payloadcms/payload/tree/main/templates/website

量が多いので、今回は詳しく解説しません:bow:


## blankテンプレート

`blank`は必要最小限の `Users`, `Media`コレクションのみを含んだテンプレートです。
実際の開発では、`blank`から機能を足していく方が楽かもしれません（好みの問題？）


```terminal
➜ npm create payload-app@latest

> npx
> create-payload-app

┌   create-payload-app 
│
◇   ────────────────────────────────────────────╮
│                                               │
│  Welcome to Payload. Let's create a project!  │
│                                               │
├───────────────────────────────────────────────╯
│
◆  Project name?
│  sample_cms
◆  Choose project template
│  ● blank
│  ○ website
◆  Select a database
│  ○ MongoDB
│  ○ PostgreSQL
│  ● SQLite
│  ○ Vercel Postgres
◆  Enter SQLite connection string
│  file:./sample-cms.db
│
│
◇  Using npm.
│  
◇  Successfully installed Payload and dependencies
│
◇  [DEBUG] .env.example file successfully updated
│
◇  [DEBUG] .env file successfully created or updated
│
◇  Payload project successfully created!
│
◇   Next Steps  ──────────────────────────────────────╮
│                                                     │
│                                                     │
│  Launch Application:                                │
│                                                     │
│    - cd ./sample-cms                                │
│    - npm run dev or follow directions in README.md  │
│                                                     │
│  Documentation:                                     │
│                                                     │
│    - Getting Started                                │
│    - Configuration                                  │
│                                                     │
│                                                     │
│                                                     │
├─────────────────────────────────────────────────────╯
│
└   Have feedback?  Visit us on GitHub.

```

プロジェクトが作成されたので、移動してプロジェクト構造を見てみましょう

```
➜ cd sample-cms

➜ tree src 
src
├── app
│   ├── (payload)
│   │   ├── admin
│   │   │   ├── [[...segments]]
│   │   │   │   ├── not-found.tsx
│   │   │   │   └── page.tsx
│   │   │   └── importMap.js
│   │   ├── api
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   ├── graphql
│   │   │   │   └── route.ts
│   │   │   └── graphql-playground
│   │   │       └── route.ts
│   │   ├── custom.scss
│   │   └── layout.tsx
│   └── my-route
│       └── route.ts
├── collections
│   ├── Media.ts
│   └── Users.ts
├── payload-types.ts
└── payload.config.ts

```

appフォルダを見てみます。

```
src
├── app
│   ├── (payload)
│   │   ├── admin
│   │   │   ├── [[...segments]]
│   │   │   │   ├── not-found.tsx
│   │   │   │   └── page.tsx
│   │   │   └── importMap.js
│   │   ├── api
│   │   │   ├── [...slug]
│   │   │   │   └── route.ts
│   │   │   ├── graphql
│   │   │   │   └── route.ts
│   │   │   └── graphql-playground
│   │   │       └── route.ts
│   │   ├── custom.scss
│   │   └── layout.tsx
│   └── my-route
│       └── route.ts
```

appフォルダがあるので、Next.jsのApp Router機能を使っていることがわかります。
payloadフレームワークが自動生成するルートをApp Routerの`Route Groups`という機能で、(payload)以下に隔離しています。
開発者がルートを手作業で追加したい場合は、my-routeの例のように(payload)以外に追加します。

(payload)/admin 以下は、管理画面の自動生成コードです。
(payload)/api 以下は、WebAPIとして公開される自動生成コードです。

```
├── collections
│   ├── Media.ts
│   └── Users.ts
├── payload-types.ts
└── payload.config.ts
```

`payload.config.ts`ではアプリ全体の設定を行い、
`collections`以下では、管理したいコレクションを設定します。

Payloadは、この２つの設定から`(payload)`以下のルートを自動生成します。便利ですね！


# 興味が湧いたら

ぜひ実際にPayloadを触ってみてください。日本語情報が増えると私が喜びます。

アールエフェクトさんから、ハンズオン記事が公開されています。開発の雰囲気を知ることができます。(PayloadがNext.jsベースになる前の記事なので、適宜読み替える必要あり)


https://reffect.co.jp/javascript/payload-cms

さらに詳しく知りたいなら、わかりやすい[公式ドキュメント](https://payloadcms.com/docs)があります。
[websiteテンプレート](https://github.com/payloadcms/payload/tree/main/templates/website)のコードを合わせて参照すると良いでしょう。

[公式Youtubeチャンネル](https://www.youtube.com/@payloadcms)も充実しています。Payload社としても解説動画には力を入れていくとのこと。


# さいごに

ここまで読んでいただきありがとうございました。
まとまりのない記事になってしまいましたが、１人でもPayloadに興味を持つ人が増えれば幸いです。



明日のアドベントカレンダーは、
@atsukishさんの「[PydanticのLLMエージェントフレームワーク「PydanticAI」を使ってみた](https://qiita.com/atsukish/items/a1613c77cecd41980467)」です。
Pydanticにそんな機能が！？ これは期待大！

➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖
RetailAIとTRIALではエンジニアを募集しています。

[Retail AI採用サイト](https://www.recruit-retail-ai.jp/)
[株式会社トライアルカンパニー エンジニア の求人一覧](https://hrmos.co/pages/trialgp/jobs?category=2020003768196907008)
➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖