dist/plugins/api-graphql-mercurius/skills/api-graphql-mercurius/SKILL.md
GraphQL server for Fastify with Mercurius — loaders, subscriptions, federation, JIT compilation
npx skillsauth add agents-inc/skills api-graphql-mercuriusInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Mercurius as a Fastify plugin for GraphQL APIs with built-in loader batching (solves N+1), JIT query compilation, subscriptions via WebSocket, and federation support. Register with
app.register(mercurius, { schema, resolvers, loaders }). Loaders are Mercurius's killer feature: define them per-type to batch field resolution automatically. Usejit: 1to enable query compilation for production performance.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST define loaders for any field that fetches related data — loaders solve the N+1 problem automatically through batching)
(You MUST use named constants for all numeric values — JIT thresholds, query depth limits, port numbers)
(You MUST return an array from loaders matching the exact length and order of the queries parameter)
(You MUST use fastify.graphql.pubsub.publish() inside mutations to trigger subscriptions — not external pubsub directly)
</critical_requirements>
Auto-detection: Mercurius, mercurius, app.graphql, fastify.graphql, mercurius loaders, mercurius subscription, pubsub.publish, pubsub.subscribe, @mercuriusjs/federation, @mercuriusjs/gateway, mercurius-codegen, MercuriusContext, graphql-jit, withFilter, preParsing, preValidation, preExecution, onResolution
When to use:
@mercuriusjs/federationWhen NOT to use:
Key patterns covered:
withFilterMercuriusContext augmentationDetailed Resources:
Fastify-native GraphQL. Mercurius is not a standalone server bolted onto Fastify — it is a Fastify plugin that deeply integrates with Fastify's lifecycle, encapsulation model, and plugin system. This means your GraphQL API inherits Fastify's performance characteristics and plugin architecture naturally.
Loaders over DataLoader. Instead of requiring a separate DataLoader library, Mercurius provides a built-in loader system. Loaders are defined per-type/per-field and receive batched queries automatically. This is simpler than manually instantiating DataLoader instances per-request and is the primary mechanism for solving the N+1 problem.
JIT for production. Mercurius uses graphql-jit to compile frequently-executed queries into optimized V8 functions. After a configurable threshold of executions, subsequent runs of the same query bypass the GraphQL execution engine entirely — delivering significant performance gains for repeated queries.
Federation as a plugin. Federation support is split into separate packages (@mercuriusjs/federation for services, @mercuriusjs/gateway for composition), keeping the core library lean for non-federated use cases.
Register Mercurius as a Fastify plugin with schema (SDL string), resolvers, and optional loaders.
import Fastify from "fastify";
import mercurius from "mercurius";
const JIT_THRESHOLD = 1;
const SERVER_PORT = 3000;
const app = Fastify({ logger: true });
const schema = `
type Query {
user(id: ID!): User
users: [User!]!
}
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
}
`;
app.register(mercurius, {
schema,
resolvers,
loaders,
jit: JIT_THRESHOLD,
graphiql: process.env.NODE_ENV !== "production",
});
Why good: JIT threshold as named constant, GraphiQL disabled in production, loaders passed at registration level alongside resolvers
Full registration with context, error handling, and all options: examples/core.md
Loaders are Mercurius's primary mechanism for batch data fetching. Define them per-type per-field. Each loader receives an array of queries (batched requests) and must return an array of results in the same order.
const loaders = {
User: {
async posts(queries: Array<{ obj: User; params: Record<string, unknown> }>, context: MercuriusContext) {
const userIds = queries.map(({ obj }) => obj.id);
const allPosts = await fetchPostsByUserIds(userIds);
// Return array matching queries order
return queries.map(({ obj }) =>
allPosts.filter((post) => post.authorId === obj.id),
);
},
},
};
Why good: Single bulk query replaces N individual queries, result array matches input order (required), batching is automatic per-request
Gotcha: The returned array MUST match the length and order of queries — Mercurius maps results by index, not by key.
Full loader patterns with caching options: examples/core.md
Resolvers follow the standard GraphQL signature: (parent, args, context, info). The context includes the Fastify reply object for accessing Fastify decorators.
const resolvers = {
Query: {
user: async (_parent: unknown, args: { id: string }, context: MercuriusContext) => {
return context.reply.server.db.findUser(args.id);
},
users: async (_parent: unknown, _args: unknown, context: MercuriusContext) => {
return context.reply.server.db.listUsers();
},
},
};
Why good: Accesses Fastify decorators via context.reply.server, standard GraphQL resolver signature
Complete resolver examples with mutations: examples/core.md
Augment the MercuriusContext interface to get type-safe context in resolvers and loaders.
import type { FastifyRequest, FastifyReply } from "fastify";
const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => {
return {
userId: req.headers["x-user-id"] as string | undefined,
};
};
type PromiseType<T> = T extends PromiseLike<infer U> ? U : T;
declare module "mercurius" {
interface MercuriusContext
extends PromiseType<ReturnType<typeof buildContext>> {}
}
// Registration
app.register(mercurius, {
schema,
resolvers,
context: buildContext,
});
Why good: Context type is derived from the builder function, no manual interface duplication, resolvers get full type inference on ctx.userId
Full TypeScript patterns with codegen: examples/core.md
Enable subscriptions for real-time data. Mercurius provides a built-in pubsub system accessible via context.
const NOTIFICATION_TOPIC = "NOTIFICATION_ADDED";
const resolvers = {
Mutation: {
addNotification: async (_parent: unknown, args: { message: string }, context: MercuriusContext) => {
const notification = { id: generateId(), message: args.message };
await context.pubsub.publish({
topic: NOTIFICATION_TOPIC,
payload: { notificationAdded: notification },
});
return notification;
},
},
Subscription: {
notificationAdded: {
subscribe: async (_parent: unknown, _args: unknown, context: MercuriusContext) => {
return context.pubsub.subscribe(NOTIFICATION_TOPIC);
},
},
},
};
Why good: Topic as named constant, pubsub accessed from context (Mercurius-managed), subscribe returns async iterator
Full subscription patterns with withFilter and Redis: examples/subscriptions.md
Build federated services with @mercuriusjs/federation. Define __resolveReference as a loader for batch entity resolution.
import mercuriusFederation from "@mercuriusjs/federation";
const schema = `
extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String!
}
`;
const loaders = {
User: {
async __resolveReference(queries: Array<{ obj: { id: string } }>) {
const ids = queries.map(({ obj }) => obj.id);
const users = await fetchUsersByIds(ids);
return queries.map(({ obj }) => users.find((u) => u.id === obj.id));
},
},
};
app.register(mercuriusFederation, { schema, resolvers, loaders });
Why good: __resolveReference as loader prevents N+1 on entity resolution (strongly recommended by Mercurius docs), batch fetches all referenced entities at once
Full federation with gateway: examples/federation.md
Mercurius provides hooks for cross-cutting concerns at specific points in the GraphQL execution lifecycle.
Hook execution order:
preParsing - Before query string parsing (tracing, query preprocessing)preValidation - After parsing, before validation (skipped for cached queries)preExecution - Before execution (auth, rate limiting, query modification)onResolution - After execution completes (metrics, response logging)app.graphql.addHook("preExecution", async (schema, document, context) => {
const startTime = performance.now();
context.startTime = startTime;
});
app.graphql.addHook("onResolution", async (execution, context) => {
const duration = performance.now() - context.startTime;
context.reply.server.log.info({ duration }, "GraphQL query executed");
});
Why good: Hooks integrate with Fastify's logging, run at precise lifecycle points, can modify schema/document/variables in preExecution
Warning: Modifying schema or document in preExecution disables JIT compilation for that query.
Full hook patterns: reference.md
Enable JIT to compile frequently-executed queries into optimized V8 functions.
const JIT_THRESHOLD = 1;
const MAX_QUERY_DEPTH = 10;
app.register(mercurius, {
schema,
resolvers,
jit: JIT_THRESHOLD,
queryDepth: MAX_QUERY_DEPTH,
});
Why good: jit: 1 compiles after first execution (suitable for production with repeated queries), queryDepth prevents abuse, both values as named constants
Gotcha: JIT is disabled (default 0) out of the box. Set jit: 1 for production. Setting it higher (e.g., 5) delays compilation until the query has been seen N times, which helps avoid compiling one-off queries.
<red_flags>
User.posts, Post.author) should use a loader, not inline resolver queries. Without loaders, you get the classic N+1 problem.queries array by index. Returning fewer/more items or in wrong order corrupts the response silently.__resolveReference as resolver instead of loader in federation — Causes N+1 on entity resolution across services. The docs strongly recommend defining it as a loader.jit: 0 means no JIT compilation. Set jit: 1 for production workloads with repeated queries.context function for per-request data — Accessing request headers or auth tokens requires a context builder function, not Fastify decorators alonegraphiql: false or conditionally disable based on NODE_ENVqueryDepth limit — Without depth limiting, deeply nested queries can exhaust server resourcespreExecution — Disables JIT compilation for that query executionsubscription: true in registration — Subscriptions are disabled by default; subscription resolvers silently fail without this optionpayload in pubsub.publish() must match the subscription field name exactly (e.g., { notificationAdded: data } for a notificationAdded subscription)addHook before app.ready() — GraphQL hooks must be registered after app.ready() or inside a Fastify plugin that ensures readinessDetailed anti-pattern code examples: reference.md
opts: { cache: false } when data changes mid-requestpreValidation is skipped for cached queries — If a query is parsed from cache, validation hooks do not firesubscription.context option for custom subscription contextconnection_init payload goes into request.headers — During WebSocket handshake, properties from the client's connection_init payload are copied into request headers automaticallyschema, resolvers, or loaders on the gateway instancequeryDepth must be at least 7 for GraphiQL — GraphiQL's introspection query requires depth 7+; lower values break the IDE</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST define loaders for any field that fetches related data — loaders solve the N+1 problem automatically through batching)
(You MUST use named constants for all numeric values — JIT thresholds, query depth limits, port numbers)
(You MUST return an array from loaders matching the exact length and order of the queries parameter)
(You MUST use fastify.graphql.pubsub.publish() inside mutations to trigger subscriptions — not external pubsub directly)
Failure to follow these rules will cause N+1 performance problems, corrupted GraphQL responses, and broken subscriptions.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events