graphql

GraphQL schema design, resolver patterns, subscriptions, DataLoader batching, federation, and security best practices

39 stars

Best use case

graphql is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

GraphQL schema design, resolver patterns, subscriptions, DataLoader batching, federation, and security best practices

Teams using graphql should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/graphql/SKILL.md --create-dirs "https://raw.githubusercontent.com/InugamiDev/ultrathink-oss/main/.claude/skills/graphql/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/graphql/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How graphql Compares

Feature / AgentgraphqlStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

GraphQL schema design, resolver patterns, subscriptions, DataLoader batching, federation, and security best practices

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# GraphQL Domain Skill

## Purpose

Provide expert-level guidance on GraphQL schema design, resolver implementation, subscriptions, DataLoader for N+1 prevention, schema federation, security hardening, and production optimization patterns.

## Key Patterns

### 1. Schema Design Principles

```graphql
# Use Relay-style connections for pagination
type Query {
  """List users with cursor-based pagination."""
  users(
    first: Int
    after: String
    last: Int
    before: String
    filter: UserFilter
    orderBy: UserOrderBy
  ): UserConnection!

  """Get a single user by ID."""
  user(id: ID!): User

  """Get the currently authenticated user."""
  me: User
}

type Mutation {
  """Create a new user account."""
  createUser(input: CreateUserInput!): CreateUserPayload!

  """Update user profile fields."""
  updateUser(input: UpdateUserInput!): UpdateUserPayload!

  """Delete a user account (soft delete)."""
  deleteUser(id: ID!): DeleteUserPayload!
}

type Subscription {
  """Subscribe to new messages in a channel."""
  messageReceived(channelId: ID!): Message!
}

# Input types for mutations
input CreateUserInput {
  email: String!
  name: String!
  role: UserRole = MEMBER
}

# Payload types with userErrors for client-safe errors
type CreateUserPayload {
  user: User
  userErrors: [UserError!]!
}

type UserError {
  field: [String!]
  message: String!
  code: ErrorCode!
}

enum ErrorCode {
  INVALID_INPUT
  NOT_FOUND
  ALREADY_EXISTS
  UNAUTHORIZED
}

# Relay connection types
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# Interfaces for shared types
interface Node {
  id: ID!
}

interface Timestamped {
  createdAt: DateTime!
  updatedAt: DateTime!
}

type User implements Node & Timestamped {
  id: ID!
  email: String!
  name: String!
  role: UserRole!
  posts(first: Int, after: String): PostConnection!
  createdAt: DateTime!
  updatedAt: DateTime!
}

enum UserRole {
  ADMIN
  MEMBER
  VIEWER
}

# Custom scalars
scalar DateTime
scalar JSON
scalar EmailAddress
```

### 2. Resolver Patterns (Node.js / TypeScript)

```typescript
import { GraphQLResolveInfo } from 'graphql';
import DataLoader from 'dataloader';

// Context type
interface Context {
  currentUser: User | null;
  loaders: ReturnType<typeof createLoaders>;
  db: Database;
}

// DataLoader factory -- create per-request to prevent caching across users
function createLoaders(db: Database) {
  return {
    user: new DataLoader<string, User | null>(async (ids) => {
      const users = await db.user.findMany({ where: { id: { in: [...ids] } } });
      const userMap = new Map(users.map(u => [u.id, u]));
      return ids.map(id => userMap.get(id) ?? null);
    }),

    userPosts: new DataLoader<string, Post[]>(async (userIds) => {
      const posts = await db.post.findMany({
        where: { authorId: { in: [...userIds] } },
      });
      const grouped = groupBy(posts, 'authorId');
      return userIds.map(id => grouped[id] ?? []);
    }),
  };
}

// Resolvers
const resolvers = {
  Query: {
    me: (_parent: unknown, _args: unknown, ctx: Context) => {
      return ctx.currentUser;
    },

    users: async (_parent: unknown, args: ConnectionArgs, ctx: Context) => {
      requireAuth(ctx);
      return paginateConnection(ctx.db.user, args);
    },

    user: async (_parent: unknown, args: { id: string }, ctx: Context) => {
      return ctx.loaders.user.load(args.id);
    },
  },

  Mutation: {
    createUser: async (_parent: unknown, args: { input: CreateUserInput }, ctx: Context) => {
      requireRole(ctx, 'ADMIN');

      const existing = await ctx.db.user.findUnique({ where: { email: args.input.email } });
      if (existing) {
        return {
          user: null,
          userErrors: [{ field: ['email'], message: 'Email already registered', code: 'ALREADY_EXISTS' }],
        };
      }

      const user = await ctx.db.user.create({ data: args.input });
      return { user, userErrors: [] };
    },
  },

  User: {
    // Field-level resolver with DataLoader
    posts: (parent: User, args: ConnectionArgs, ctx: Context) => {
      return ctx.loaders.userPosts.load(parent.id);
    },
  },

  Subscription: {
    messageReceived: {
      subscribe: (_parent: unknown, args: { channelId: string }, ctx: Context) => {
        requireAuth(ctx);
        return ctx.pubsub.asyncIterator(`CHANNEL_${args.channelId}`);
      },
    },
  },
};
```

### 3. Connection/Pagination Helper

