Trendy Coder Logo
  • Home
  • About
  • Blog
  • Newsletter

Payload CMS Collections: How They Streamline Content Management

Posted on: April 04 2025
By Dave Becker
Hero image for Payload CMS Collections: How They Streamline Content Management

With Payload CMS 3.0, Collections have received a major upgrade, making it easier than ever to organize, manage, and design data models for any business. Using the latest release, I'll show how Collections can greatly streamline content managment.

In this article I'm going to build a college course Collections model to manage students, teachers, courses and more.

This exercise will emphasize the flexibility of Collections and content workflows and cover the following:

  • What are Collections?
  • How to create Collections
  • Collection relationship and join usage
  • How to query Collections with relationships
  • How Collections build amazing admin interfaces

Getting Started

If you would like to follow along, you can generate a Payload project with the following command.

npx create-payload-app

Overview of Collections

Let's start by getting the basic idea of what a Collection is and why they are at the core of Payload.

What is a Collection?

A Collection is essentially a grouping of field types that map to a database record. Payload uses these field mappings to build a model representation called a schema.

Collections provide a handle to the schema mapping but can do much more than simple database mapping.

Payload uses the term document to refer to a record which is a term used in relational databases.

The key benefits of the generated schema include:

  • Automatically generates powerful Admin UI pages for managing content.
  • Generates all the needed TypeScript interfaces.
  • Generates a GraphQL schema for building powerful queries.
  • Creates consistent interfaces for document relationships.
  • Provides interfaces for media and content upload handling.
  • Generates local, REST and GraphQL endpoint API's.
  • Uses the schema to validate data.

Collections are strongly typed

Payload CMS is a TypeScript rich framework, built with consistency in mind.

This allows developers to build Collection models without the need to define any typing which if written by hand could lead to inconsistent and duplicate types.

So in this article we'll take a look at some common Collection models and patterns to get a general feel for how to use them in a practical way.

Building a School Course Schema

The first step is to have a visual representation and roadmap of what to build. Here's the schema design that I'll be using.

Each block in the diagram represents a Collection that will be defined. The connecting lines represent the relationships between each Collection.

A diagram showing database schema for students, teachers and courses

Analyzing the schema

Let's analyze the schema and how each Collection will relate to the other.

  • Students: Each student can pick many courses (many-to-many) and have only one major (one-to-one).
  • Teachers: Each teacher can teach many courses (many-to-many) and only be assigned to one department (one-to-one).
  • Courses: Each course can have many students (many-to-many) and only have one assigned teacher to a course (one-to-one).
  • Departments: Each department can be associated with many majors (one-to-many)
  • Majors: Each major can have many students (many-to-many) and only fall under one department (many-to-one).
  • Pictures: An upload Collection to store all of the student and teacher profile pictures.

Payload Collection realtionships

In this exercise, I'll be using some of the key relationship fields in Payload, including:

  • relationship: A one directional relationship linking one Collection to another.
  • join: A new feature which offers support for bi-directional relationship queries.

The owning side of relationships

One thing to keep in mind is that Payload uses the relationship field as the owning side in the relationship.

This means that even though the relationship and the join will be used together, the relationship side still makes all the modifications to the Collections.

Why use a join field?

The join will provide bi-directional query support to the adjoining Collections.

The join field doesn't have to really be used in this case and you could only use the relationship field to do the job but it will provide a lot more visibility in the Admin UI pages.

This is very beneficial for content managers to have greater visibility. Also, there are times when developers need the inverse relationship data to build pages.

Join relationships can be taxing

One thing to be cautious of when using a join is that it will increase the size of query responses because it loads the relationships too, even if setting depth = 0.

The Admin UI will handle these concerns but when you build your own custom pages and query with joins, it's worth considering using GraphQL for more optimized and tailored queries.

Here's a recent article that goes into more depth on GraphQL usage and optimization. GraphQL Optimization in Payload CMS

Defining the Students Collection

Here's the Collection code for the Students configuration.

Students Collection configuration
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import type { CollectionConfig } from 'payload'

