Agent skill
graphql-api
GraphQL API design and implementation patterns. Use when building GraphQL servers, defining schemas, writing resolvers, implementing subscriptions, or consuming GraphQL APIs.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/graphql-api
SKILL.md
GraphQL API Patterns
Schema Design (SDL)
# schema.graphql
type Query {
user(id: ID!): User
users(filter: UserFilter, pagination: Pagination): UserConnection!
posts(authorId: ID, published: Boolean): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
deleteUser(id: ID!): Boolean!
}
type Subscription {
messageAdded(chatId: ID!): Message!
}
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
body: String!
published: Boolean!
author: User!
tags: [String!]!
}
# Pagination (Relay-style)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge { node: User!; cursor: String! }
type PageInfo { hasNextPage: Boolean!; endCursor: String }
# Input types
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UserFilter {
role: Role
search: String
}
input Pagination {
first: Int
after: String
}
# Error handling — union pattern
union UserPayload = User | UserError
type UserError { message: String!; code: String! }
enum Role { USER ADMIN }
scalar DateTime
Server Setup (Strawberry / Python)
# graphql_app.py
import strawberry
from strawberry.fastapi import GraphQLRouter
from strawberry.types import Info
@strawberry.type
class User:
id: strawberry.ID
email: str
name: str
@strawberry.field
async def posts(self, info: Info) -> list["Post"]:
return await info.context["loaders"].posts_by_user.load(self.id)
@strawberry.type
class Query:
@strawberry.field
async def user(self, id: strawberry.ID, info: Info) -> User | None:
return await info.context["db"].get_user(id)
@strawberry.field
async def users(self) -> list[User]:
return await info.context["db"].list_users()
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_user(self, email: str, name: str, info: Info) -> User:
return await info.context["db"].create_user(email=email, name=name)
schema = strawberry.Schema(query=Query, mutation=Mutation)
# FastAPI integration
graphql_app = GraphQLRouter(schema, context_getter=get_context)
app.include_router(graphql_app, prefix="/graphql")
DataLoader (N+1 Prevention)
from strawberry.dataloader import DataLoader
async def load_posts_by_user(user_ids: list[str]) -> list[list[Post]]:
"""Batch-load posts for many users in one DB query."""
rows = await db.fetch(
"SELECT * FROM posts WHERE author_id = ANY($1)", user_ids
)
# Group by author_id
grouped = {}
for row in rows:
grouped.setdefault(row["author_id"], []).append(Post(**row))
return [grouped.get(uid, []) for uid in user_ids]
async def get_context():
return {
"db": db,
"loaders": {
"posts_by_user": DataLoader(load_fn=load_posts_by_user)
}
}
Server Setup (Node.js / Apollo)
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
const typeDefs = `#graphql
type Query {
users: [User!]!
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
`
const resolvers = {
Query: {
users: async (_, __, { dataSources }) => dataSources.usersAPI.getUsers(),
user: async (_, { id }, { dataSources }) => dataSources.usersAPI.getUser(id),
},
}
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: { usersAPI: new UsersAPI() },
}),
})
Client Usage (urql / React)
import { useQuery, useMutation } from 'urql'
const GET_USERS = `
query GetUsers($filter: UserFilter) {
users(filter: $filter) {
edges {
node { id name email }
}
pageInfo { hasNextPage endCursor }
}
}
`
function UserList() {
const [result] = useQuery({
query: GET_USERS,
variables: { filter: { role: 'USER' } },
})
if (result.fetching) return <p>Loading...</p>
if (result.error) return <p>Error: {result.error.message}</p>
return result.data.users.edges.map(({ node }) => (
<div key={node.id}>{node.name}</div>
))
}
// Mutation
const CREATE_USER = `
mutation CreateUser($email: String!, $name: String!) {
createUser(input: { email: $email, name: $name }) {
... on User { id name }
... on UserError { message code }
}
}
`
const [, createUser] = useMutation(CREATE_USER)
await createUser({ email, name })
Subscriptions (WebSocket)
// Server
const typeDefs = `
type Subscription {
messageAdded(chatId: ID!): Message!
}
`
const resolvers = {
Subscription: {
messageAdded: {
subscribe: (_, { chatId }, { pubsub }) =>
pubsub.asyncIterator(`MESSAGE_ADDED_${chatId}`),
},
},
}
// Publish from mutation
await pubsub.publish(`MESSAGE_ADDED_${chatId}`, { messageAdded: newMessage })
// Client
import { createClient } from 'graphql-ws'
const client = createClient({ url: 'ws://localhost:4000/graphql' })
client.subscribe(
{ query: `subscription { messageAdded(chatId: "1") { id text } }` },
{ next: (data) => console.log(data), error: console.error }
)
Authentication & Authorization
# Strawberry permission classes
from strawberry.permission import BasePermission
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
def has_permission(self, source, info: Info, **kwargs) -> bool:
return info.context["user"] is not None
class IsAdmin(BasePermission):
message = "User is not an admin"
def has_permission(self, source, info: Info, **kwargs) -> bool:
user = info.context["user"]
return user and user.role == "ADMIN"
@strawberry.type
class Query:
@strawberry.field(permission_classes=[IsAuthenticated])
async def me(self, info: Info) -> User:
return info.context["user"]
@strawberry.field(permission_classes=[IsAdmin])
async def admin_stats(self) -> Stats:
...
Rules
- ALWAYS use DataLoaders for relation fields (never query DB in individual resolvers)
- Union types for mutations (UserPayload = User | UserError) — not exceptions
- Relay-style pagination for lists (edges/node/pageInfo + totalCount)
- Never expose internal IDs — use opaque
IDtype - Depth limiting: reject queries deeper than 5-7 levels (prevent DoS)
- Query complexity analysis: reject queries above budget (e.g., 100 complexity)
- Use persisted queries in production (security + performance)
- Subscriptions over WebSocket; SSE is not standard for GraphQL
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?