2023年10月にバージョン2がリリースされたHeadless CMSのPayloadとはどんなものか実際にローカルのMacOS環境にインストールを行い動かしてみました。

Payloadとは

PayloadはNode.jsのExpress上で動作するTypeScript-based headless CMS/アプリケーションフレームワークです。Node.jsとReactを利用して構築されています。提供されるAdminパネルを通してデータの管理を行うことができます。登録したデータは複雑な設定を行うことなくREST APIもしくはGraphQLを利用して外部からアクセスすることができます。管理するデータに対して細かなアクセスコントロールも行える上、定義したCollection(=モデル/テーブル)に認証機能を設定することもできます。

PayloadはオープンソースなのでどこにでもデプロイすることができますがPayload Cloudというクラウドサービスを利用して運用することができます。クラウドサービスを利用する場合は料金がかかります。

Payload内のすべてコードはTypeScriptで記述されています。これまでサポートしていたデータベースはMongoDBのみでしたがバージョン2にアップが行われORMのDrizzleを利用してPostgresが利用できるようになったり(現在はbeta)、ビルドにはWebpackだけではなくViteをサポートしたり、Rich Text EditorにはSlateだけではなくLexicalをサポートしたりと積極的に開発が進められている注目度の高いHeadless CMSの一つです。

MongoDBにはMongooseを利用しています。今後はDrizzleを利用してSQLiteやMySQLもサポートする予定です。

Payloadプロジェクトの作成

Payloadプロジェクトを作成するために”npx create-payload-app@latest”コマンドを実行します。実行するとプロジェクト名やテンプレート、接続するデータベースなどを選択、入力する必要があります。プロジェクト名は任意の名前をつけることができるのでここでは”payload_app”としています。テンプレートはBlank, データベースにはMongoDBを選択します。betaですがPostrgesも選択可能です。MongoDBを選択すると接続文字列を設定する必要がありますがデフォルトのままで進めます。


 % npx create-payload-app@latest
Need to install the following packages:
create-payload-app@1.0.0
Ok to proceed? (y) y

  Welcome to Payload. Let's create a project! 

✔ Project name? … payload_app
✔ Choose project template › blank
✔ Select a database › MongoDB
✔ Enter MongoDB connection string … mongodb://127.0.0.1/payload-app

  Creating project in /Users/mac/Desktop/payload-app

✔ Dependencies installed
✔ .env file created
✔ Payload project successfully created

  ★ Launch Application:

    - cd ./payload-app
    - yarn dev or follow directions in README.md: file:///Users/mac/Desktop/payload-app/README.md

  ★ Documentation:

    - Getting Started: https://payloadcms.com/docs/getting-started/what-is-payload
    - Configuration: https://payloadcms.com/docs/configuration/overview

開発サーバの起動

コマンドが完了するとプロジェクト名で入力したディレクトリが作成されるのでそのディレクトリに移動してyarn devコマンドを実行します。ローカルでMongoDBが動作してない場合には接続ができないためエラーとなり開発サーバは起動できません。


 % yarn dev