export const Students: CollectionConfig = {
  slug: 'students',
  admin: {
    useAsTitle: 'fullName',
  },
  fields: [
    {
      name: 'picture',
      type: 'upload',
      relationTo: 'pictures',
    },
    {
      type: 'row',
      fields: [
        {
          name: 'firstName',
          label: 'First Name',
          type: 'text',
          admin: {
            width: '50%',
          },
          required: true,
        },
        {
          name: 'lastName',
          label: 'Last Name',
          type: 'text',
          admin: {
            width: '50%',
          },
          required: true,
        },
        {
          name: 'fullName',
          label: 'Full Name',
          type: 'text',
          hooks: {
            beforeChange: [
              ({ data }) => {
                return `${data?.firstName} ${data?.lastName}`
              },
            ],
          },
          admin: {
            hidden: true,
          },
        },
      ],
    },
    {
      name: 'bio',
      label: 'Bio',
      type: 'richText',
      required: true,
      editor: lexicalEditor(),
    },
    {
      name: 'courses',
      type: 'relationship',
      relationTo: 'courses',
      hasMany: true,
    },
    {
      name: 'major',
      type: 'relationship',
      relationTo: 'majors',
      hasMany: false,
    }
  ]
}

Analyzing the Students Collection

So the students configuration defines the following fields:

  • picture: Creates a relationship to allow for photo upload and storage.
  • firstName: Text field for first name.
  • lastName: Text field for last name.
  • fullName: A hidden field that has a hook to store the full name before any updates so it can be used as a title.
  • bio: A rich text field for bio content and information.
  • courses: A relationship field to assign students to many courses.
  • major: A relationship field to set the student to a single major.

Collections provide a way to define the model and structure of data so it can be managed through the Admin UI, however, custom pages will use the API's to build customer facing pages.

Generated Students Admin UI

I seeded the database with some random fake mock data to provide a more realistic demo but here's what the Admin UI will generate for the Students Collection.

Shows a table list of registered students in Payload Admin UI

By clicking on one of the table rows it will show the edit details page for a student.

As you can see the courses and the major can be selected and removed by the owning relationship side.

Shows a student collection edit form in Payload Admin UI

Querying the API for Students

Using the REST query and depth set to 0, with two relationship fields courses and major.

http://localhost:3000/api/students?limit=1&depth=0

Response:

{
    "docs": [
        {
            "createdAt": "2025-04-02T23:08:22.198Z",
            "updatedAt": "2025-04-02T23:08:22.666Z",
            "firstName": "Claudine",
            "lastName": "Upton-Osinski",
            "fullName": "Claudine Upton-Osinski",
            "bio": {
                "root": {
                }
            },
            "major": "67edc365496a465fb93cd26c",
            "picture": "67edc366496a465fb93cd3a2",
            "courses": [
                "67edc366496a465fb93cd41b",
                "67edc366496a465fb93cd431",
                "67edc366496a465fb93cd444",
                "67edc366496a465fb93cd450",
                "67edc366496a465fb93cd44d",
                "67edc366496a465fb93cd441",
                "67edc366496a465fb93cd473",
                "67edc366496a465fb93cd44f",
                "67edc366496a465fb93cd460",
                "67edc366496a465fb93cd43a"
            ],
            "id": "67edc366496a465fb93cd3a4"
        }
    ],
    "totalDocs": 10,
    "limit": 1,
    "totalPages": 10,
    "page": 1,
    "pagingCounter": 1,
    "hasPrevPage": false,
    "hasNextPage": true,
    "prevPage": null,
    "nextPage": 2
}

Defining the Teachers Collection

Let's add the teachers configuration next since it's similar to the Students Collection but with slighty different relationship fields.

Teachers Collection configuration
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import type { CollectionConfig } from 'payload'

export const Teachers: CollectionConfig = {
  slug: 'teachers',
  admin: {
    useAsTitle: 'fullName',
  },
  fields: [
    {
      name: 'picture',
      type: 'upload',
      relationTo: 'pictures',
    },
    {
      type: 'row',
      fields: [
        {
          name: 'firstName',
          label: 'First Name',
          type: 'text',
          admin: {
            width: '50%',
          },
          required: true,
        },
        {
          name: 'lastName',
          label: 'Last Name',
          type: 'text',
          admin: {
            width: '50%',
          },
          required: true,
        },
        {
          name: 'fullName',
          label: 'Full Name',
          type: 'text',
          hooks: {
            beforeChange: [({data}) => {
              return `${data?.firstName} ${data?.lastName}`
            }]
          },
          admin: {
            hidden: true
          },
        },
      ],
    },
    {
      name: 'bio',
      label: 'Bio',
      type: 'richText',
      required: true,
      editor: lexicalEditor(),
    },
    {
      name: 'department',
      type: 'join',
      collection: 'departments',
      on: 'teachers'
    },
    {
      name: 'assignedCourses',
      type: 'join',
      collection: 'courses',
      on: 'teacher',
    },
  ]
}

Analyzing the Teachers Collection

