Overview

  • Writing plugins is an expressive and exciting part of Nexus
  • But please note that it is one of the least stable components
  • Not just in terms of lack of polish or unstable APIs but entire concepts and architecture
  • The bar to a finished stable plugin system is high not just because plugin systems are in general challenging, but also because the thing which it permits extending is itself not stable either: the framework components exposed at the application layer
  • If you are embarking on creating a plugin, please be aware you are in uncharted territory with no guarantees about API stability or even entire concepts/ways of extending Nexus
  • If you are ok with that, we welcome you aboard this journey and hope you share your feedback with the team on GitHub and creations with the community on slack!

How it looks

The Nexus CLI has a command to create new Nexus-plugin projects

$nexus create plugin

To write a plugin you create any of testtime runtime and worktime modules and import the respective plugin types to type your function. In each module export your plugin as plugin.

1// runtime.ts
2import { RuntimePlugin } from 'nexus/plugin'
3
4export const plugin: RuntimePlugin = () => project => {
5 /* ... */
6}
1// testtime.ts
2import { TesttimePlugin } from 'nexus/plugin'
3
4export const plugin: TesttimePlugin = () => project => {
5 /* ... */
6}
1// worktime.ts
2import { WorktimePlugin } from 'nexus/plugin'
3
4export const plugin: WorktimePlugin = () => project => {
5 /* ... */
6}

The project parameter gives you access to utils and core components

1export const plugin: TesttimePlugin = () => project => {
2 project.utils.log.trace('hello')
3}

With runtime plugins you can pass configuration to Nexus and contribute toward the graphql resolver context:

1export const plugin: RuntimePlugin = () => project => {
2 return {
3 context: {
4 create: req => {
5 return {
6 token: req.headers.authorization.match(/^Bearer (.+)$/)?[1] ?? null
7 }
8 },
9 typeGen: {
10 fields: {
11 token: 'null | string'
12 }
13 }
14 },
15 schema: {
16 // ...
17 }
18 }
19 }
20}

With worktime plugins you can hook onto various events grouped by sub-system:

1export const plugin: WorktimePlugin = () => project => {
2 // Not all hooks shown here
3 project.hooks.build.onStart = async () => { ... }
4 project.hooks.create.onAfterBaseSetup = async () => { ... }
5 project.hooks.generate.onStart = async () => { ... }
6 project.hooks.dev.onStart = async () => { ... }
7 project.hooks.dev.onFileWatcherEvent = async () => { ... }
8 project.hooks.dev.addToSettings = { ... }
9 project.hooks.db.init.onStart = async () => { ... }
10 project.hooks.db.migrate.apply = async () => { ... }
11 project.hooks.db.plan.onStart = async () => { ... }
12 project.hooks.db.rollback.onStart = async () => { ... }
13 project.hooks.db.ui.onStart = async () => { ... }
14}

Some worktime hooks give you contextual information to reflect upon:

1export const plugin: WorktimePlugin = () => project => {
2 project.hooks.db.plan.onStart = async ctx => {
3 project.log.info(ctx.migrationName)
4 }
5})

Finally, for your plugin to be consumed by Nexus, you need to export an entrypoint which references your runtime or worktime or testtime plugin.

This entrypoint is what needs to be imported by users.

We recommend you named export the entrypoint after the suffix nexus-plugin-(*) of your npm package name.

For instance, if your plugin is named nexus-plugin-fancy-plugin, your entrypoint should be named export fancyPlugin

1import { PluginEntrypoint } from 'nexus/plugin'
2
3export const fancyPlugin: PluginEntrypoint = () => ({
4 packageJsonPath: require.resolve('../package.json'),
5 runtime: {
6 module: require.resolve('./runtime'),
7 export: 'plugin',
8 },
9 worktime: {
10 module: require.resolve('./worktime'),
11 export: 'plugin',
12 },
13 testtime: {
14 module: require.resolve('./testtime'),
15 export: 'plugin',
16 },
17})

Adding settings to your plugin

  1. Create a settings.ts file and export a type Settings containing your settings

    1// settings.ts
    2export type Settings = {
    3 myCustomOption?: boolean
    4}
  2. Import the Settings type in your entrypoint, and pass it as generic to PluginEntrypoint.

    Note: You must return the settings untouched in your entrypoint. The framework is then responsible for passing the settings to your runtime/worktime/testtime plugins .

    1//entrypoint.ts
    2import { PluginEntrypoint } from 'nexus/plugin'
    3import { Settings } from './settings'
    4
    5export const fancyPlugin: PluginEntrypoint<Settings> = settings => ({
    6 settings,
    7 ...
    8})
  3. Repeat the operation for your runtime/worktime/testtime plugins.

    1// runtime.ts
    2import { RuntimePlugin } from 'nexus/plugin'
    3import { Settings } from './settings'
    4
    5export const plugin: RuntimePlugin<Settings> = settings => project => {
    6 // ...
    7}

By default, settings will always be optional.

We strongly recommend you keep it that way and find defaults for each of your settings. This is in order to keep the framework zero-config as much as possible. If you really need required settings though, you can enable them in the following way:

1// entrypoint.ts
2import { PluginEntrypoint } from 'nexus/plugin'
3import { Settings } from './settings'
4
5export const fancyPlugin: PluginEntrypoint<Settings, 'required'> = settings => ({
6 settings,
7 // ...
8})
9
10// runtime.ts
11import { RuntimePlugin } from 'nexus/plugin'
12import { Settings } from './settings'
13
14export const plugin: RuntimePlugin<Settings, 'required'> = settings => project => {
15 // ...
16}

Wholistic

  • The breadth of Nexus' plugin system is uncommon
  • Most tools are either runtime (Express) or workflow (ESLint) oriented and thus naturally scope their plugins to their focus
  • the advantage of Nexus' approach where plugins can hook into both workflow and runtime is that they allow plugin authors to deliver rich wholistic experiences for their users
  • For example a plugin author might reinforce their plugin's runtime feature with additions to doctor which lint for idiomatic usage
  • No longer does a plugin need rely on a lengthy readme that probably isn't complete and probably isn't read by most users to guide users through correct configuration, etc. usage of their plugin
  • Nexus is fanatic about giving as much latitude as possible to plugin authors to craft plugins that forward the principal of the pit of success to Nexus app developers

Runtime vs worktime

  • runtime is for hooking into when your app is actually running
  • so logic here can directly impact your production systems' reliability and performance characteristics
  • worktime is for hooking into things like dev testing and building
  • logic here is relatively free from concern over runtime impact, e.g. some slow running build-time extensions cannot impact latency experienced by end-users of the app in production.

Publishing for consumption

  • You must name your plugin package nexus-plugin-<your-plugin-name>
  • Nexus plugin cli will rely on this convention to search install etc. plugins (todo)
  • Nexus relies on this pattern to auto-use plugins in a user's project

A code reference

  • The most sophisticated real-world Nexus plugin is nexus-plugin-prisma.
  • It currently drives many of the requirements of the plugin system where we want nexus-prisma users to feel prisma is as seamless a component as any core one.
  • If you like learning by reading code, check it out here: graphql-nexus/plugin-prisma.
Edit this page on Github