Introduction to Payload, a Headless CMS and App Framework
We take a look at Payload, an intriguing CMS and app framework choice if you don’t need your frontend and backend to be tied together.
Sep 9th, 2024 5:43am by

Image via Unsplash+.
Installation
As yet, the installation pre-requisite options are a bit narrow for databases, but there is one relational and one document-based example to choose from:
On my trusty MacBook, I installed a community MongoDB via Homebrew:

Then I started Mongo up by adding it as a service:
We can see the connection string as the URL, so we should be able to set up Payload.
In another browser tab, I installed the Payload app:
I set up a demo project and soon we are ready to launch:
One of the example projects failed because it expected Discord! As I’ve said, because of the upcoming changes some of the documentation and videos don’t quite match up yet. That’s a good thing of course — the project is very active. I selected the payload-demo template, which was fine.
Then “yarn dev” runs the project:
After MongoDB is connected to, it starts up both the payload admin (at localhost:3000/admin) as well as the demonstration app (marked as Next.js app, the React framework used for the template).
The Payload App
Going straight into the app we see:
At this point, there is no content, so you are guided to the admin dashboard to start making some. The admin dashboard allows me to create an Admin role or User role, with email.
The dashboard itself helps to explain what Payload is actually about:
Payload organizes everything into sets of types in collections. This include Pages and Users. We can create new types and start using them immediately, as we will do.
There is a slight confusion here: To start things off, you press the link to “Seed the database,” and you will get lots of demo content. That will clean out anything that already exists. You also need to make a “home” page, otherwise you will just see the default template page from above, which will perhaps make you think you have no content, when you have.
Once I understood the system, I created some simple if uninspired content on a Page type:
Once you use the admin interface to add to the collections, you can then Publish any changes (commit them). This will automatically update your site.
Doing Everything in Code
Now at this stage, I’ve not done much more than you could do with, say, Publii, which also acts like a classic CMS. However, there are two things that stand out in Payload. Not only can you talk to it in REST, you can reuse parts of Payload that blur the distinction between who owns what. And you can do everything in code, which is where we go now. For example, you can see that Users is a collection type, available for use by us:
And where are these registered? All in the `payload.config.ts` file. First they are imported, then (as you can see below) added into the known collections:
Within the Users folder, we have a basic index.ts file, which defines the Users type. While this includes quite a lot of Typescript, most of it is just describing the admin access. Payload allows quite a lot of granular access control:
import type { CollectionConfig } from 'payload/types'
import { admins } from '../../access/admins'
import { anyone } from '../../access/anyone'
import adminsAndUser from './access/adminsAndUser'
import { checkRole } from './checkRole'
import { ensureFirstUserIsAdmin } from './hooks/ensureFirstUserIsAdmin'
import { loginAfterCreate } from './hooks/loginAfterCreate'
const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'email'],
},
access: {
read: adminsAndUser,
create: anyone,
update: adminsAndUser,
delete: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {
afterChange: [loginAfterCreate],
},
auth: true,
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'roles',
type: 'select',
hasMany: true,
defaultValue: ['user'],
options: [
{
label: 'admin',
value: 'admin',
},
{
label: 'user',
value: 'user',
},
],
hooks: {
beforeChange: [ensureFirstUserIsAdmin],
},
access: {
read: admins,
create: admins,
update: admins,
},
},
],
timestamps: true,
}
export default Users
import type { CollectionConfig } from 'payload/types'
const Members: CollectionConfig = {
slug: 'members',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'membership',
type: 'number',
},
],
timestamps: true,
}
export default Members
And immediately the collections recognised the new type within the admin panel:
And we can manipulate members like any other collection type:
One last thing. Let’s access our new member via REST:
So we are locked out. But wait a minute … remember the granular access we removed to make the Member collection simple? Let’s try to put a bit back in:
import type { CollectionConfig } from 'payload/types'
import { anyone } from '../../access/anyone'
const Members: CollectionConfig = {
slug: 'members',
access: {
read: anyone,
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'memebership',
type: 'number',
},
],
timestamps: true,
}
export default Members
All I did was add the import for “anyone” and add that access. Now let’s try:
Pretty good. We created a new collection, saw it in the admin console, created an entry for it and even requested it via REST. So this content is now available for my site.
Conclusion
As I said earlier, Payload is currently transitioning to version 3, so it may make sense to hold out for a bit before investigating it. That said, the idea is already quite a strong one if you don’t insist that your frontend and backend endure a shotgun wedding.
YOUTUBE.COM/THENEWSTACK
Tech moves fast, don't miss an episode. Subscribe to our YouTube
channel to stream all our podcasts, interviews, demos, and more.