The Teachers configuration defines the following fields:

  • picture: Creates a relationship to allow for photo upload and storage.
  • firstName: Text field for first name.
  • lastName: Text field for last name.
  • fullName: A hidden field that has a hook to store the full name before any updates so it can be used as a title.
  • bio: A rich text field for bio content and information.
  • courses: A join field, to see which courses a teacher has been assigned to. The Courses Collection will have the relationship field to manage the assignment.
  • department: A join field, to see which department a teacher is assigned to. The Departments Collection will have the relationship field to assign this value.

Generated Teachers Admin UI

Here's the listing page of teachers using seeded mock data.

Shows a table list of registered teachers in Payload Admin UI

As we start to add the other Collections, we'll see how the relationships come together.

Notice how the join fields for the department and courses field are read only

Shows a teacher collection edit form in Payload Admin UI

Querying the API for Teachers

Using the REST query and depth set to 0, with two join fields department and assignedCourses.

http://localhost:3000/api/teachers?limit=1&depth=0

Response:

{
    "docs": [
        {
            "createdAt": "2025-04-02T23:08:22.616Z",
            "updatedAt": "2025-04-02T23:08:22.616Z",
            "firstName": "Maeve",
            "lastName": "Lehner",
            "fullName": "Maeve Lehner",
            "bio": {
                "root": {
                }
            },
            "picture": "67edc366496a465fb93cd3f5",
            "department": {
                "docs": [
                    "67edc365496a465fb93cd34a"
                ],
                "hasNextPage": false
            },
            "assignedCourses": {
                "docs": [
                    "67edc366496a465fb93cd433",
                    "67edc366496a465fb93cd464",
                    "67edc366496a465fb93cd41a",
                    "67edc366496a465fb93cd455",
                    "67edc366496a465fb93cd459",
                    "67edc366496a465fb93cd427",
                    "67edc366496a465fb93cd46e",
                    "67edc366496a465fb93cd41b",
                    "67edc366496a465fb93cd404",
                    "67edc366496a465fb93cd426"
                ],
                "hasNextPage": true
            },
            "id": "67edc366496a465fb93cd3f7"
        }
    ],
    "hasNextPage": true,
    "hasPrevPage": false,
    "limit": 1,
    "nextPage": 2,
    "page": 1,
    "pagingCounter": 1,
    "prevPage": null,
    "totalDocs": 10,
    "totalPages": 10
}

Defining the Picture Collection

I'll add the Pictures Collection since both the student and teacher Collections use this field to upload their profile picture.

Profile picture Collection configuration
import type { CollectionConfig } from 'payload'

export const Pictures: CollectionConfig = {
  slug: 'pictures',
  access: {
    read: () => true,
  },
  fields: [],
  upload: true,
}

The Pictures Collection doesn't contain any fields but they could be added if needed but only needs to set the following:

  • upload: Set to true to tell Payload to manage file uploads and content storage.

Using a different upload Collection versus using the default media Collection will give some isolation so web assets don't get mixed with the school profile pictures.

Generated Pictures Admin UI

Here's the listing page for Pictures.

Shows a table list of student and teacher picture uploads in Payload Admin UI

Defining the Courses Collection

The Courses Collection will start to tie things together, since the students and teachers reference this Collection in relationships.

Courses Collection configuration
import type { CollectionConfig } from 'payload'

export const Courses: CollectionConfig = {
  slug: 'courses',
  admin: {
    useAsTitle: 'name',
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'description',
      type: 'text',
    },
    {
      name: 'credits',
      type: 'number',
      defaultValue: 3,
    },
    {
      name: 'capacity',
      type: 'number',
      defaultValue: 30,
    },
    {
      name: 'teacher',
      type: 'relationship',
      relationTo: 'teachers',
      hasMany: false,
    },
    {
      name: 'enrolledStudents',
      type: 'join',
      collection: 'students',
      on: 'courses',
    },
  ],
}

Analyzing the Courses Collection

The Courses configuration defines the following fields:

  • name: Text field for the name of the course.
  • description: Text field for the course description.
  • credits: A number field for amount of credits for the course.
  • capacity: A number field to hold the maximum student enrollment.
  • teacher: A relationship field to add a single teacher to be assigned to each course.
  • enrolledStudents: A join field, to see which students have enrolled in this course.

Generated Courses Admin UI

Here's the listing page for Courses with some seeded mock data.

Shows a table list of courses in Payload Admin UI

By selecting a row, you can edit the course details and set a teacher to be assigned to the course.

The admin page also shows the students that have been enrolled which is really one of the best features of using the join field.

