How to Build Your Own Payload Plugin

Published On
New Payload Plugin Template
New Payload Plugin Template
Building your own Payload Plugin just got a whole lot easier – thanks to our new Plugin Template.

To use the plugin template run npx create-payload-app@latest -t plugin -n my-new-plugin directly in your terminal or clone the repo from GitHub.

This new template comes with everything you need to build a full life-cycle plugin:

  • Example files and functions for extending the payload config

  • A local dev environment to develop the plugin

  • Jest test suite with integrated GitHub workflow

The purpose of this template is to help you jumpstart building your own Plugin, while providing all the tools that will support your plugin from development to production.

To get started, all you need is:

  • A basic understanding of Payload 
  • Typescript / JavaScript experience

Plugin Template

In the payload-plugin-template, you will see a common file structure that is used across all plugins:

  1. root folder - general configuration

  2. /src folder - everything related to the plugin

  3. /dev folder - sanitized test project for development

Root

In the root folder, you will see various files that relate to the configuration of the plugin. We set up our environment in a similar manner in Payload core and across other projects, so hopefully these will look familiar:

  • README.md* - This contains instructions on how to use the template. When you are ready, update this to contain instructions on how to use your Plugin.
  • package.json* - Contains necessary scripts and dependencies. Overwrite the metadata in this file to describe your Plugin.
  • .editorconfig - Defines settings to maintain consistent coding styles.
  • .eslintrc.js - Eslint configuration for reporting on problematic patterns.
  • .gitignore - List specific untracked files to omit from Git.
  • .prettierrc.js - Configuration for Prettier code formatting.
  • LICENSE - As part of the open-source community, we ship all plugins with an MIT license but it is not required.
  • tsconfig.json - Configures the compiler options for TypeScript 

* IMPORTANT: You will need to modify these files.

Dev 

The purpose of the dev folder is to provide a sanitized local Payload project – so you can run and test your plugin while you are actively developing it.  

Do not store any of the plugin functionality in this folder - it is purely an environment to assist you with developing the plugin.

If you’re starting from scratch, you can easily setup a dev environment like this:

1
mkdir dev
2
cd dev
3
npx create-payload-app

If you’re using the plugin template, the dev folder is built out for you and the samplePlugin has already been installed in dev/payload.config().

1
plugins: [
2
// when you rename the plugin or add options, make sure to update it here
3
samplePlugin({
4
enabled: false,
5
})
6
]

You can add to the dev/payload.config and build out the dev project as needed to test your plugin.

When you’re ready to start development, navigate into this folder with cd dev

And then start the project with yarn dev and pull up http://localhost:3000/ in your browser.

Testing 

Another benefit of the dev folder is that you have the perfect environment established for testing.

A good test suite is essential to ensure quality and stability in your plugin. Payload typically uses Jest; a popular testing framework, widely used for testing JavaScript and particularly for applications built with React. 

Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the Jest documentation.

The plugin template provides a stubbed out test suite at dev/plugin.spec.ts which is ready to go - just add in your own test conditions.

1
import payload from 'payload'
2
3
describe('Plugin tests', () => {
4
// Example test to check for seeded data
5
it('seeds data accordingly', async () => {
6
const newCollectionQuery = await payload.find({
7
collection: 'newCollection',
8
sort: 'createdAt',
9
})
10
11
newCollection = newCollectionQuery.docs
12
13
expect(newCollectionQuery.totalDocs).toEqual(1)
14
})
15
})
16
Seeding data

It is a good habit to seed data for each test, this isolates them and ensures they are repeatable. You can see in the example above how easy this is to do with the local API payload offers.

In the plugin template, you can navigate to dev/src/server.ts and see an example seed function.

1
if (process.env.PAYLOAD_SEED === 'true') {
2
await seed(payload)
3
}

A sample seed function has been created for you at dev/src/seed – update this file with additional data as needed.

1
export const seed = async (payload: Payload): Promise<void> => {
2
payload.logger.info('Seeding data...')
3
4
await payload.create({
5
collection: 'new-collection',
6
data: {
7
title: 'Seeded title',
8
},
9
})
10
11
// Add additional seed data here
12
}
13