```typescript
interface ConnectionArgs {
  first?: number | null;
  after?: string | null;
  last?: number | null;
  before?: string | null;
}

async function paginateConnection<T extends { id: string }>(
  model: PrismaModel<T>,
  args: ConnectionArgs,
  where?: Record<string, unknown>,
) {
  const limit = args.first ?? args.last ?? 20;
  const clampedLimit = Math.min(limit, 100);

  let cursor: string | undefined;
  let direction: 'forward' | 'backward' = 'forward';

  if (args.after) {
    cursor = decodeCursor(args.after);
    direction = 'forward';
  } else if (args.before) {
    cursor = decodeCursor(args.before);
    direction = 'backward';
  }

  // Fetch one extra to determine hasNextPage/hasPreviousPage
  const items = await model.findMany({
    where: {
      ...where,
      ...(cursor ? { id: { [direction === 'forward' ? 'gt' : 'lt']: cursor } } : {}),
    },
    take: clampedLimit + 1,
    orderBy: { id: direction === 'forward' ? 'asc' : 'desc' },
  });

  const hasMore = items.length > clampedLimit;
  const nodes = hasMore ? items.slice(0, clampedLimit) : items;
  if (direction === 'backward') nodes.reverse();

  const totalCount = await model.count({ where });

  return {
    edges: nodes.map(node => ({
      node,
      cursor: encodeCursor(node.id),
    })),
    pageInfo: {
      hasNextPage: direction === 'forward' ? hasMore : !!cursor,
      hasPreviousPage: direction === 'backward' ? hasMore : !!cursor,
      startCursor: nodes.length ? encodeCursor(nodes[0].id) : null,
      endCursor: nodes.length ? encodeCursor(nodes[nodes.length - 1].id) : null,
    },
    totalCount,
  };
}
```

### 4. Security

```typescript
// Query depth limiting
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(10)],
  plugins: [
    // Query complexity analysis
    createComplexityPlugin({
      maximumComplexity: 1000,
      defaultComplexity: 1,
      estimators: [
        fieldExtensionsEstimator(),
        simpleEstimator({ defaultComplexity: 1 }),
      ],
      onComplete: (complexity) => {
        if (complexity > 500) {
          logger.warn({ complexity }, 'High query complexity');
        }
      },
    }),
  ],
});

// Disable introspection in production
const server = new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production',
});

// Rate limiting per operation
const rateLimitDirective = (limit: number, window: string) => {
  return (next: Function) => async (root: any, args: any, ctx: Context, info: any) => {
    const key = `ratelimit:${ctx.currentUser?.id}:${info.fieldName}`;
    const current = await ctx.redis.incr(key);
    if (current === 1) await ctx.redis.expire(key, parseWindow(window));
    if (current > limit) throw new Error('Rate limit exceeded');
    return next(root, args, ctx, info);
  };
};
```

## Best Practices

1. **Use Relay cursor-based pagination** for all list fields
2. **Return payload types from mutations** with `userErrors` array, never throw for user errors
3. **Use DataLoader for every relationship** -- N+1 queries are the #1 GraphQL perf issue
4. **Create DataLoader instances per-request** -- never share across requests
5. **Limit query depth** (10 max) and complexity (1000 max)
6. **Use input types** for mutation arguments -- never inline scalars
7. **Design nullable by default** -- only mark `!` when you can guarantee the field
8. **Version via schema evolution** -- add fields, deprecate old ones, never remove
9. **Use persisted queries** in production to prevent arbitrary query injection
10. **Log and monitor resolver execution times** per field

## Common Pitfalls

| Pitfall | Impact | Fix |
|---------|--------|-----|
| N+1 queries without DataLoader | Exponential DB load | DataLoader for every relationship resolver |
| Deeply nested queries | DoS via resource exhaustion | Depth limiting + complexity analysis |
| Over-fetching in resolvers | Slow responses | Check `info.fieldNodes` to resolve only requested fields |
| Throwing errors in mutations | Bad client experience | Return structured `userErrors` in payload types |
| Shared DataLoader across requests | Data leaks between users | Create new DataLoader per request in context |
| No introspection control | Schema exposure | Disable introspection in production |

Related Skills

graphql-codegen

39
from InugamiDev/ultrathink-oss

GraphQL code generation — typed operations, fragment colocation, and schema-first development.

ultrathink

39
from InugamiDev/ultrathink-oss

UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.

ultrathink_review

39
from InugamiDev/ultrathink-oss

Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.

ultrathink_memory

39
from InugamiDev/ultrathink-oss

Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.

ui-design

39
from InugamiDev/ultrathink-oss

Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.

Zod

39
from InugamiDev/ultrathink-oss

> TypeScript-first schema validation with static type inference.

webinar-registration-page

39
from InugamiDev/ultrathink-oss

Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".

webhooks

39
from InugamiDev/ultrathink-oss

Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys

web-workers

39
from InugamiDev/ultrathink-oss

Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns

web-vitals

39
from InugamiDev/ultrathink-oss

Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js

web-components

39
from InugamiDev/ultrathink-oss

Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns

wasm

39
from InugamiDev/ultrathink-oss

WebAssembly integration — Rust to WASM with wasm-pack/wasm-bindgen, WASI, browser usage, server-side WASM, and performance considerations