Part 4: Utilities
Introduction
In the previous parts you built the foundation of your source plugin, from now on it’s about adding new functionalities, improving the user experience, and making the plugin better overall.
This part of the tutorial will focus on a subset of the Node API helpers. Feel free to revisit this document as it can also work as standalone instructions for those APIs.
By the end of this part of the tutorial, you will be able to:
- Use the
reporter
API to output structured terminal messages - Use the
cache
API to save artifacts between runs - Define custom errors for your plugin through
reporter.setErrorMap
- Use the
pluginOptionsSchema
API to verify your plugin’s options
reporter
API
You, as a source plugin author, can interact with your users in a variety of ways. You can share information in the README, chat in issues & PRs, create guides, etc. But no matter how active of a maintainer you are, it is your source plugin that will interact the most with your users through the terminal when they run gatsby develop
or gatsby build
. Gatsby outputs what it does behind the scenes with the goal of giving the users rich information that helps them be more productive with Gatsby itself.
For this, Gatsby internally uses the reporter
API and you can use it, too! When adding logs to the terminal be mindful of keeping of them concise, informative, and to a minimum. Most Gatsby plugins don’t even need to add logs (other than errors), so in doubt leave them out at the beginning and reevaluate once you have a bigger user base.
If you need information about an API, always be sure to check Gatsby’s API docs, in this case the reporter API docs. The reporter
API is available in all Node APIs and its methods can be used like this:
Here’s a short explanation on the most important methods:
info
: Print a message to the terminal.warn
: Print a warning message to the terminal.error
: Print an error message to the terminal.panic
: Print an error message to the terminal and immediately exit the process.panicOnBuild
: Print an error message to the terminal and immediately exit the process (only duringgatsby build
). Most often you should use this overpanic
as it’ll allow users to debug the error duringgatsby develop
.verbose
: Print a message to the terminal that is only visible when the “verbose” flag is enabled (e.g.gatsby build --verbose
).activityTimer
: Print an informational message to the terminal that has a timer attached to it (e.g. how long that step took).setErrorMap
: Set a custom error map to the reporter. This allows the reporter to extend Gatsby’s internal error map.
You’ll learn how to use panicOnBuild
, activityTimer
, and setErrorMap
in the next three tasks as they are the most relevant and common for plugins.
Task: Output errors
As source plugins interact with third-party APIs they are inherently prone to errors (network issues, API outages, etc.). As explained in Part 2 it’s good practice to gracefully handle all possible error states, as there’s nothing worse than no actionable feedback at all.
In Part 2 you also added the fetchGraphQL
function that accesses the GraphQL API inside the api
folder. When you send a malformed request to it, it can not only give back data
but also errors
. Use that information to output it with panicOnBuild
.
Go to your
plugin/src/source-nodes.ts
file. Destructurereporter
fromgatsbyApi
at the top ofsourceNodes
. Adderrors
to the destructuring statement for thefetchGraphQL
response. Also add a conditional statement whenerrors
is truthy directly belowfetchGraphQL
:Inside the
errors
conditional, usepanicOnBuild
to output the error message that you’re receiving from your API.error
,panicOnBuild
, andpanic
accept astring
,Error
or a structured error (you’ll learn about the latter in Task: Define custom errors). Also add an early return to not continue creating nodes.Pro tip: Try to rely on TypeScript types to check if the error you’re getting back is a valid
Error
type. If not, you might need to modify the error a little bit to fit the requirements.To see if it works, add a typo to your GraphQL request inside
fetchGraphQL
:Restart the
develop:site
script and you should see an error in the terminal:Yes, it works 🎉 Don’t forget to remove the typo again. In Task: Define custom errors you’ll learn how to further customize this error.
Pro tip: This is the error that the GraphQL API directly returns. You can see the same error if you go to
http://localhost:4000/graphql
(the GraphQL API fromapi
folder) and try the query there. Get yourself familiar with how, when, and in which form your API throws errors.
You’re now surfacing any errors that occur during the data fetching to your users. Use the error
, panicOnBuild
, and panic
methods throughout your code to display all relevant errors to your user.
Please note: If necessary, add context to your error messages instead of only passing the error along. This is especially true if the error could be thrown in multiple places or if the original error is not clear enough.
Task: Show activities and timings
Since your source plugin is retrieving information from a third-party API, that process could take a while, especially if it’s fetching a lot of data. In these instances you can use the activityTimer
method to let the user know that something is happening in the background. You shouldn’t add separate activityTimer
instances (remember: Keep the output concise and to a minimum) but instead use the setStatus
method on activityTimer
to update its text.
Now you’ll learn exactly that, by adding an activity for the sourcing and node creation process of your plugin.
Create a new
activityTimer
and store that in a variable (the name doesn’t matter). You’ll use the variable to start, update, and stop the timer. Try to use a text that clearly indicates your plugin name and what it’s doing.Since the
sourcingTimer
was initialized before thefetchGraphQL
call you can alsostart()
it before that call:With the
start()
call the timer is running until you’ll eventually callend()
.If the sourcing fails, the timer should be stopped. So update the
panicOnBuild
call to use thesourcingTimer
instead:Please note: When looking at the TypeScript types for
sourcingTimer
you’ll notice that it also implementspanicOnBuild
andpanic
. You can pass in the same arguments as you’re used to, theactivityTimer
will handle everything for you. If you had continued to usereporter.panicOnBuild
the terminal would output a successful message for the timer which is incorrect of course!Right now the
sourcingTimer
shows “Sourcing from plugin API” together with a time in seconds. You can use thesetStatus()
method to append text.Pro tip: Adding the information of how many types were created is a really nice debugging help since it’ll allow folks to quickly compare builds and see if e.g. all the information they expect is sourced.
Stop the timer once node creation is done:
Restart the
develop:site
script and you should read a new activity in the terminal:That’s what we’d expect, nice!
Task: Define custom errors
The error you’ve output to the terminal in Task: Output errors was functional but missing some crucial information. Where is the error coming from? Which part of the plugin? Any suggestions for immediate fixes?
You can use the setErrorMap
method to create an error map for your plugin. Gatsby internally uses an error map to benefit from two things (among other additional benefits):
- A user can google the error with its unique ID and hopefully only find resources that are directly related to that error.
- One can track down the location in the source code where an error is thrown quicker through the use of constants and unique IDs.
Both benefits also apply to you when you define a custom error map for your plugin. You’ll see the improvement at the end of this task. As a reminder, this is how an error (for the data fetching step) currently looks like:
Let’s get to work:
Open the
plugin/src/constants.ts
file and add anERROR_CODES
map with its first keyGraphQLSourcing
:The keys of the
ERROR_CODES
object can be arbitrary, we’d recommend using names that make it easy to differentiate them for their different purposes. The value of each entry is the unique ID. So make sure that each ID only exists once insideERROR_CODES
.You don’t need to worry about other plugins or Gatsby itself as the IDs are namespaced with your plugin. If possible, use IDs that are not already used by Gatsby itself.
Import
ERROR_CODES
intoplugin/src/on-plugin-init.ts
and initializesetErrorMap
. Remove the existinginfo
call:You can populate the error map like so:
- The object’s keys are the unique IDs
- The object should contain these three keys and values:
text
: Your text that will be shown to the user for this specific errorlevel
: Set this toERROR
category
: Set this toTHIRD_PARTY
Add the following to
plugin/src/on-plugin-init.ts
:text
has to be a function that returns astring
and receives a freeformcontext
. You can pass through any information you’d like incontext
and then use it to construct the final message that is shown to the user. So the error above requiressourceMessage
andgraphqlError
to work.Pro tip: If your
text
should be a multi-line string, consider using thestripIndent
function from common-tags. This way your error will be displayed correctly independent from how you write the string.In this step you’ll invoke the error with the custom ID and pass along the required
context
information. Openplugin/src/source-nodes.ts
and add theERROR_CODES
import and use it inside the existingpanicOnBuild
call:As in step 3 of Task: Output errors, add a typo to the GraphQL error again (e.g.
title2
), restart thedevelop:site
script and you should see a new output for the error:
Your error now has a custom ID (#plugin_10000
), it prints the name of the plugin (PLUGIN
), and you’ve added additional context to the error itself (Sourcing from the GraphQL API failed:
). Whenever you need to output an error message, create a new structured error inside setErrorMap
and use it to display the best possible error to the user.
cache
API
You might already know that Gatsby creates two folders during its build phase: .cache
and public
. It’s recommended to keep both folders around in between builds as then Gatsby can leverage its caching functionalities the best.
With the cache API you have access to a key-value store to persist data inside the .cache
folder. You can use it to persist timestamps, tokens, etc. to use delta updates, or to store the artifacts of time/memory/cpu intensive tasks (e.g. images, big files).
If the term “delta update” doesn’t ring a bell, here’s a short explanation in context of a source plugin: A delta update requires the source plugin to only fetch data from the remote API that is new, or has been changed from a previous state, in contrast to having to fetch all the data again.
Example usage: Your API endpoint accepts an optional query parameter of lastFetched
. If used, only updates since that date are returned. When storing the timestamp of last sourcing with the cache
API you then can use that date in your request, e.g. http://yourapi.com/endpoint?lastFetched=2023-01-1
.
Important: Your remote API has to support delta updates, it’s not a universal thing that works everywhere.
Task: Save the timestamp of last sourcing
The most common use case of the cache
API is to store timestamps, tokens, etc. and therefore you’ll learn exactly that. Even though the example API this tutorial uses doesn’t support delta updates, you’ll go through all the necessary steps in this task to apply it to your own source plugin.
The cache
API is a key-value store, so you save specific data under a key, and then can retrieve a value with a given key.
Store the name of the key in an constants to minimize the likelyhood of typos. Open
plugin/src/constants.ts
and add:Import the new
CACHE_KEYS
variable intoplugin/src/source-nodes.ts
and grab thecache
API fromgatsbyApi
insidesourceNodes
:Generate a date timestamp just before the
fetchGraphQL
call and usecache.set
to save the timestamp after the successful fetch. This way you’re ensuring accurate timestamps that are only saved when a successful data call happened. The key you use forcache.set
has to be unique.Date.now()
returns the number of milliseconds elapsed since the epoch. If you need another format, read the Date.now() documentation.It’s also important to know that
cache
is shared by all instances of your plugin. So if your user can add your plugin multiple times togatsby-config
(e.g. by setting a differentworkspaceId
in the plugin options), you need to ensure that thosecache.set
calls use unique keys. In those cases, use a unique instance identifier, for example said plugin options.The next step is to use
cache.get
to retrieve the timestamp from the cache before thefetchGraphQL
call. Since you won’t use it for delta fetching in this tutorial, output the information in theverbose
mode of Gatsby withreporter.verbose
:Time to test your changes! Restart the
develop:site
script but this time with the--verbose
flag enabled:The output got a lot busier! You should see a new log:
You’re seeing this because on this first run the
lastFetchedDate
didn’t exist yet and thusundefined
is returned from thecache.get
call. Stop the development server and restart the script. You now should see a timestamp defined that looks something like this:Great, it’s working!
Pro tip: If you save files with the
cache
API you can also inspect what is saved by going to the.cache/caches
folder and looking for a folder named like your plugin. For this tutorial the path would besite/.cache/caches/plugin
.
pluginOptionsSchema
API
In most cases a Gatsby source plugin needs one or more options as it interacts with third-party APIs that require customization and authentication (e.g. different endpoints or API keys). You don’t want other users to use your credentials, so you’ll need a way for your users to provide theirs. That’s where plugin options come in!
The Configuring Plugin Usage with Plugin Options guide goes into depth about plugin options and the pluginOptionsSchema
API so if you want to know more beyond this guide, be sure to give it a read. But to give a short summary:
A Gatsby plugin with options included makes those options available in the second argument of Gatsby Node, Browser, and SSR APIs.
A sample
gatsby-config
file:Usage of that option in a Node API:
A plugin can optionally define a schema (through the
pluginOptionsSchema
API) to enforce a type for each option. Gatsby will validate that the options users pass match the schema to help them correctly set up their site.
Here are some tips when working with plugin options:
- Consider the options a public API contract between your users and your plugin. Follow SemVer versioning when adding, modifying, and removing options. For example, if you want to remove an option you should only do this in a major release of your plugin since it’s a breaking change.
- Add as few plugin options as necessary. In the beginning, use safe defaults where possible (to the best of your knowledge) and when users wish to have new options available, seriously contemplate if you should add them. Once added, you’ll need to maintain the option forever (or until you remove it again).
- Use clear names. Naming things is hard, but take extra care in using clear names for your plugin’s options. Best case scenario is that an option is clear enough without any additional description.
- Use “enable” options, not negated options. An example: Instead of using
noWarnings: true
, usewarnings: false
. The double negation that would occur withnoWarnings: false
is really confusing. - Pass options through. If your source plugin is using a library under the hood (e.g. an SDK from a CMS) that has options on its own, allow people to configure all those options, too. You could collect those under a new key in the
options
object (e.g.options.sdkOptions
) and then refer people to the SDK’s documentation.
Task: Use endpoint
option
Instead of hardcoding the URL to the GraphQL API, you’ll use an endpoint
option and thus make the URL configurable.
The boilerplate you cloned at the beginning already has an
endpoint
option defined in the site’sgatsby-config
. Go tosite/gatsby-config.ts
to inspect the code:So the plugin already receives the GraphQL endpoint through the
endpoint
option.The
IPluginOptionsKeys
TypeScript type insideplugin/src/types.ts
currently has a generic[key: string]: any
catch-all. Replace it with yourendpoint
option so that both internally and externally the types are correct:Open the file
plugin/src/source-nodes.ts
and validate that you can access the option. You can do this by accessing the second parameter of thesourceNodes
function andconsole.log
it. Use theIPluginOptionsInternal
type for it:Restart the
develop:site
script and you should see an output in the terminal like this:Don’t worry about the
plugins
key there. Since Gatsby’s plugins can also have sub-plugins, the keyplugins
is added by default to thepluginOptions
object. What you really should care about is that theendpoint
option successfully comes through.Destructure
endpoint
from thepluginOptions
object and use it in thefetchGraphQL
call:Once again, restart the
develop:site
script and you should see the logSourcing from plugin API - 0.033s - Processing 3 posts and 2 authors
to indicate that the sourcing was successful.
You’ve successfully used a plugin option in your source plugin’s source code.
Task: Verify endpoint
option
Right now a user could use any serializable value for endpoint
like numbers, objects, strings, or booleans. But those are not valid options for endpoint
, it should be a string, more specifically a valid URI. Use the pluginOptionsSchema
API to validate exactly that.
Create a new file called
plugin-options-schema.ts
inside the plugin’ssrc
folder with the following contents:pluginOptionsSchema
uses Joi to define a validation schema. Use its API documentation to discover all available options.Define
endpoint
in the schema. It should be a string (a valid URI), should be required, and have a short description. You can write something like this:Export the
pluginOptionsSchema
API in the plugin’sgatsby-node
file so that it is run:You can test that it works correctly by opening up the site’s
gatsby-config
and changing the option to an invalid value, e.g. entering a number:Restart the
develop:site
script. The development server should crash and show an error:It’s working correctly, great! Go back to
site/gatsby-config.ts
and use the correctendpoint
option again.
Joi as a validation library is really powerful and as shown in Configuring Plugin Usage with Plugin Options you can define a really fine-grained validation schema.
Summary
Awesome! You’ve learned a lot about plugin options.
Take a moment to think back on what you’ve learned so far. Challenge yourself to answer the following questions from memory:
- How can you use the
reporter
API and what methods does it have? - What is the difference between the methods
panicOnBuild
andpanic
? - How can you show an activity for e.g. the time it takes to source data?
- What are the advantages of custom errors defined through
setErrorMap
? - Which type of storage is the
cache
API and for which purposes is it useful? - How can you access plugin options?
- How can you validate that the user input for plugin options is correct?
Key takeaways
- Use the
reporter
API and its methods to output information (warnings, errors, logs, timers, etc.) to the terminal duringgatsby develop
andgatsby build
. You can also stop the entire Gatsby process on errors if necessary. - By default, a generic error handler is used. You can use the
setErrorMap
API to define your own, custom errors to show richer information to your users. - The
cache
API gives you access to a key-value store to save data between runs. - Use the
pluginOptionsSchema
API to validate the input of your plugin options.
Share Your Feedback!
Our goal is for this tutorial to be helpful and easy to follow. We’d love to hear your feedback about what you liked or didn’t like about this part of the tutorial.
Use the “Was this doc helpful to you?” form at the bottom of this page to let us know what worked well and what we can improve.
What’s coming next?
In Part 5 you’ll learn all about Incremental Builds and how to best build your API and plugin for it.
Continue to Part 5