Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Design, implement, and optimize production-grade GraphQL APIs with a
schema-first approach. This skill equips an agent to build performant,
secure, and federated GraphQL services — not just lint a schema.
Quick Reference
Dimension
What to Check
Key Indicators
🔵 Schema Design
Naming, types, pagination, errors
Verb-first mutations, Relay pagination, union errors
🔴 BLOCKER — Must fix before production. SQL injection via resolvers,
auth bypass, unbounded recursion, schema that returns secrets.
🟠 MAJOR — Should fix. N+1 on hot path, missing depth limit, no error
handling on critical mutations, federation entity mismatch.
🟡 MINOR — Nice to fix. Deprecated field usage, inconsistent naming
convention, missing description strings.
⚪ NIT — Optional. Field ordering preference, comment style, type name
bikeshedding.
When to Use This Skill
Activate when the user asks you to:
"Design a GraphQL schema for..." / "Create a GraphQL API for..."
"Review this GraphQL schema" / "Check my resolvers for N+1 queries"
"Set up Apollo Federation" / "Convert my monolith to subgraphs"
"Add subscriptions to my GraphQL API" / "Implement real-time updates with GraphQL"
"Optimize my GraphQL performance" / "Add persisted queries"
"Harden my GraphQL API" / "Add depth limiting and rate limiting"
"Implement pagination in GraphQL" / "Add Relay-style cursor connections"
"Design error handling for GraphQL" / "Use union types for errors"
"Set up DataLoader" / "Fix N+1 queries in my resolvers"
Any request combining "GraphQL" + design, review, optimize, secure, or implement
Do NOT Activate For
Near-miss negatives — these mention GraphQL but are NOT design/implementation:
REST vs GraphQL comparison: "Should I use GraphQL or REST?" — technology
evaluation, not GraphQL development.
GraphQL client usage: "How do I use useQuery in Apollo Client?" — client-side
consumption, not API development.
Generic debugging: "My GraphQL query returns null but the database has data"
without schema/resolver context — debugging, not development.
Tooling questions: "Which GraphQL IDE should I use?" / "How to set up
GraphiQL?" — tool selection, not API design.
General Q&A about GraphQL concepts: "What is a resolver?" / "How does
introspection work?" — education, not implementation.
GraphQL migration without design: "Move my REST endpoint to GraphQL" without
schema design or resolver planning — migration planning, not API development.
GraphQL gateway/proxy setup without schema work: "Set up Apollo Router" with
no subgraph design — infrastructure, not API development.
When in doubt, ask: "Are you looking for schema design, resolver implementation,
or performance optimization for your GraphQL API?"
Common Pitfalls & Anti-Patterns
❌ GraphQL Anti-Patterns
N+1 Queries in Resolvers — The most common GraphQL performance killer.
Every resolver firing individual DB calls cascades into hundreds of queries.
Always batch with DataLoader, not per-field queries.
Over-fetching in Resolvers — Resolvers returning all columns when the
query only asks for id and name. Use field-aware database projections or
parent-to-child delegation.
Mutation Resolver as Business Logic Dump — Thick mutation resolvers with
validation, authorization, side effects, and notifications. Keep resolvers thin:
validate → authorize → delegate to service layer → return result.
String-Based Error Handling — Returning null or magic strings for errors.
Use typed error unions or the errors extensions payload so clients can
pattern-match instead of string-parse.
Monolithic Schema Before Federation — Building one massive schema and
then retrofitting federation. Design with federation from the start: define
entity boundaries, @key fields, and subgraph ownership.
No Depth or Complexity Limits — Unbounded recursive queries can bring
down a server. A single malicious query fetching user.posts.author.posts.author
recursively is a DoS vector. Always set graphql-depth-limit or query cost
analysis.
Authentication in Resolvers, Not Middleware — Checking context.user
inline in every resolver bloats code. Extract auth to a GraphQL context
function or a schema directive so resolvers receive an already-authenticated
(or rejected) context.
Subscription Leaks — AsyncIterators that never clean up lead to memory
pressure. Every subscription source must have a proper teardown in the
subscribe function's return { unsubscribe }.
Hardcoded Field Selections in Business Logic — Business code that
assumes specific GraphQL selections (if (info.fieldNodes...)). Use
attribute-based access patterns or GraphQL-aware ORMs instead.
Ignoring the extensions Field — The extensions field is the
GraphQL protocol's extensibility point. Use it for tracing (Apollo Tracing),
request IDs, deprecation warnings, and rate limit headers — don't invent
custom envelopes.
✅ GraphQL Quality Checklist
Before claiming implementation complete, verify:
Schema uses verb-first mutation naming (createUser, not UserCreate)
All list fields are paginated (Relay Connection or simplified offset)
Errors use typed unions, not loose strings
Every resolver with a DB call uses DataLoader
Depth limit and query cost analysis are configured
Mutations accept a single input type argument
Subscriptions have teardown/unsubscribe logic
Authentication happens in context, not individual resolvers
Federation entities have @key directives and reference resolvers
Persisted queries are enabled for production builds
All types and fields have description strings
Deprecated fields use @deprecated(reason: "...") with a migration path
Workflow
Phase 1: Schema Design
Design the schema first — the schema is the contract. Resolvers implement it,
not the other way around.
1.1 Naming Conventions
Construct
Convention
Example
Types
PascalCase, singular noun
User, Post, Payment
Query fields
camelCase, noun or noun phrase
user(id:), searchPosts
Mutations
camelCase, verb + object
createPost, cancelOrder
Input types
PascalCase, suffixed with Input
CreatePostInput, UserFilter
Enum values
UPPER_SNAKE_CASE
OrderStatus.PENDING, PAYMENT_FAILED
Payload types
PascalCase, suffixed with Payload
CreatePostPayload, LoginPayload
Union errors
PascalCase, suffixed with Error
ValidationError, NotFoundError
Critical Rule: Mutations MUST be verb-first. postCreate is wrong;
createPost is correct. This isn't style — it's a GraphQL spec expectation
that tooling (Apollo Studio, GraphiQL introspection) sorts on.
1.2 Pagination Patterns
Always paginate list fields. Never return a bare [User!]!.
Relay Cursor Connections (Preferred):
type Query {
users(
first: Int
after: String
last: Int
before: String
filter: UserFilter
): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
When to use Relay spec: public APIs, APIs consumed by multiple clients,
when you need stable cursor-based pagination, or when client uses Relay/Apollo
Client pagination helpers.
Simplified Offset Pagination (Internal APIs):
type Query {
users(limit: Int = 20, offset: Int = 0): UserPage!
}
type UserPage {
items: [User!]!
totalCount: Int!
hasMore: Boolean!
}
When to use offset: internal/admin APIs, when clients need to jump to
arbitrary pages, or when data set is small and stable.
Anti-pattern — Never:
# ❌ Unpaginated list — unbounded response, DoS risk
users: [User!]!
1.3 Error Handling Patterns
Don't overload null to mean "error". Structure your errors.
Typed Union Errors (Recommended):
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}
type CreatePostPayload {
post: Post
errors: [CreatePostError!]!
}
union CreatePostError = ValidationError | UnauthorizedError | RateLimitError
type ValidationError {
message: String!
field: String!
code: String!
}
type UnauthorizedError {
message: String!
}
type RateLimitError {
message: String!
retryAfterSeconds: Int!
}
Clients pattern-match on __typename:
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
post {
id
title
}
errors {
__typename
... on ValidationError { message field code }
... on RateLimitError { message retryAfterSeconds }
}
}
}
Top-Level Errors (for partial failures):
Use the standard GraphQL errors array for infrastructure errors (auth,
rate limit, internal server error). Use typed union errors for business
logic errors the client should handle.
Anti-patterns:
# ❌ Magic null — was it not found? forbidden? deleted?
user(id: "1"): User
# ❌ Stringly-typed error — client must parse strings
type CreatePostPayload {
post: Post
error: String # "VALIDATION_ERROR: title required"
}
1.4 Schema Documentation
Every type and field MUST have a description:
"""
A user account in the system. Users can create posts,
comment, and manage their profile.
"""
type User {
"""Unique identifier, stable across renames."""
id: ID!
"""Display name shown on posts and comments."""
name: String!
"""Set when the account was created. Immutable."""
createdAt: DateTime!
}
Descriptions feed into GraphiQL, Apollo Studio, and codegen tools.
Undocumented schemas are tech debt.
Phase 2: Resolver Architecture
2.1 Resolver Signature
Every resolver receives (parent, args, context, info):
const resolvers = {
Query: {
user: async (_parent, { id }, context, info) => {
// parent — result from parent resolver (null for root queries)
// args — GraphQL arguments ({ id: "42" })
// context — per-request shared state (auth, loaders, db)
// info — AST, field name, return type, path
return context.loaders.user.load(id);
},
},
};
2.2 DataLoader & N+1 Prevention
The N+1 problem: resolving posts.author for 10 posts fires 11 queries
(1 for posts + 10 individual author queries). DataLoader coalesces the
10 author loads into a single WHERE id IN (...) query.
Setup (per-request):
import DataLoader from "dataloader";
function createLoaders(db) {
return {
user: new DataLoader(async (ids: readonly string[]) => {
const users = await db.users.findByIds([...ids]);
// MUST return in same order as input ids
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) || null);
}),
postsByAuthor: new DataLoader(async (authorIds: readonly string[]) => {
const posts = await db.posts.findByAuthorIds([...authorIds]);
const grouped = new Map<string, Post[]>();
for (const post of posts) {
const list = grouped.get(post.authorId) || [];
list.push(post);
grouped.set(post.authorId, list);
}
return authorIds.map(id => grouped.get(id) || []);
}),
};
}
// In Apollo Server context:
const server = new ApolloServer({
schema,
context: async ({ req }) => ({
user: await authenticate(req),
loaders: createLoaders(db),
}),
});
Critical DataLoader Rules:
Create new DataLoader instances per request — Never reuse across
requests. Caching across requests causes stale data and security leaks.
Return arrays in the same order as input keys — DataLoader matches
by index. Wrong order = wrong data.
Batch function must accept and return arrays — Single-item batch
functions defeat the purpose.
Handle nulls for not-found — Return null (not throw) for
individual missing items so other items still resolve.
Use DataLoader instance in context, not imported globally.
GraphQL null-propagates: if a non-null field resolver throws, the error
bubbles up to the first nullable parent. Design your schema nullability
with this in mind:
type Query {
# ❌ If post.author.email throws, the entire query fails
post(id: ID!): Post!
# ✅ post.author.email can fail without killing the whole query
post(id: ID!): Post
}
For partial data, return what you can + errors in the extensions payload.
GraphQL can return both data and errors simultaneously.
Phase 3: Mutation Design
3.1 Input Types
Every mutation MUST accept a single input argument of a dedicated input type:
The resolver checks if the idempotencyKey has been seen:
async function processPayment(_, { input }, { db, paymentService }) {
const existing = await db.payments.findByKey(input.idempotencyKey);
if (existing) return { payment: existing, errors: [] };
// Process payment — if this fails and client retries with same key,
// the above check prevents double-charge
const payment = await paymentService.charge(input);
await db.payments.create({ ...payment, idempotencyKey: input.idempotencyKey });
return { payment, errors: [] };
}
3.3 Mutation Response Pattern
Always return a payload type, never the entity directly:
# ✅ Payload type — evolvable
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
type CreateUserPayload {
user: User
errors: [CreateUserError!]!
}
# ❌ Direct entity — no room for errors or metadata
type Mutation {
createUser(input: CreateUserInput!): User!
}
3.4 Thin Resolvers, Thick Services
Mutations are entry points. They should not contain business logic:
Use contracts (@tag, @inaccessible) to version public vs internal APIs.
Test _entities queries directly — they're the gateway's query API.
Phase 6: Security Hardening
6.1 Depth Limiting
Prevent recursive query DoS attacks:
npm install graphql-depth-limit
import depthLimit from "graphql-depth-limit";
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7)], // Max 7 levels of nesting
});
Automatic Persisted Queries (APQ) reduce bandwidth and improve cacheability:
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
// Client side:
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true,
}).concat(httpLink);
// Server side:
import responseCachePlugin from "apollo-server-plugin-response-cache";
const server = new ApolloServer({
schema,
plugins: [
responseCachePlugin({
sessionIdFromContext: (ctx) => ctx.user?.id || null,
// Private data varies by user; public data can be fully cached
}),
],
persistedQueries: {
cache: new PrefixingKeyValueCache(
new InMemoryLRUCache({ maxSize: 1000 }),
"apq:"
),
ttl: 900, // 15 minutes
},
});
CDN Integration: With useGETForHashedQueries: true, persisted queries are
sent as GET requests, making them cachable by standard CDNs and edge caches.
7.2 Field-Level Caching
Use @cacheControl directives:
type Query {
user(id: ID!): User @cacheControl(maxAge: 60)
topPosts: [Post!]! @cacheControl(maxAge: 300, scope: PUBLIC)
me: User @cacheControl(maxAge: 0, scope: PRIVATE)
}
type Post @cacheControl(maxAge: 600) {
id: ID!
title: String!
body: String! @cacheControl(maxAge: 3600)
viewCount: Int! @cacheControl(maxAge: 30)
}
7.3 Response Compression
import compression from "compression";
import express from "express";
const app = express();
app.use(compression()); // gzip/brotli for all responses
7.4 Batching & Defer/Stream
@defer (experimental) for incremental delivery:
query {
post(id: "42") {
title # Delivered immediately
author { name }
... on Post @defer {
body # Delivered in a later payload
comments {
body
author { name }
}
}
}
}
Enable with Apollo Server 4:
import { ApolloServer } from "@apollo/server";
import { buildSubgraphSchema } from "@apollo/subgraph";
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
// @defer support is built-in for federated schemas
});
7.5 Monitoring & Tracing
import { ApolloServerPluginInlineTrace } from "@apollo/server/plugin/inlineTrace";
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginInlineTrace({
includeErrors: { unmodified: true },
}),
],
});
Key metrics to track:
Resolver execution time by field
DataLoader batch sizes (are they actually batching?)
Query parse/validation time vs execution time
Error rate by operation
Subscription connection churn
Safety Rules
ABSOLUTE RULES — never violate these:
Never expose internal database IDs as the node identifier in Relay
patterns. Use opaque, globally unique IDs (base64-encoded TypeName:UUID).
Database IDs leak information about table sizes and insertion rate.
Never disable introspection in a way that breaks developer tooling
without providing an alternative. If you gate introspection, document how
authenticated developers can access the schema.
Never return raw database errors to clients. Always map to typed
GraphQL errors. Stack traces and SQL errors in production responses are
information leaks.
Never create circular references in federation @key chains.
A → B → A entity resolution will cause infinite loops in the gateway.
Never use @shareable without coordination across subgraph teams.
A @shareable field with conflicting resolvers across subgraphs creates
nondeterministic behavior.
Never deploy without depth/cost limits. An unprotected GraphQL
endpoint is a DoS vector. Minimum: depth limit = 7, maximum cost = 1000.
Never pass raw req.body or unvalidated variables to resolvers.
GraphQL argument coercion handles type validation, but business validation
must be explicit.
Never share DataLoader instances across requests. This causes data
leaking between users and stale cache hits.
Platform Compatibility Notes
Platform
Notes
Claude Code
Excellent for schema design iteration and resolver patterns. Use terminal access for npx graphql-codegen and schema composition.
Codex (OpenAI)
Strong at generating resolver implementations from schema definitions. Good at spotting N+1 patterns.
Cursor
Can read multiple schema/resolver files simultaneously. Ideal for cross-subgraph entity resolution validation.
Gemini CLI
Large context window aids full-schema review. Good for analyzing complex federated schemas end-to-end.
OpenClaw
Access to exec for running rover subgraph check and graphql-inspector. Use for CI/CD pipeline integration.
GitHub Copilot
Inline suggestions excel at resolver boilerplate and DataLoader patterns. Less effective at cross-file architectural review.
Windsurf
Multi-file workspace awareness helps with subgraph boundaries and entity cross-references.
OpenCode
Terminal-native: use for graphql-codegen setup, rover CLI operations, and schema composition in CI.
Platform-Specific Adjustments
If rover CLI is unavailable: manually validate Federation directives
and _service { sdl } output for each subgraph.
If graphql-codegen is unavailable: manually verify TypeScript types
against schema. Flag type mismatches as MAJOR findings.
If introspection is disabled: rely on schema SDL files. Verify that
SDL files are in version control and match deployed schemas.
For Discord/Slack delivery: use bullet lists, not tables. Split
schema reviews across multiple messages if >10 findings.
References
references/graphql-patterns.md — Comprehensive GraphQL patterns and anti-patterns reference