yarn run v1.22.18
warning ../package.json: No license field
$ cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node src/server.ts -- -I`
[05:08:04] ERROR (payload): Error: cannot connect to MongoDB. Details: connect ECONNREFUSED 127.0.0.1:27017
[nodemon] app crashed - waiting for file changes before starting...

本文書ではMongoDBはFreeプランのあるクラウドサービスのAtlasを利用します。アカウントの作成からデータベースの作成/接続文字列の取得までの一連の流れはすでに別の記事で公開しているので参考にしてください。

Atlasを利用して取得した接続文字列を.envファイルに設定してデフォルトの値を上書きします。下記のままでは接続できないので各自が取得した値を設定してください。


DATABASE_URI='mongodb+srv://johodoe10:L2eBNHqgr99cvOWJ@cluster0.ya8yt7x.mongodb.net/?retryWrites=true&w=majority'
PAYLOAD_SECRET=667330ea6fe3fb94352dffca

.envファイルを更新後に再度yarn devコマンドを実行します。MongoDBへの接続とWebpackのコンパイルに成功していることが起動メッセージから確認できます。


 % yarn dev
yarn run v1.22.18
warning ../package.json: No license field
$ cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts
[nodemon] starting `ts-node src/server.ts -- -I`
[04:56:23] INFO (payload): Connected to MongoDB server successfully!
[04:56:23] INFO (payload): Starting Payload...
[04:56:24] INFO (payload): Payload Admin URL: /admin
webpack built 0e5163ae0b0963f73401 in 11664ms
webpack compiled successfully

ユーザの作成

ブラウザからhttp://localhost:3000/adminにアクセスするとWelcomeの文字列とユーザの作成の入力フォームが表示されます。最初にユーザの作成を行う必要があります。入力フォームにメールアドレスとパスワードを入力して”Create”ボタンをクリックします。

ユーザ作成画面
ユーザ作成画面

ユーザの作成が完了するとCollections画面が表示されます。

Collectionsの表示
Collectionsの表示

Usersをクリックします。ユーザ一覧画面が表示され作成したユーザが表示されます。

ユーザ一覧画面の表示
ユーザ一覧画面の表示

さらにメールアドレスをクリックするとAPIエンドポイントのURLを確認することができます。右側のAPIをクリックしてください。

APIのURLとその戻り値
APIのURLとその戻り値

MongoDB Atlas上のデータベースにも作成したユーザが登録されていることが確認できます。MongoDBはNO SQLデータベースなのでテーブルのことをCollectionと呼びますがusersの他にpayloads-migrationsとpayload-preferencesを確認することができます。どちらのCollectionにもユーザを作成しただけではデータはありません。

MongoDB Atlas上のCollectionsの確認
MongoDB Atlas上のCollectionsの確認

起動の流れ

yarn devコマンドを実行するとどのようなファイルを読み込みながら起動しているのか確認するためにpackage.jsonファイルのscriptsを確認します。


{
  "name": "payload_app",
  "description": "A blank template to get started with Payload",
  "version": "1.0.0",
  "main": "dist/server.js",
  "license": "MIT",
  "scripts": {
    "dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
    "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
    "build:server": "tsc",
    "build": "yarn copyfiles && yarn build:payload && yarn build:server",
    "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
    "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
    "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
    "generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
    "payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload"
  },
  "dependencies": {
    "@payloadcms/bundler-webpack": "^1.0.0",
    "@payloadcms/db-mongodb": "^1.0.0",
    "@payloadcms/plugin-cloud": "^2.0.0",
    "@payloadcms/richtext-slate": "^1.0.0",
    "cross-env": "^7.0.3",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "payload": "^2.0.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.9",
    "copyfiles": "^2.4.1",
    "nodemon": "^2.0.6",
    "ts-node": "^9.1.1",
    "typescript": "^4.8.4"
  },
  "resolutions": {
    "jackspeak": "2.1.1"
  }
}

“dev”ではPAYLOAD_CONFIG_PATHでsrc/payload.config.tsファイルが指定されているので中身を確認します。


import path from 'path';

import { payloadCloud } from '@payloadcms/plugin-cloud';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
import { webpackBundler } from '@payloadcms/bundler-webpack';
import { slateEditor } from '@payloadcms/richtext-slate';
import { buildConfig } from 'payload/config';

import Users from './collections/Users';

export default buildConfig({
  admin: {
    user: Users.slug,
    bundler: webpackBundler(),
  },
  editor: slateEditor({}),
  collections: [Users],
  typescript: {
    outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
  graphQL: {
    schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
  },
  plugins: [payloadCloud()],
  db: mongooseAdapter({
    url: process.env.DATABASE_URI,
  }),
});

データベースやeditorの選択がこの画面から行えることがわかります。scriptsを見るとnodemonが利用されており、プロジェクトディレクトリ直下にnodemon.jsonファイルがあるのでnodemon.jsonを元にnodemonが実行されていることがわかります。

nodemon.jsonのexecでsrcディレクトリのserver.tsファイルが指定されているのでyarn devコマンドでsrc/server.tsファイルが実行されることがわかります。


{
  "$schema": "https://json.schemastore.org/nodemon.json",
  "ext": "ts",
  "exec": "ts-node src/server.ts -- -I",
  "stdin": false
}

server.tsファイルの中身を下記の通りです。Expressサーバを利用してstart関数の中でPaylodが起動していることがわかります。


import express from 'express'
import payload from 'payload'

require('dotenv').config()
const app = express()

// Redirect root to Admin panel
app.get('/', (_, res) => {
  res.redirect('/admin')
})

const start = async () => {
  // Initialize Payload
  await payload.init({
    secret: process.env.PAYLOAD_SECRET,
    express: app,
    onInit: async () => {
      payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
    },
  })

  // Add your own express routes here

  app.listen(3000)
}

start()

“/”にアクセスすると”/admin”にリダイレクトされるものこのファイルから確認できます。さらにポート番号が3000であることもこのファイルからわかります。

Payloadの動作確認

Collectionの追加、追加したCollectionへのGETリクエスト、アクセスコントロール、APIによりログイン、ログアウトの確認、ファイルのアップロードなど基本的な動作確認を行います。

Collectionの追加

デフォルトではUser Collectionのみが作成されている状態なので新たに別のCollectionを追加してみましょう。

collectionsディレクトリにCustomers.tsファイルを作成します。


import { CollectionConfig } from 'payload/types';

const Customers: CollectionConfig = {
  slug: 'customers',
  auth: true,
  fields: [
    { name: 'firstName', type: 'text', required: true },
    { name: 'lastName', type: 'text', required: true },
  ],
};

export default Customers;

ファイルの中ではCollectionを構成するフィールドの設定を行います。slugはユニークな文字列でAPIのエンドポイントにも関連する値です。customersと設定すると/api/customersでアクセスすることができます。authの値をtrueに設定していますがこのCollectionに認証機能を持たせています認証機能がどのようなものか後ほど確認します。fieldsで指定しているfirstName, lastNameはCollectionを構成するデータ構造でcustomers CollectionにはfirstNameとlastNameに値を保存することになります。

Customers.tsファイルを作成後にpayload.config.tsファイルに作成したCustomers.tsファイルをimportしてcollectionsの配列にCustomersを追加します。


import path from 'path';

import { payloadCloud } from '@payloadcms/plugin-cloud';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
import { webpackBundler } from '@payloadcms/bundler-webpack';
import { slateEditor } from '@payloadcms/richtext-slate';
import { buildConfig } from 'payload/config';

import Users from './collections/Users';
import Customers from './collections/Customers';

export default buildConfig({
  admin: {
    user: Users.slug,
    bundler: webpackBundler(),
  },
  editor: slateEditor({}),
  collections: [Users, Customers],
  typescript: {
    outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
  graphQL: {
    schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
  },
  plugins: [payloadCloud()],
  db: mongooseAdapter({
    url: process.env.DATABASE_URI,
  }),
});

ファイルを保存すると自動でCustomersのCollectionが追加され/adminにアクセスすると追加されたCustomersを確認することができます。この時点でMongoDBのAtlas上にもCollectionが追加されています。

Customers Collectionsの追加
Customers Collectionsの追加

クリックするとCustomers一覧が表示されます。

Customers一覧画面
Customers一覧画面

“Create new Customer”ボタンからCustomerを追加することができます。入力フォームも自動で作成されauthをtrueに設定しているのでEmailとパスワードの入力フォームも一緒に表示されます。フォームに入力して”Save”ボタンをクリックします。

Customer作成フォーム
Customer作成フォーム

“Save”ボタンをクリックすると入力した内容でユーザが作成されます。

登録されたユーザ情報
登録されたユーザ情報

アクセスコントロールによるデータ取得

Customers Collectionにデータ追加を行ったので/api/customersにGETリクエストを送信してユーザ情報が取得できるか確認を行います。REST APIでのURLについてはドキュメントに記述されてており、/api/{collection_slug}にGETメソッドでアクセスするとFindが実行されCollectionのデータを取得することができます。

REST API
REST API

Visual Studio Codeを利用している場合にはAPIリクエストを送信する際にExtensionsのREST ClientやThunder Clientを利用することができます。それ以外にはPostmanやcurlコマンドも利用できます。

ここではThunder Clientを利用してGETリクエストを送信します。リクエストを送信するとステータス403のForbiddenが戻されてアクセスすることができません。

403 Forbidden
403 Forbidden

アクセスできない原因はすべてのCollectionはデフォルトでログインしているユーザしかアクセスができない設定になっているためです。


const defaultPayloadAccess = ({ req: { user } }) => {
  // Return `true` if a user is found
  // and `false` if it is undefined or null
  return Boolean(user)
}

Collections, Fields, Globalsの単位でアクセスコントロールの設定を行うことができるのでドキュメントのCollection Access Controlを参考に設定を行います。

create, read, update, deleteで設定を行うことができますがGETリクエストによりデータの取得を行いたいのでreadの設定を行います。設定はcollections/Customer.tsファイルで行います。accessオプションを追加してreadプロパティに設定した関数の戻り値をtrueにしています。


import { CollectionConfig } from 'payload/types';

const Customers: CollectionConfig = {
  slug: 'customers',
  auth: true,
  admin: {
    useAsTitle: 'firstName',
  },
  fields: [
    { name: 'firstName', type: 'text', required: true },
    { name: 'lastName', type: 'text', required: true },
  ],
  access: {
    read: () => true,
  },
};

export default Customers;

設定後に再度/api/customersに対してGETリクエストを送信します。先程とは異なり、ステータスコード200でcustomersの情報が戻されていることが確認できます。

GETリクエストによるデータ取得
GETリクエストによるデータ取得

このようにCollections毎にアクセスコントロールを設定することができます。動作確認ができたのでaccessオプションの設定はコメントしておきます。


import { CollectionConfig } from 'payload/types';

const Customers: CollectionConfig = {
  slug: 'customers',
  auth: true,
  admin: {
    useAsTitle: 'firstName',
  },
  fields: [
    { name: 'firstName', type: 'text', required: true },
    { name: 'lastName', type: 'text', required: true },
  ],
  // access: {
  //   read: () => true,
  // },
};

export default Customers;

認証

すべてのCollectionはデフォルトでログインしているユーザしかアクセスができない設定になっていることを説明しました。ではどのようにログインを行うことができるのでしょうか。

ログイン

ログインを行うためのREST APIでのAPIエンドポイントはドキュメントのAuthenticationのLoginに記述されています。


http://localhost:3000/api/[collection-slug]/login
ログインの方法
ログインの方法

[collection-slug]にcustomersを設定します。

Customersで作成したユーザのemailとpasswordを設定してPOSTリクエストを送信します。ログインに成功するとTokenの情報と有効期限とメッセージとログインしたユーザの情報が戻されます。

ログインのためPOSTリクエストを送信
ログインのためPOSTリクエストを送信

PayloadではHTTP-only cookiesを利用しているのでCookiesを見るとpayload-cookieを確認することができます。

 Cookieの確認
Cookieの確認

Cookiesが設定されたまま/api/customersにGETリクエストを送信するとログインが完了しているのでデータを取得することができます。

ログアウト

ログアウトを行いたい場合には以下のURLにPOSTリクエストを送信することでログアウトすることができます。


http://localhost:3000/api/[collection-slug]/logout
ログアウト処理
ログアウト処理

ログインしている場合はログアウト処理が正常に行われると”You have been loggged out successfuly”と表示され、ログインしていない状態でPOSTリクエストを送信するとステータスコード400のBad Requestで”No User”のメッセージが戻されます。

Me

ログインしているユーザ情報を戻したい場合には以下のURLにGETリクエストを送信することで取得することができます。


http://localhost:3000/api/[collection-slug]/me

その他にもRefresh Tokenを取得するURLなどもあります。

ファイルのアップロード

ファイルをアップロードできるCollection Mediaを新たに追加します。

collectionsディレクトリにMedia.tsファイルを作成します。ファイルをアップロードする場合はCollectionにuploadオプションを設定します。staticDirでファイルを保存するディレクトリを指定しています。


import { slateEditor } from '@payloadcms/richtext-slate';
import path from 'path';
import type { CollectionConfig } from 'payload/types';

const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    staticDir: path.resolve(__dirname, '../../../media'),
  },
  fields: [
    {
      name: 'alt',
      type: 'text',
      required: true,
    },
    {
      name: 'caption',
      type: 'richText',
      editor: slateEditor({
        admin: {
          elements: [],
        },
      }),
    },
  ],
};

export default Media;

フィールドにはaltとcaptionを追加していますがcaptionではtypeをrichTextとしてslateEditorを指定しています。adminのelementsの配列にオプションを設定することでEditorをカスタマイズすることができます。何も設定しない状態で設定します。

Media.tsファイルが作成できたらpayload.config.tsファイルに作成したMedia Collectionを追加します。


import path from 'path';

import { payloadCloud } from '@payloadcms/plugin-cloud';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
import { webpackBundler } from '@payloadcms/bundler-webpack';
import { slateEditor } from '@payloadcms/richtext-slate';
import { buildConfig } from 'payload/config';

import Users from './collections/Users';
import Customers from './collections/Customers';
import Media from './collections/Media';

export default buildConfig({
  admin: {
    user: Users.slug,
    bundler: webpackBundler(),
  },
  editor: slateEditor({}),
  collections: [Users, Customers, Media],
  typescript: {
    outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
  graphQL: {
    schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
  },
  plugins: [payloadCloud()],
  db: mongooseAdapter({
    url: process.env.DATABASE_URI,
  }),
});

/adminにアクセスするとMedia Collectionが表示されます。

追加したMedia Collectionの確認
追加したMedia Collectionの確認

Mediaをクリックすると一覧画面が表示され、Mediaを追加するために”Create new Media”ボタンをクリックします。

Media一覧画面
Media一覧画面

Media作成画面ではファイルのアップロードはDrag&Dropでも行うことができます。

Media作成画面
Media作成画面

ファイルを選択してAlt, Captionに文字列を入力しています。CaptionではSlate Editorにより文字の太さなどを変更することができます。入力が完了したら”Save”ボタンをクリックします。

Mediaのフォームに入力後の画面
Mediaのフォームに入力後の画面

“Save”ボタンをクリックするとプロジェクトディレクトリ直下にmediaディレクトリが作成されアップロードしたファイルが保存されます。

Media一覧画面にもアップロードしたファイルの情報が表示されます。

アップロードしたファイルの情報
アップロードしたファイルの情報

次にSlateEditorのカスタマイズを行うためにelementsの配列にデフォルトで利用可能なh1とlinkを設定します。


{
  name: 'caption',
  type: 'richText',
  editor: slateEditor({
    admin: {
      elements: ['h1', 'link'],
    },
  }),
},

h1タグとリンクのアイコンが追加されていることが確認できます。

カスタマイズしたCaptionエリア
カスタマイズしたCaptionエリア

Customer Collectionで作成したユーザを利用して/api/mediaに対してGETリクエストを送信するとアップロードしたファイルの情報を確認することができます。 Rich Textで入力した値については下記のように保存され、ファイル名やmimeTypeなどURL以外の情報も確認できます。


{
  "docs": [
    {
      "id": "6571da6af24a28c5b6a980a3",
      "alt": "Payload Hero Image",
      "caption": [
        {
          "children": [
            {
              "text": "Payload",
              "bold": true
            },
            {
              "text": "の"
            },
            {
              "text": "Hero Image",
              "italic": true
            },
            {
              "text": "です。"
            }
          ]
        }
      ],
      "filename": "payload_cms.png",
      "mimeType": "image/png",
      "filesize": 14916,
      "width": 1200,
      "height": 645,
      "createdAt": "2023-12-07T14:44:58.210Z",
      "updatedAt": "2023-12-07T14:44:58.210Z",
      "url": "/media/payload_cms.png"
    }
  ],
  "totalDocs": 1,
  "limit": 10,
  "totalPages": 1,
  "page": 1,
  "pagingCounter": 1,
  "hasPrevPage": false,
  "hasNextPage": false,
  "prevPage": null,
  "nextPage": null
}
ユーザのログインが行われていない場合にはエラーになります。

Collectionのフィールドの設定やAccess Controlなどまだまださまざまな機能がありますがPayloadの基本的な機能を確認することができました。