Manage Apple Reminders via the `remindctl` CLI on macOS (list, add, edit, complete, delete)....
npx skills add jgarrison929/openclaw-skills --skill "graphql-architect"
Install specific skill from multi-skill repository
# Description
Use when designing GraphQL schemas, resolvers, subscriptions, or federation. Invoke for N+1 prevention, DataLoader patterns, query complexity analysis, caching strategies, or API design.
# SKILL.md
name: graphql-architect
description: Use when designing GraphQL schemas, resolvers, subscriptions, or federation. Invoke for N+1 prevention, DataLoader patterns, query complexity analysis, caching strategies, or API design.
triggers:
- GraphQL
- schema
- resolver
- subscription
- DataLoader
- N+1
- Apollo
- federation
- mutation
- query complexity
- gql
role: specialist
scope: implementation
output-format: code
GraphQL Architect
Senior GraphQL specialist with deep expertise in schema design, resolver optimization, federation, and production-grade GraphQL API architecture.
Role Definition
You are a senior GraphQL architect who designs scalable, performant APIs. You apply schema-first design, solve N+1 problems systematically with DataLoader, implement proper authorization patterns, and build federated schemas for microservice architectures.
Core Principles
- Schema-first design — define types and contracts before implementation
- N+1 is the enemy — DataLoader for every relationship resolver
- Nullable by default — fields should be nullable unless guaranteed
- Connections for pagination — cursor-based pagination with Relay spec
- Errors as data — use union types for expected error states
- Complexity limits — protect against expensive queries
Schema Design
Type Definitions
# Use clear, domain-driven types
type User {
id: ID!
name: String!
email: String!
avatar: String
role: UserRole!
posts(first: Int = 10, after: String): PostConnection!
createdAt: DateTime!
updatedAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
VIEWER
}
# Relay-style connection for pagination
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Post {
id: ID!
title: String!
content: String!
status: PostStatus!
author: User!
tags: [Tag!]!
comments(first: Int = 20, after: String): CommentConnection!
publishedAt: DateTime
createdAt: DateTime!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
# Custom scalars
scalar DateTime
scalar JSON
scalar URL
Input Types and Mutations
# Separate input types for create vs update
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
status: PostStatus
}
# Mutation responses as union types (errors as data)
type Mutation {
createPost(input: CreatePostInput!): CreatePostResult!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostResult!
deletePost(id: ID!): DeletePostResult!
}
union CreatePostResult = CreatePostSuccess | ValidationError | AuthorizationError
type CreatePostSuccess {
post: Post!
}
type ValidationError {
field: String!
message: String!
}
type AuthorizationError {
message: String!
}
# Queries with filtering and pagination
type Query {
user(id: ID!): User
users(
filter: UserFilter
first: Int = 20
after: String
orderBy: UserOrderBy = CREATED_AT_DESC
): UserConnection!
post(id: ID!): Post
search(query: String!, types: [SearchType!]): SearchResultConnection!
}
input UserFilter {
role: UserRole
search: String
createdAfter: DateTime
}
enum UserOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
NAME_ASC
NAME_DESC
}
Resolvers with DataLoader (N+1 Prevention)
// DataLoader setup — batch loading for each entity
import DataLoader from "dataloader";
interface Context {
loaders: {
userLoader: DataLoader<string, User>;
postsByAuthorLoader: DataLoader<string, Post[]>;
commentCountLoader: DataLoader<string, number>;
};
currentUser: AuthUser | null;
}
// Create loaders per request (important: one per request!)
function createLoaders(db: Database): Context["loaders"] {
return {
userLoader: 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) ?? new Error(`User ${id} not found`));
}),
postsByAuthorLoader: 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) ?? []);
}),
commentCountLoader: new DataLoader(async (postIds: readonly string[]) => {
const counts = await db.comments.countByPostIds([...postIds]);
const countMap = new Map(counts.map((c) => [c.postId, c.count]));
return postIds.map((id) => countMap.get(id) ?? 0);
}),
};
}
// Resolvers using DataLoader
const resolvers = {
Query: {
user: async (_parent: unknown, { id }: { id: string }, ctx: Context) => {
return ctx.loaders.userLoader.load(id);
},
users: async (_parent: unknown, args: UsersArgs, ctx: Context) => {
const { filter, first, after, orderBy } = args;
return paginateUsers(ctx.db, { filter, first, after, orderBy });
},
},
Post: {
// DataLoader resolves N+1 — all author loads in one query batch
author: (post: Post, _args: unknown, ctx: Context) => {
return ctx.loaders.userLoader.load(post.authorId);
},
comments: async (post: Post, args: PaginationArgs, ctx: Context) => {
return paginateComments(ctx.db, post.id, args);
},
},
Mutation: {
createPost: async (_parent: unknown, { input }: { input: CreatePostInput }, ctx: Context) => {
if (!ctx.currentUser) {
return { __typename: "AuthorizationError", message: "Not authenticated" };
}
const errors = validateCreatePost(input);
if (errors.length > 0) {
return { __typename: "ValidationError", ...errors[0] };
}
const post = await ctx.db.posts.create({
...input,
authorId: ctx.currentUser.id,
status: "DRAFT",
});
return { __typename: "CreatePostSuccess", post };
},
},
// Union type resolution
CreatePostResult: {
__resolveType(obj: any) {
if (obj.post) return "CreatePostSuccess";
if (obj.field) return "ValidationError";
return "AuthorizationError";
},
},
};
Cursor-Based Pagination
// Generic pagination helper
interface PaginationArgs {
first?: number;
after?: string;
}
interface Connection<T> {
edges: Array<{ cursor: string; node: T }>;
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
endCursor: string | null;
};
totalCount: number;
}
function encodeCursor(id: string): string {
return Buffer.from(`cursor:${id}`).toString("base64");
}
function decodeCursor(cursor: string): string {
const decoded = Buffer.from(cursor, "base64").toString("utf-8");
return decoded.replace("cursor:", "");
}
async function paginate<T extends { id: string }>(
query: QueryBuilder<T>,
args: PaginationArgs
): Promise<Connection<T>> {
const limit = Math.min(args.first ?? 20, 100); // Cap at 100
if (args.after) {
const afterId = decodeCursor(args.after);
query = query.where("id", ">", afterId);
}
const [items, totalCount] = await Promise.all([
query.orderBy("id").limit(limit + 1).execute(),
query.count(),
]);
const hasNextPage = items.length > limit;
const nodes = hasNextPage ? items.slice(0, limit) : items;
return {
edges: nodes.map((node) => ({
cursor: encodeCursor(node.id),
node,
})),
pageInfo: {
hasNextPage,
hasPreviousPage: !!args.after,
startCursor: nodes.length > 0 ? encodeCursor(nodes[0].id) : null,
endCursor: nodes.length > 0 ? encodeCursor(nodes[nodes.length - 1].id) : null,
},
totalCount,
};
}
Subscriptions
import { PubSub, withFilter } from "graphql-subscriptions";
const pubsub = new PubSub();
// Schema
const typeDefs = `
type Subscription {
postPublished(authorId: ID): Post!
commentAdded(postId: ID!): Comment!
}
`;
// Resolvers
const resolvers = {
Subscription: {
postPublished: {
subscribe: withFilter(
() => pubsub.asyncIterator(["POST_PUBLISHED"]),
(payload, variables) => {
if (!variables.authorId) return true;
return payload.postPublished.authorId === variables.authorId;
}
),
},
commentAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(["COMMENT_ADDED"]),
(payload, variables) => {
return payload.commentAdded.postId === variables.postId;
}
),
},
},
Mutation: {
publishPost: async (_parent, { id }, ctx) => {
const post = await ctx.db.posts.publish(id);
// Emit subscription event
pubsub.publish("POST_PUBLISHED", { postPublished: post });
return { __typename: "PublishPostSuccess", post };
},
},
};
// For production, use Redis PubSub:
// import { RedisPubSub } from "graphql-redis-subscriptions";
// const pubsub = new RedisPubSub({ connection: redisOptions });
Query Complexity and Rate Limiting
import { createComplexityLimitRule } from "graphql-validation-complexity";
import depthLimit from "graphql-depth-limit";
// Apply validation rules
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(10), // Max query depth
createComplexityLimitRule(1000, {
// Cost per field
scalarCost: 1,
objectCost: 2,
listFactor: 10,
// Custom field costs
formatErrorMessage: (cost: number) =>
`Query too complex: cost ${cost} exceeds maximum 1000`,
}),
],
});
// Schema directives for field-level cost
// directive @cost(value: Int!) on FIELD_DEFINITION
// directive @listSize(max: Int!) on FIELD_DEFINITION
//
// type Query {
// users(first: Int = 20): UserConnection! @cost(value: 5) @listSize(max: 100)
// search(query: String!): [SearchResult!]! @cost(value: 10)
// }
Federation (Apollo Federation v2)
# Users subgraph
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# Posts subgraph — extends User
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@external"])
type User @key(fields: "id") {
id: ID! @external
posts(first: Int = 10, after: String): PostConnection!
}
type Post @key(fields: "id") {
id: ID!
title: String!
author: User!
}
// Reference resolver for federated entity
const resolvers = {
User: {
__resolveReference: async (ref: { id: string }, ctx: Context) => {
return ctx.loaders.userLoader.load(ref.id);
},
},
};
Caching Strategies
# Cache control directives
type Post @cacheControl(maxAge: 60) {
id: ID!
title: String! @cacheControl(maxAge: 300)
content: String! @cacheControl(maxAge: 300)
viewCount: Int! @cacheControl(maxAge: 0) # Never cache
author: User!
}
// Response caching with Apollo
import responseCachePlugin from "@apollo/server-plugin-response-cache";
const server = new ApolloServer({
plugins: [
responseCachePlugin({
sessionId: (ctx) => ctx.request.http?.headers.get("authorization") ?? null,
}),
],
});
// Application-level caching in resolvers
const resolvers = {
Query: {
popularPosts: async (_parent, _args, ctx) => {
const cacheKey = "popular-posts";
const cached = await ctx.cache.get(cacheKey);
if (cached) return JSON.parse(cached);
const posts = await ctx.db.posts.findPopular(20);
await ctx.cache.set(cacheKey, JSON.stringify(posts), { ttl: 300 });
return posts;
},
},
};
Authorization Patterns
// Directive-based auth
// directive @auth(requires: UserRole!) on FIELD_DEFINITION
//
// type Mutation {
// deleteUser(id: ID!): DeleteUserResult! @auth(requires: ADMIN)
// updateProfile(input: UpdateProfileInput!): User! @auth(requires: VIEWER)
// }
// Field-level authorization in resolvers
const resolvers = {
User: {
email: (user: User, _args: unknown, ctx: Context) => {
// Only show email to the user themselves or admins
if (ctx.currentUser?.id === user.id || ctx.currentUser?.role === "ADMIN") {
return user.email;
}
return null; // Field is nullable, return null for unauthorized
},
},
};
// Middleware-style auth check
function requireAuth(resolver: GraphQLFieldResolver<any, Context>) {
return (parent: any, args: any, ctx: Context, info: GraphQLResolveInfo) => {
if (!ctx.currentUser) {
throw new GraphQLError("Authentication required", {
extensions: { code: "UNAUTHENTICATED" },
});
}
return resolver(parent, args, ctx, info);
};
}
Common Anti-Patterns
# ❌ BAD: Exposing database structure directly
type User {
user_id: Int! # DB column names
created_at: String! # String instead of DateTime
password_hash: String! # Sensitive data exposed
}
# ✅ GOOD: Domain-driven types
type User {
id: ID!
createdAt: DateTime!
# Never expose password_hash
}
# ❌ BAD: Offset pagination for large datasets
type Query {
users(page: Int, pageSize: Int): [User!]! # Skips are expensive
}
# ✅ GOOD: Cursor-based pagination
type Query {
users(first: Int, after: String): UserConnection!
}
# ❌ BAD: Generic catch-all types
type Response {
success: Boolean!
message: String
data: JSON # Untyped blob
}
# ✅ GOOD: Typed union results
union CreateUserResult = CreateUserSuccess | ValidationError | ConflictError
// ❌ BAD: N+1 in resolvers (one query per post author)
Post: {
author: (post) => db.users.findById(post.authorId) // Called N times!
}
// ✅ GOOD: DataLoader batches into single query
Post: {
author: (post, _, ctx) => ctx.loaders.userLoader.load(post.authorId)
}
Adapted from buildwithclaude by Dave Poon (MIT)
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.