From graphql-consistency
Write, review, refactor, or debug GraphQL server code with graphql-js / the graphql npm package (buildSchema, makeExecutableSchema, resolvers, execution, GraphQLError, schema design) using one canonical idiom set. Use this skill whenever code defines GraphQL schemas or resolvers in Node, executes queries programmatically, or when the user hits resolvers silently returning null, "Cannot use GraphQLSchema from another module or realm", a whole query nulling out from one failed field, resolver N+1 query storms, string-built queries, or asks SDL-first vs code-first or buildSchema vs makeExecutableSchema. Trigger it even when the user just says "add a GraphQL API" in Node — without saying the words "graphql-js."
How this skill is triggered — by the user, by Claude, or both
Slash command
/graphql-consistency:graphql-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
graphql-js is stable at v16, but generated code mixes construction styles mid-project,
graphql-js is stable at v16, but generated code mixes construction styles mid-project,
wires resolvers to buildSchema (where they silently don't run as expected), interpolates
user input into query strings, and is perpetually surprised by null bubbling and the
dual-package "two graphql copies" error. This skill pins one canonical idiom set.
| Always | Never | Why |
|---|---|---|
pick ONE construction path: SDL-first via makeExecutableSchema({ typeDefs, resolvers }) (house default) or code-first GraphQLObjectType — per project | mixing both, or buildSchema + a resolver map | buildSchema ignores a resolver map's nested resolvers — it only consults rootValue for top-level fields, with default resolvers elsewhere; half your resolvers silently never run. |
variables: graphql({ schema, source, variableValues }) with $vars in the document | template-literal interpolation into query strings | Interpolation is injection plus a new document per value (kills caching/persisted queries). |
the named-args execution signature: graphql({ schema, source, rootValue, contextValue, variableValues }) | positional graphql(schema, source, root, ctx, vars) | The object form is the modern v16 signature; positional is legacy-era reading. |
per-request contextValue carrying loaders/auth/db | module-level mutable state in resolver files | Shared state leaks across requests and users; context is the designed channel. |
| one DataLoader instance per request (built in context) | fetching by id inside list-item resolvers raw | The N+1: a 100-item list issues 100 user lookups; per-request loaders batch and dedupe them. |
throw new GraphQLError("msg", { extensions: { code: "FORBIDDEN" } }) | throwing bare strings / leaking internal Error messages | extensions.code is the machine-readable contract; raw errors leak internals into the response. |
design nullability: ! on fields whose absence is impossible; nullable where failure is isolated | non-null everything by reflex | An error in a non-null field bubbles: the nearest nullable ancestor becomes null — one bad field can null the entire query. |
check both errors and data on results | treating presence of data as success | GraphQL returns partial data + errors routinely; ignoring errors hides failures. |
a single graphql package instance (dedupe; npm ls graphql) | multiple/duplicated graphql copies in node_modules | instanceof checks fail across copies → "Cannot use GraphQLSchema from another module or realm." |
schema description strings ("""docs""") and introspection-driven tooling | undocumented schemas | The SDL is the API contract; descriptions flow to every client tool. |
House style:
import { makeExecutableSchema } from "@graphql-tools/schema";
import { graphql, GraphQLError } from "graphql";
import DataLoader from "dataloader";
const typeDefs = /* GraphQL */ `
type Post { id: ID!, title: String!, author: User }
type User { id: ID!, name: String! }
type Query { posts(limit: Int = 20): [Post!]! }
`;
const resolvers = {
Query: {
posts: (_p, { limit }, ctx) => ctx.db.posts.list({ limit }),
},
Post: {
author: (post, _a, ctx) => ctx.loaders.user.load(post.authorId), // batched
},
};
export const schema = makeExecutableSchema({ typeDefs, resolvers });
export const buildContext = (req) => ({
db,
user: authenticate(req),
loaders: { user: new DataLoader((ids) => db.users.byIds(ids)) }, // fresh per request
});
const result = await graphql({
schema,
source: "query ($limit: Int) { posts(limit: $limit) { title author { name } } }",
variableValues: { limit: 10 },
contextValue: buildContext(req),
});
if (result.errors) handle(result.errors); // data may STILL be partially present
String!, the parent object becomes null; if that's
inside [Post!]!, the list dies — cascading to the nearest nullable ancestor or
data: null. Diagnose "everything is null" by reading errors[].path first.null with no error.errors with no
data; resolver throws → partial data + errors. Clients need both branches.extensions.code conventions (UNAUTHENTICATED, FORBIDDEN, BAD_USER_INPUT,
NOT_FOUND) are what clients switch on; free-text messages are for humans. Mask
unexpected errors (catch, log, rethrow a generic GraphQLError) — stack traces in
responses are a leak.type can't be used as an
argument — define input CreatePostInput; sharing shapes requires duplication by
design.graphql (or ESM+CJS both
loaded) breaks instanceof-based checks with the "another module or realm" error —
fix with dedupe/resolutions/peerDependencies, never by catching it.Target graphql-js 16.x (the long-stable line; 17 modernizes packaging/ESM).
Positional graphql()/execute() arguments and execute(schema, doc, ...) shapes are
legacy — the options-object signatures are current. graphql-tools' makeExecutableSchema
lives at @graphql-tools/schema. Server frameworks (Apollo, Yoga, Helix) wrap this
same execution layer — these idioms apply beneath all of them; HTTP/subscription
transport is the framework's concern, not this skill's.
makeExecutableSchema; resolver map mirrors the SDL exactly (lint with
tooling that diffs the two).errors[].path lands where designed.! on every field, data-only result handling,
per-item fetches in list field resolvers.For the construction-path comparison, null-bubbling worked examples, DataLoader
patterns, error taxonomy, and custom scalar implementation, read
references/graphql-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/graphql-consistency --plugin graphql-consistency