From mateonunez-skills
Defines standard Fastify plugin shape: fastify-plugin wrapper, idempotency guard, decorator pattern, withX() TS narrowing helper, module augmentation. For writing, reviewing plugins or adding decorators.
npx claudepluginhub mateonunez/skillsThis skill uses the workspace's default tool permissions.
> If it doesn't hold up in production, it doesn't make the cut.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
If it doesn't hold up in production, it doesn't make the cut.
I publish Fastify plugins (fastify-orama, fastify-at-mysql, lyra-impact). The shape below is what every one of mine looks like. It's small, it's deliberate, and it doesn't drift between repos.
You are about to:
fastify.decorate(...) callfastify-plugin (fp) wrapperdeclare module 'fastify' {})'use strict'
const fp = require('fastify-plugin')
async function fastifyMyPlugin (fastify, options) {
if (fastify.myPlugin) {
throw new Error('fastify-my-plugin is already registered')
}
const { /* my-plugin options */ ...rest } = options
// ... build api, open connections, restore state ...
function withMyPlugin () {
return this
}
fastify.decorate('myPlugin', api)
fastify.decorate('withMyPlugin', withMyPlugin)
}
module.exports = fp(fastifyMyPlugin, {
fastify: '5.x',
name: 'fastify-my-plugin'
})
// Re-export raw function + helpers as named exports
module.exports.fastifyMyPlugin = fastifyMyPlugin
That's the spine. Eight non-negotiables:
'use strict' at the top, CommonJS, JS source — TS lives in index.d.ts.fastify-plugin wrapper. Without it, decorators are encapsulated and your plugin "doesn't work" outside its register scope. fp is the de-encapsulation primitive.async (fastify, options) signature. Even if you don't await — keep it async for symmetry.if (fastify.myPlugin) throw. Stops the foot-gun where two registers silently clobber each other.withMyPlugin() helper that returns this. No-op at runtime; pure TypeScript narrowing tool. Lets users write fastify.withMyPlugin().myPlugin.foo() in handlers where the decorator's type isn't yet visible.module.exports = fp(plugin, { fastify, name }) — pin the Fastify major ('5.x') and the plugin name. The name is what shows up in fp warnings.Ship types in index.d.ts. Augment Fastify, don't subclass it:
import 'fastify'
declare module 'fastify' {
interface FastifyInstance {
myPlugin: MyPluginApi
withMyPlugin: () => FastifyInstance & { myPlugin: MyPluginApi }
}
}
export interface MyPluginOptions { /* ... */ }
export interface MyPluginApi { /* ... */ }
declare const fastifyMyPlugin: FastifyPluginAsync<MyPluginOptions>
export default fastifyMyPlugin
Module augmentation is the only correct way — it composes with other plugins, it's discoverable, it works with Fastify's type inference.
Validate at the entry point, fail fast:
if (!options.schema && !options.persistence) {
throw new Error('You must provide a schema or a persistence adapter')
}
Don't reach for ajv for plugin options — overkill. Plain checks at the top of the function are enough; the schema for request/response is what ajv is for.
fastify.orama, fastify.mysql, fastify.redis. Not fastify.oramaClient, not fastify.dbConnection.with<Name>() helper for the type-narrowing trick. Always returns this.fastify.search() is wrong — that lives on the API object: fastify.orama.search().fastify-plugin. Decorators get encapsulated and disappear in handlers. The bug is silent and infuriating to debug.fastify.decorate('orama', () => api) — passing a function instead of the value. Now every callsite invokes it (fastify.orama().search). Pass the API object directly.fastify.decorate('orama', oramaDb) and let consumers call Orama.search(fastify.orama, …). Wrap the methods so the db instance is implicit: fastify.orama.search(...).register calls silently overwrite the decorator and you spend an afternoon hunting it down.^5.0.0 instead of '5.x'. The fp peer constraint is a major-version range, not a npm range.withX() helper. Users have to cast fastify as FastifyInstance & { myPlugin: ... } everywhere. Annoying. The helper costs three lines.Use fastify.addHook('onClose', async () => { /* close db, drain queue, persist state */ }). Don't expose a manual close() decorator — the framework already has the lifecycle hook.
deprecated/fastify-orama/references/core-plugin-api.md