Src

Now that we have our environment setup and dev project ready to go - it’s time to build the plugin.

index.ts

First up, the src/index.ts file - this is where the plugin should be imported from. It is best practice not to build the plugin directly in this file, instead we use this to export the plugin and types from their respective files.

Plugin.ts

To reiterate, the essence of a payload plugin is simply to extend the payload config - and that is exactly what we are doing in this file.

1
export const samplePlugin =
2
(pluginOptions: PluginTypes) =>
3
// ^ The args you want to expose to people using this plugin
4
(incomingConfig: Config): Config => {
5
// ^ This will be plugin users config
6
let config = { ...incomingConfig }
7
8
// do something cool with the config here
9
10
return config
11
}

First, you need to receive the existing payload config along with any plugin options.

Then set the variable config to be equal to a copy of the existing config.

From here, you can extend the config as you wish. 

Finally, you return the config and that is it!

Spread Syntax

Spread syntax (or the spread operator) is a feature in JavaScript that uses the dot notation (...) to spread elements from arrays, strings, or objects into various contexts. 

We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly – else this can cause adverse behavior and conflicts with Payload config and other plugins.

Let’s say you want to build a plugin that adds a new collection:

1
config.collections = [
2
...(config.collections || []),
3
newCollection,
4
// Add additional collections here
5
]

First, you need to spread the config.collections to ensure that we don’t lose the existing collections. Then you can add any additional collections, just as you would in a regular payload config.

This same logic is applied to other properties like admin, globals, hooks:

1
config.globals = [
2
...(config.globals || []),
3
// Add additional globals here
4
]
5
6
config.hooks = {
7
...(config.hooks || {}),
8
// Add additional hooks here
9
}

Some properties will be slightly different to extend, for instance the onInit property:

1
config.onInit = async payload => {
2
if (incomingConfig.onInit) await incomingConfig.onInit(payload)
3
// Add additional onInit code by using the onInitExtension function
4
onInitExtension(pluginOptions, payload)
5
}

If you wish to add to the onInit, you must include the async/await. We don’t use spread syntax in this case, instead you must await the existing onInit before running additional functionality.

In the template, we have stubbed out a basic onInitExtension file that you can use, if not needed feel free to delete it.

Webpack

If any of your files use server only packages such as fs, stripe, nodemailer, etc, they will need to be removed from the browser bundle. To do that you can alias the file imports with webpack.

When files are bundled for the browser the import paths are essentially crawled to determine what files to include in the bundle. To prevent the server only files from making it into the bundle, we can alias their import paths to a file that can be included in the browser. This will short-circuit the import path crawling and ensure browser only code is bundled.

Webpack is another part of the payload.config that can be a little more tricky to extend. To help here, the template includes a webpack.ts file which takes care of spreading the existing webpack, so you can just add your new stuff:

1
config.admin = {
2
...(config.admin || {}),
3
// Add your aliases to the helper function below
4
webpack: extendWebpackConfig(incomingConfig)
5
}
Types.ts

If your plugin has options, you should define and provide types for these options in a separate file which gets exported from the main index.ts.

1
export interface PluginTypes {
2
/**
3
* Enable or disable plugin
4
* @default false
5
*/
6
enabled?: boolean
7
}
8

If possible, include JSDoc comments to describe the options and their types. This allows a developer to see details about the options in their editor.

Wrap Up

The Payload plugin template provides a fully fitted plugin environment so you can simply focus on building out your feature and get to coding quicker. You can use the template by running npx create-payload-app@latest -t plugin -n my-new-plugin directly in your terminal or by cloning the template directly from GitHub.

For a full life-cycle plugin, it is essential that best practices are followed and testing is thoroughly considered. With the template, you will be able to build out the existing test suite with everything you need.

To learn more about plugins, checkout the plugin documentation. For more on working with the template and best practices, head over to our new plugin template docs.

If you have any questions or feedback, please feel free to reach out - happy building 👋