Files
payload/docs/plugins/build-your-own.mdx
Alessio Gravili 9f9db3ff81 chore: bump prettier, re-enable prettier for docs (#11695)
## Introducing Prettier for docs

Prettier [was originally disabled for our docs as it didn't support MDX
2.0](1fa636417f),
outputting invalid MDX syntax.

This has since been fixed - prettier now supports MDX 2.0.

## Reducing print width

This also reduces the print width for the docs folder from 100 to 70.
Our docs code field are very narrow - this should help make code more
readable.

**Before**
![CleanShot 2025-03-13 at 19 58
11@2x](https://github.com/user-attachments/assets/0ae9e27b-cddf-44e5-a978-c8e24e99a314)

**After**

![CleanShot 2025-03-13 at 19 59
19@2x](https://github.com/user-attachments/assets/0e424f99-002c-4adc-9b37-edaeef239b0d)



**Before**
![CleanShot 2025-03-13 at 20 00
05@2x](https://github.com/user-attachments/assets/614e51b3-aa0d-45e7-98f4-fcdb1a778bcf)

**After**

![CleanShot 2025-03-13 at 20 00
16@2x](https://github.com/user-attachments/assets/be46988a-2cba-43fc-a8cd-fd3c781da930)
2025-03-14 17:13:08 +00:00

299 lines
10 KiB
Plaintext

---
title: Building Your Own Plugin
label: Build Your Own
order: 20
desc: Starting to build your own plugin? Find everything you need and learn best practices with the Payload plugin template.
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly.
<Banner type="success">
To use the template, run `npx create-payload-app@latest --template plugin`
directly in your terminal.
</Banner>
Our plugin template includes 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
- Test suite with integrated GitHub workflow
By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use.
## Plugins Recap
Here is a brief recap of how to integrate plugins with Payload, to learn more head back to the [plugin overview page](https://payloadcms.com/docs/plugins/overview).
### How to install a plugin
To install any plugin, simply add it to your Payload Config in the plugins array.
```
import samplePlugin from 'sample-plugin';
const config = buildConfig({
plugins: [
// Add plugins here
samplePlugin({
enabled: true,
}),
],
});
export default config;
```
### Initialization
The initialization process goes in the following order:
1. Incoming config is validated
2. Plugins execute
3. Default options are integrated
4. Sanitization cleans and validates data
5. Final config gets initialized
## Plugin Template
In the [Payload Plugin Template](https://github.com/payloadcms/payload/tree/main/templates/plugin), you will see a common file structure that is used across plugins:
1. `/` root folder - general configuration
2. `/src` folder - everything related to the plugin
3. `/dev` folder - sanitized test project for development
### The root folder
In the root folder, you will see various files related to the configuration of the plugin. We set up our environment in a similar manner in Payload core and across other projects. The only two files you need to modify are:
- **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.
### The dev folder
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:
```
mkdir dev
cd dev
npx create-payload-app@latest
```
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.ts`.
```
plugins: [
// when you rename the plugin or add options, make sure to update it here
samplePlugin({
enabled: false,
})
]
```
You can add to the `dev/payload.config.ts` 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 `pnpm 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](https://jestjs.io/); 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.](https://jestjs.io/)
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 and you're all set!
```
let payload: Payload
describe('Plugin tests', () => {
// Example test to check for seeded data
it('seeds data accordingly', async () => {
const newCollectionQuery = await payload.find({
collection: 'newCollection',
sort: 'createdAt',
})
newCollection = newCollectionQuery.docs
expect(newCollectionQuery.totalDocs).toEqual(1)
})
})
```
## Seeding data
For development and testing, you will likely need some data to work with. You can streamline this process by seeding and dropping your database - instead of manually entering data.
In the plugin template, you can navigate to `dev/src/server.ts` and see an example seed function.
```
if (process.env.PAYLOAD_SEED === 'true') {
await seed(payload)
}
```
A sample seed function has been created for you at `dev/src/seed`, update this file with additional data as needed.
```
export const seed = async (payload: Payload): Promise<void> => {
payload.logger.info('Seeding data...')
await payload.create({
collection: 'new-collection',
data: {
title: 'Seeded title',
},
})
// Add additional seed data here
}
```
## Building a Plugin
Now that we have our environment setup and dev project ready to go - it's time to build the plugin!
```
import type { Config } from 'payload'
export const samplePlugin =
(pluginOptions: PluginTypes) =>
(incomingConfig: Config): Config => {
// create copy of incoming config
let config = { ...incomingConfig }
/**
* This is where you could modify the
* config based on the plugin options
*/
// If you wanted to add a new collection:
config.collections = [
...(config.collections || []),
newCollection,
]
// If you wanted to add a new global:
config.globals = [
...(config.globals || []),
newGlobal,
]
/**
* If you wanted to add a new field to a collection:
*
* 1. Loop over collections
* 2. Find the collection you want to add the field to
* 3. Add the field to the collection
*/
// If you wanted to add to the onInit:
config.onInit = async payload => {
if (incomingConfig.onInit) await incomingConfig.onInit(payload)
// Add additional onInit code here
}
// Finally, return the modified config
return config
}
```
To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend the [Payload Config](../configuration/overview) - and that is exactly what we are doing in this file.
### Spread syntax
[Spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/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:
```
config.collections = [
...(config.collections || []),
newCollection,
// Add additional collections here
]
```
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 array and object like properties such as admin, globals and hooks:
```
config.globals = [
...(config.globals || []),
// Add additional globals here
]
config.hooks = {
...(config.hooks || {}),
// Add additional hooks here
}
```
### Extending functions
Function properties cannot use spread syntax. The way to extend them is to execute the existing function if it exists and then run your additional functionality.
Here is an example extending the `onInit` property:
```
config.onInit = async payload => {
if (incomingConfig.onInit) await incomingConfig.onInit(payload)
// Add additional onInit code by using the onInitExtension function
onInitExtension(pluginOptions, payload)
}
```
## Types
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`.
```
export interface PluginTypes {
/**
* Enable or disable plugin
* @default false
*/
enabled?: boolean
}
```
If possible, include [JSDoc comments](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#types-1) to describe the options and their types. This allows a developer to see details about the options in their editor.
## Best practices
In addition to the setup covered above, here are other best practices to follow:
### Providing an enable / disable option
For a better user experience, provide a way to disable the plugin without uninstalling it.
### Include tests in your GitHub CI workflow
If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs)
### Publish your finished plugin to npm
The best way to share and allow others to use your plugin once it is complete is to publish an npm package. This process is straightforward and well documented, find out more about [creating and publishing a npm package here](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/).
### Add payload-plugin topic tag
Apply the tag **payload-plugin** to your GitHub repository. This will boost the visibility of your plugin and ensure it gets listed with [existing Payload plugins](https://github.com/topics/payload-plugin).
### Use Semantic Versioning (SemVer)
With the [Semantic Versioning](https://semver.org/) (SemVer) system you release version numbers that reflect the nature of changes (major, minor, patch). Ensure all major versions reference their Payload compatibility.