Shows a courses collection edit form in Payload Admin UI

Querying the API for Courses

Using the REST query and depth set to 0, with one relationship field teacher and one join field enrolledStudents.

http://localhost:3000/api/courses?limit=1&depth=0

Response:

{
    "docs": [
        {
            "createdAt": "2025-04-02T23:08:22.636Z",
            "updatedAt": "2025-04-02T23:08:22.743Z",
            "name": "Intro to Radio and Television",
            "description": "Intro to Radio and Television Course",
            "credits": 3,
            "capacity": 30,
            "teacher": "67edc366496a465fb93cd3c0",
            "enrolledStudents": {
                "docs": [
                    "67edc366496a465fb93cd399",
                    "67edc365496a465fb93cd35a"
                ],
                "hasNextPage": false
            },
            "id": "67edc366496a465fb93cd467"
        }
    ],
    "hasNextPage": true,
    "hasPrevPage": false,
    "limit": 1,
    "nextPage": 2,
    "page": 1,
    "pagingCounter": 1,
    "prevPage": null,
    "totalDocs": 134,
    "totalPages": 134
}

Defining the Departments Collection

The Departments Collection is primarily used to organize the following:

  • Each department can assign many teachers.
  • Each department can have many majors, since the teachers in this department are likely to specialize on these majors.
Departments Collection configuration
import type { CollectionConfig } from 'payload'

export const Departments: CollectionConfig = {
  slug: 'departments',
  admin: {
    useAsTitle: 'name'
  },
  fields: [
    {
      name: 'name',
      label: 'Department Name',
      type: 'text',
    },
    {
      name: 'majors',
      type: 'relationship',
      relationTo: 'majors',
      hasMany: true
    },
    {
      name: 'teachers',
      label: 'Assigned Teachers',
      type: 'relationship',
      relationTo: 'teachers',
      hasMany: true
    },
  ],
}

Analyzing the Departments Collection

The Departments configuration defines the following fields:

  • name: Text field for the department name.
  • teachers: A relationship field to assign teachers to the department.
  • majors: A relationship field to add and manage majors that will fall under this department.

Generated Departments Admin UI

Any majors and teachers can be managed from this interface since they both define the relationship field.

Shows a table list of school departments in Payload Admin UI

By selecting a row, the department details can be edited.

Shows a department collection edit form in Payload Admin UI

Querying the API for Departments

Using the REST query and depth set to 0, with two relationship fields majors and teachers.

http://localhost:3000/api/departments?limit=1&depth=0

Response:

{
    "docs": [
        {
            "createdAt": "2025-04-02T23:08:21.623Z",
            "updatedAt": "2025-04-02T23:08:22.848Z",
            "name": "Humanities",
            "majors": [
                "67edc365496a465fb93cd281",
                "67edc365496a465fb93cd282",
                "67edc365496a465fb93cd29e",
                "67edc365496a465fb93cd29f"
            ],
            "teachers": [
                "67edc366496a465fb93cd3f7"
            ],
            "id": "67edc365496a465fb93cd34a"
        }
    ],
    "totalDocs": 7,
    "limit": 1,
    "totalPages": 7,
    "page": 1,
    "pagingCounter": 1,
    "hasPrevPage": false,
    "hasNextPage": true,
    "prevPage": null,
    "nextPage": 2
}

Defining the Majors Collection

The final configuration that needs to be defined is the Majors Collection.

Majors Collection configuration
import type { CollectionConfig } from 'payload'

export const Majors: CollectionConfig = {
  slug: 'majors',
  admin: {
    useAsTitle: 'name',
  },
  fields: [
    {
      name: 'name',
      label: 'Major Name',
      type: 'text',
      required: true,
    },
    {
      name: 'department',
      type: 'join',
      collection: 'departments',
      on: 'majors'
    }
  ]
}

Analyzing the Majors Collection

The Majors configuration defines the following fields:

  • name: Text field for the major name.
  • department: A join field to view and query on the department these majors are assigned to.

Generated Majors Admin UI

Here's the list page of Majors. Notice the department is visible in the listing for quick reference.

Shows a table list of school majors in Payload Admin UI

By selecting a row, the majors can be edited. The only field that is editable is the name of the major.

Majors will actually be assigned in the student and department Collection Admin UI interfaces.

Shows a school major collection edit form in Payload Admin UI

Querying the API for Courses

Using the REST query and depth set to 0, with one join field department.

http://localhost:3000/api/majors?limit=1&depth=0

Response:

{
    "docs": [
        {
            "createdAt": "2025-04-02T23:08:21.599Z",
            "updatedAt": "2025-04-02T23:08:21.599Z",
            "name": "Humanities",
            "department": {
                "docs": [
                    "67edc365496a465fb93cd34a"
                ],
                "hasNextPage": false
            },
            "id": "67edc365496a465fb93cd28e"
        }
    ],
    "hasNextPage": true,
    "hasPrevPage": false,
    "limit": 1,
    "nextPage": 2,
    "page": 1,
    "pagingCounter": 1,
    "prevPage": null,
    "totalDocs": 134,
    "totalPages": 134
}

Adding the new Collections

The last step is to add the new Collections to the main payload.config.ts file so the server can be started and you can try out the new Admin UI.

/src/payload.config.ts
// ...
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { Courses } from './payload/collections/Courses'
import { Departments } from './payload/collections/Departments'
import { Majors } from './payload/collections/Majors'
import { Students } from './payload/collections/Students'
import { Teachers } from './payload/collections/Teachers'
import { Pictures } from './payload/collections/Pictures'

export default buildConfig({
  // ...
  collections: [Students, Teachers, Courses, Departments, Majors, Pictures],
  editor: lexicalEditor(),
})

Summing it all up

Of course there are many details left to be implemented but the general schema structure and definition is in place.

Key takeaways of Payload CMS Collections

A few key aspects about using Payload CMS are:

  • It can greatly simplify building manageable content with the auto-generated admin pages.
  • The relationship and join fields are powerful tools to build the foundation for real world apps.
  • Collections have great API flexibility to access your Collection data.

By using Payload CMS it can truly streamline your workflows without the burden of building boilerplate code.

Where to go from here

Now that the schema and the admistrative aspects are in place, the data can now be used in a meaningful way using Next.js React pages.

For other related articles for Collections, you might want to check out Document Nesting With Payload's Nested Docs Plugin and also Maximizing Efficiency: The Power of Payload CMS Blocks.

The generated API endpoints are great for building your own pages with precision but using relationships the right way is key to query performance.

Some basic guidelines are:

  • Don't over use the join but use it when it makes sense.
  • Utilize GraphQL for your custom pages that use joins because REST can dig pretty deep into relationships.
  • Set the depth = 0 for REST to get only the nested ID's unless the nested relationship data is actually needed.
  • Set GraphQL complexity for certain fields if needed to reduce response sizes.

I hope this article helps to better understand some of the relationship aspects of Payload CMS.

Topics

SEOLinuxSecuritySSHEmail MarketingMore posts...

Related Posts

Hero image for Boost Payload CMS with Search: Step-by-Step Tutorial
Posted on: August 11 2025
By Dave Becker
Boost Payload CMS with Search: Step-by-Step Tutorial
Hero image for Server-Side Pagination Made Easy in Payload CMS
Posted on: August 11 2025
By Dave Becker
Server-Side Pagination Made Easy in Payload CMS
Hero image for Payload CMS: Getting Started Using the New Join Field
Posted on: August 11 2025
By Dave Becker
Payload CMS: Getting Started Using the New Join Field
Hero image for Maximizing Efficiency: The Power of Payload CMS Blocks
Posted on: August 11 2025
By Dave Becker
Maximizing Efficiency: The Power of Payload CMS Blocks
Hero image for Create Custom Forms Using Payload CMS Form Builder Plugin
Posted on: August 11 2025
By Dave Becker
Create Custom Forms Using Payload CMS Form Builder Plugin
Hero image for Payload CMS SEO Plugin: Boosting Your Site's Search Ranking
Posted on: April 04 2025
By Dave Becker
Payload CMS SEO Plugin: Boosting Your Site's Search Ranking
Hero image for GraphQL Optimization in Payload CMS
Posted on: April 04 2025
By Dave Becker
GraphQL Optimization in Payload CMS
Hero image for Exploring the Game-Changing Features of Payload CMS 3.0
Posted on: April 04 2025
By Dave Becker
Exploring the Game-Changing Features of Payload CMS 3.0
Hero image for Document Nesting With Payload's Nested Docs Plugin
Posted on: April 04 2025
By Dave Becker
Document Nesting With Payload's Nested Docs Plugin
Hero image for Payload CMS Collections: How They Streamline Content Management
Posted on: April 04 2025
By Dave Becker
Payload CMS Collections: How They Streamline Content Management
Trendy Coder Logo
Resources
  • Blog
Website
  • Home
  • About us
Subscribe

Get the latest news and articles to your inbox periodically.

We respect your email privacy

© TrendyCoder.com. All rights reserved.