home / skills / arustydev / ai / lang-graphql-dev

lang-graphql-dev skill

/components/skills/lang-graphql-dev

This skill helps you design and consume GraphQL APIs efficiently by applying foundational patterns for schemas, queries, mutations, and resolvers.

npx playbooks add skill arustydev/ai --skill lang-graphql-dev

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
36.3 KB
---
name: lang-graphql-dev
description: Foundational GraphQL patterns covering schema design, queries, mutations, subscriptions, and resolvers. Use when building or consuming GraphQL APIs. This is the entry point for GraphQL development.
---

# GraphQL Fundamentals

Foundational GraphQL patterns covering schema definition, queries, mutations, subscriptions, resolvers, and API design best practices. Use this skill when building GraphQL APIs, consuming GraphQL endpoints, or designing data graph architectures.

## Skill Hierarchy

```
lang-graphql-dev (foundational, this skill)
├── graphql-federation (multi-service graphs)
├── graphql-optimization (query performance, n+1, dataloaders)
└── graphql-security (auth, rate limiting, depth limiting)
```

This skill covers:
- Schema definition (types, fields, scalars, enums)
- Query operations and variables
- Mutations and input types
- Subscriptions for real-time updates
- Resolver implementation patterns
- Interfaces and unions for polymorphism
- Custom directives
- Error handling strategies
- API design best practices

## Quick Reference

| Pattern | GraphQL Syntax |
|---------|----------------|
| Object Type | `type User { id: ID! name: String! }` |
| Query Field | `type Query { user(id: ID!): User }` |
| Mutation | `type Mutation { createUser(input: CreateUserInput!): User! }` |
| Subscription | `type Subscription { userCreated: User! }` |
| Non-Null | `field: String!` (cannot be null) |
| List | `users: [User!]!` (non-null list of non-null users) |
| Input Type | `input CreateUserInput { name: String! email: String! }` |
| Enum | `enum Role { ADMIN USER GUEST }` |
| Interface | `interface Node { id: ID! }` |
| Union | `union SearchResult = User \| Post \| Comment` |
| Directive | `field: String @deprecated(reason: "Use newField")` |
| Fragment | `fragment UserFields on User { id name }` |
| Alias | `admin: user(id: "1") { name }` |
| Variable | `query GetUser($id: ID!) { user(id: $id) }` |

## Schema Definition Language (SDL)

### Object Types

Object types represent entities in your API with named fields:

```graphql
# Basic object type
type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  isActive: Boolean!
  createdAt: DateTime!
}

# Type with relationships
type Post {
  id: ID!
  title: String!
  content: String!
  author: User!          # Single relationship
  comments: [Comment!]!  # List relationship
  tags: [String!]!       # List of scalars
  publishedAt: DateTime
}

# Nested object type
type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
  replies: [Comment!]!   # Self-referential
  createdAt: DateTime!
}
```

### Scalar Types

Built-in scalars and custom scalar definitions:

```graphql
# Built-in scalars
# Int: 32-bit signed integer
# Float: signed double-precision floating-point
# String: UTF-8 character sequence
# Boolean: true or false
# ID: unique identifier (serialized as String)

# Custom scalar declarations
scalar DateTime
scalar Email
scalar URL
scalar JSON
scalar Upload

# Usage in types
type Event {
  id: ID!
  name: String!
  startTime: DateTime!
  endTime: DateTime!
  website: URL
  metadata: JSON
}

type User {
  id: ID!
  email: Email!
  avatar: URL
}
```

### Enums

Enumeration types with fixed set of values:

```graphql
# Basic enum
enum Role {
  ADMIN
  USER
  GUEST
}

# Enum with descriptions
enum PostStatus {
  """
  Draft state - not visible to public
  """
  DRAFT

  """
  Published and visible to all users
  """
  PUBLISHED

  """
  Archived - read-only access
  """
  ARCHIVED
}

# Enum in type
type User {
  id: ID!
  name: String!
  role: Role!
  status: UserStatus!
}

enum UserStatus {
  ACTIVE
  SUSPENDED
  DEACTIVATED
}
```

### Input Types

Input types for mutation and query arguments:

```graphql
# Basic input type
input CreateUserInput {
  name: String!
  email: String!
  age: Int
  role: Role!
}

# Nested input type
input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
  tags: [String!]!
  metadata: PostMetadataInput
}

input PostMetadataInput {
  category: String
  featured: Boolean
  seoKeywords: [String!]
}

# Update input (all fields optional)
input UpdateUserInput {
  name: String
  email: String
  age: Int
  role: Role
}

# Filter input for queries
input UserFilterInput {
  role: Role
  isActive: Boolean
  createdAfter: DateTime
  search: String
}

# Pagination input
input PaginationInput {
  limit: Int = 10
  offset: Int = 0
}

input CursorPaginationInput {
  first: Int
  after: String
  last: Int
  before: String
}
```

### Interfaces

Interfaces define common fields shared by multiple types:

```graphql
# Basic interface
interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

# Types implementing interface
type User implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
  email: String!
}

type Post implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  content: String!
  author: User!
}

# Multiple interfaces
interface Timestamped {
  createdAt: DateTime!
  updatedAt: DateTime!
}

interface Authored {
  author: User!
}

type Article implements Node & Timestamped & Authored {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  author: User!
  title: String!
  body: String!
}

# Querying with interfaces
type Query {
  node(id: ID!): Node
  nodes(ids: [ID!]!): [Node!]!

  # Returns any type implementing Node
  search(query: String!): [Node!]!
}
```

### Unions

Unions represent values that could be one of several types:

```graphql
# Basic union
union SearchResult = User | Post | Comment

# Union in query
type Query {
  search(query: String!): [SearchResult!]!
}

# Querying unions (requires inline fragments)
# query {
#   search(query: "graphql") {
#     ... on User {
#       id
#       name
#       email
#     }
#     ... on Post {
#       id
#       title
#       author { name }
#     }
#     ... on Comment {
#       id
#       text
#       author { name }
#     }
#   }
# }

# Error handling with unions
union CreateUserResult = User | ValidationError | DuplicateEmailError

type ValidationError {
  message: String!
  fields: [String!]!
}

type DuplicateEmailError {
  message: String!
  email: String!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserResult!
}

# Media type union
union Media = Photo | Video | Audio

type Photo {
  id: ID!
  url: URL!
  width: Int!
  height: Int!
}

type Video {
  id: ID!
  url: URL!
  duration: Int!
  thumbnail: URL!
}

type Audio {
  id: ID!
  url: URL!
  duration: Int!
}
```

### Directives

Built-in and custom directives:

```graphql
# Built-in directives

# @deprecated - mark fields as deprecated
type User {
  id: ID!
  name: String!
  username: String! @deprecated(reason: "Use name instead")
  email: String!
}

# @skip - conditionally exclude field
# @include - conditionally include field
# query GetUser($id: ID!, $withEmail: Boolean!) {
#   user(id: $id) {
#     id
#     name
#     email @include(if: $withEmail)
#   }
# }

# Custom directive definitions
directive @auth(
  requires: Role = USER
) on OBJECT | FIELD_DEFINITION

directive @rateLimit(
  limit: Int!
  duration: Int!
) on FIELD_DEFINITION

directive @cacheControl(
  maxAge: Int
  scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT

enum CacheControlScope {
  PUBLIC
  PRIVATE
}

# Using custom directives
type Query {
  publicPosts: [Post!]! @cacheControl(maxAge: 300, scope: PUBLIC)

  me: User! @auth(requires: USER)

  adminUsers: [User!]! @auth(requires: ADMIN)

  search(query: String!): [SearchResult!]!
    @rateLimit(limit: 100, duration: 60)
}

# Field-level directive
type User @auth(requires: USER) {
  id: ID!
  name: String!
  email: String! @auth(requires: ADMIN)
  posts: [Post!]!
}
```

## Root Operation Types

### Query Type

Read-only operations:

```graphql
type Query {
  # Single entity by ID
  user(id: ID!): User
  post(id: ID!): Post

  # List with optional filtering
  users(
    filter: UserFilterInput
    limit: Int = 10
    offset: Int = 0
  ): [User!]!

  # Search operations
  searchUsers(query: String!): [User!]!
  searchPosts(query: String!): [Post!]!
  search(query: String!): [SearchResult!]!

  # Nested queries
  userPosts(userId: ID!, limit: Int = 10): [Post!]!
  postComments(postId: ID!): [Comment!]!

  # Aggregations
  userCount: Int!
  postCount(authorId: ID): Int!

  # Current user
  me: User
}
```

### Mutation Type

Write operations that modify data:

```graphql
type Mutation {
  # Create operations
  createUser(input: CreateUserInput!): User!
  createPost(input: CreatePostInput!): Post!
  createComment(input: CreateCommentInput!): Comment!

  # Update operations
  updateUser(id: ID!, input: UpdateUserInput!): User!
  updatePost(id: ID!, input: UpdatePostInput!): Post!

  # Delete operations
  deleteUser(id: ID!): DeleteResult!
  deletePost(id: ID!): DeleteResult!

  # Batch operations
  deleteUsers(ids: [ID!]!): BatchDeleteResult!
  updateUserRoles(updates: [UserRoleUpdate!]!): [User!]!

  # Complex mutations
  publishPost(id: ID!): Post!
  likePost(postId: ID!): Post!
  followUser(userId: ID!): User!

  # File upload
  uploadAvatar(file: Upload!): User!
}

type DeleteResult {
  success: Boolean!
  id: ID!
}

type BatchDeleteResult {
  success: Boolean!
  deletedCount: Int!
  deletedIds: [ID!]!
}

input UserRoleUpdate {
  userId: ID!
  role: Role!
}
```

### Subscription Type

Real-time event streams:

```graphql
type Subscription {
  # Entity created events
  userCreated: User!
  postCreated: Post!
  commentCreated(postId: ID): Comment!

  # Entity updated events
  userUpdated(id: ID!): User!
  postUpdated(id: ID!): Post!

  # Entity deleted events
  userDeleted: ID!
  postDeleted: ID!

  # Custom events
  messageReceived(channelId: ID!): Message!
  notificationReceived: Notification!

  # Filtered subscriptions
  postsInCategory(category: String!): Post!
  userActivity(userId: ID!): ActivityEvent!
}

type Message {
  id: ID!
  channelId: ID!
  author: User!
  text: String!
  createdAt: DateTime!
}

type Notification {
  id: ID!
  type: NotificationType!
  title: String!
  body: String!
  createdAt: DateTime!
}

enum NotificationType {
  MENTION
  LIKE
  COMMENT
  FOLLOW
}

union ActivityEvent = PostCreated | PostLiked | CommentCreated

type PostCreated {
  post: Post!
}

type PostLiked {
  post: Post!
  user: User!
}

type CommentCreated {
  comment: Comment!
}
```

## Query Operations

### Basic Queries

Simple field selection:

```graphql
# Fetch single user
query GetUser {
  user(id: "123") {
    id
    name
    email
  }
}

# Fetch list of users
query GetUsers {
  users(limit: 10) {
    id
    name
    email
  }
}

# Nested fields
query GetUserWithPosts {
  user(id: "123") {
    id
    name
    posts {
      id
      title
      publishedAt
    }
  }
}

# Multiple root fields
query GetDashboardData {
  me {
    id
    name
  }
  recentPosts(limit: 5) {
    id
    title
  }
  notifications {
    id
    title
  }
}
```

### Query Variables

Parameterized queries for reusability:

```graphql
# Query with variables
query GetUser($userId: ID!, $postLimit: Int = 5) {
  user(id: $userId) {
    id
    name
    email
    posts(limit: $postLimit) {
      id
      title
    }
  }
}

# Variables JSON
{
  "userId": "123",
  "postLimit": 10
}

# Optional variables with defaults
query GetPosts($limit: Int = 10, $offset: Int = 0) {
  posts(limit: $limit, offset: $offset) {
    id
    title
  }
}

# Variables with input types
query SearchUsers($filter: UserFilterInput!) {
  users(filter: $filter) {
    id
    name
    email
  }
}

# Variables JSON
{
  "filter": {
    "role": "ADMIN",
    "isActive": true
  }
}

# Non-null variables
query CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
  }
}
```

### Aliases

Rename fields in response:

```graphql
query GetMultipleUsers {
  admin: user(id: "1") {
    id
    name
  }
  regularUser: user(id: "2") {
    id
    name
  }
}

# Response shape:
# {
#   "data": {
#     "admin": { "id": "1", "name": "Admin User" },
#     "regularUser": { "id": "2", "name": "Regular User" }
#   }
# }

query GetUserStats {
  allUsers: userCount
  activeUsers: userCount(filter: { isActive: true })
  adminUsers: userCount(filter: { role: ADMIN })
}
```

### Fragments

Reusable field selections:

```graphql
# Named fragment
fragment UserFields on User {
  id
  name
  email
  createdAt
}

query GetUsers {
  users {
    ...UserFields
  }
}

query GetUser($id: ID!) {
  user(id: $id) {
    ...UserFields
    posts {
      id
      title
    }
  }
}

# Nested fragments
fragment PostPreview on Post {
  id
  title
  publishedAt
  author {
    ...UserFields
  }
}

fragment CommentFields on Comment {
  id
  text
  author {
    ...UserFields
  }
}

query GetPost($id: ID!) {
  post(id: $id) {
    ...PostPreview
    content
    comments {
      ...CommentFields
    }
  }
}

# Inline fragments for unions
query Search($query: String!) {
  search(query: $query) {
    ... on User {
      id
      name
      email
    }
    ... on Post {
      id
      title
      author { name }
    }
    ... on Comment {
      id
      text
    }
  }
}

# Inline fragments for interfaces
query GetNodes($ids: [ID!]!) {
  nodes(ids: $ids) {
    id
    ... on User {
      name
      email
    }
    ... on Post {
      title
      content
    }
  }
}
```

### Directives in Queries

Conditional field inclusion:

```graphql
query GetUser($id: ID!, $withEmail: Boolean!, $withPosts: Boolean!) {
  user(id: $id) {
    id
    name
    email @include(if: $withEmail)
    posts @include(if: $withPosts) {
      id
      title
    }
  }
}

query GetUsers($skipArchived: Boolean!) {
  users {
    id
    name
    archivedAt @skip(if: $skipArchived)
  }
}

# Combining directives
query GetUserProfile(
  $id: ID!
  $withEmail: Boolean!
  $skipAvatar: Boolean!
) {
  user(id: $id) {
    id
    name
    email @include(if: $withEmail)
    avatar @skip(if: $skipAvatar)
  }
}
```

## Mutation Operations

### Basic Mutations

Create, update, and delete operations:

```graphql
# Create mutation
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
    createdAt
  }
}

# Variables
{
  "input": {
    "name": "John Doe",
    "email": "[email protected]",
    "role": "USER"
  }
}

# Update mutation
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    id
    name
    email
    updatedAt
  }
}

# Variables
{
  "id": "123",
  "input": {
    "name": "Jane Doe"
  }
}

# Delete mutation
mutation DeleteUser($id: ID!) {
  deleteUser(id: $id) {
    success
    id
  }
}

# Variables
{
  "id": "123"
}
```

### Multiple Mutations

Execute multiple mutations in sequence:

```graphql
mutation CreateUserAndPost(
  $userInput: CreateUserInput!
  $postInput: CreatePostInput!
) {
  createUser(input: $userInput) {
    id
    name
  }
  createPost(input: $postInput) {
    id
    title
    author { name }
  }
}

# Mutations with aliases
mutation BatchUpdate {
  user1: updateUser(id: "1", input: { name: "User One" }) {
    id
    name
  }
  user2: updateUser(id: "2", input: { name: "User Two" }) {
    id
    name
  }
}
```

### Optimistic Response Pattern

Return updated data after mutation:

```graphql
mutation LikePost($postId: ID!) {
  likePost(postId: $postId) {
    id
    title
    likeCount
    likedBy {
      id
      name
    }
    # Return full post data for cache update
    author {
      id
      name
    }
    createdAt
  }
}

mutation FollowUser($userId: ID!) {
  followUser(userId: $userId) {
    id
    name
    followerCount
    isFollowedByMe
  }
}
```

## Subscription Operations

### Basic Subscriptions

Subscribe to real-time events:

```graphql
subscription OnUserCreated {
  userCreated {
    id
    name
    email
    createdAt
  }
}

subscription OnPostUpdated($postId: ID!) {
  postUpdated(id: $postId) {
    id
    title
    content
    updatedAt
  }
}

subscription OnMessageReceived($channelId: ID!) {
  messageReceived(channelId: $channelId) {
    id
    text
    author {
      id
      name
    }
    createdAt
  }
}
```

### Subscription with Fragments

Reuse fragments in subscriptions:

```graphql
fragment MessageFields on Message {
  id
  text
  author {
    id
    name
    avatar
  }
  createdAt
}

subscription OnNewMessage($channelId: ID!) {
  messageReceived(channelId: $channelId) {
    ...MessageFields
  }
}

query GetMessages($channelId: ID!) {
  messages(channelId: $channelId) {
    ...MessageFields
  }
}
```

## Resolver Patterns

Resolvers are functions that populate data for fields in your schema.

### Basic Resolvers (JavaScript/TypeScript)

```javascript
// Type resolvers
const resolvers = {
  Query: {
    // (parent, args, context, info) => result
    user: async (parent, { id }, context) => {
      return await context.db.users.findById(id);
    },

    users: async (parent, { filter, limit, offset }, context) => {
      return await context.db.users.find(filter, { limit, offset });
    },

    me: async (parent, args, context) => {
      if (!context.user) {
        throw new Error('Not authenticated');
      }
      return context.user;
    }
  },

  Mutation: {
    createUser: async (parent, { input }, context) => {
      if (!context.user || context.user.role !== 'ADMIN') {
        throw new Error('Not authorized');
      }
      return await context.db.users.create(input);
    },

    updateUser: async (parent, { id, input }, context) => {
      return await context.db.users.update(id, input);
    },

    deleteUser: async (parent, { id }, context) => {
      const deleted = await context.db.users.delete(id);
      return { success: true, id };
    }
  },

  Subscription: {
    userCreated: {
      subscribe: (parent, args, context) => {
        return context.pubsub.asyncIterator(['USER_CREATED']);
      }
    },

    postUpdated: {
      subscribe: (parent, { id }, context) => {
        return context.pubsub.asyncIterator([`POST_UPDATED_${id}`]);
      }
    }
  }
};
```

### Field Resolvers

Resolve nested fields:

```javascript
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return await context.db.users.findById(id);
    }
  },

  User: {
    // Resolve posts for a user
    posts: async (user, { limit }, context) => {
      return await context.db.posts.findByAuthorId(user.id, { limit });
    },

    // Computed field
    fullName: (user) => {
      return `${user.firstName} ${user.lastName}`;
    },

    // Resolve from different data source
    profile: async (user, args, context) => {
      return await context.profileAPI.get(user.profileId);
    },

    // Permission-based field
    email: (user, args, context) => {
      if (context.user?.id === user.id || context.user?.role === 'ADMIN') {
        return user.email;
      }
      return null;
    }
  },

  Post: {
    author: async (post, args, context) => {
      // Use DataLoader to prevent N+1 queries
      return await context.loaders.userLoader.load(post.authorId);
    },

    comments: async (post, args, context) => {
      return await context.db.comments.findByPostId(post.id);
    },

    likeCount: async (post, args, context) => {
      return await context.db.likes.countByPostId(post.id);
    }
  }
};
```

### Interface & Union Resolvers

Resolve type for interfaces and unions:

```javascript
const resolvers = {
  Query: {
    search: async (parent, { query }, context) => {
      const users = await context.db.users.search(query);
      const posts = await context.db.posts.search(query);
      const comments = await context.db.comments.search(query);
      return [...users, ...posts, ...comments];
    },

    node: async (parent, { id }, context) => {
      // Determine type from ID format or lookup
      const type = getTypeFromId(id);
      if (type === 'User') {
        return await context.db.users.findById(id);
      } else if (type === 'Post') {
        return await context.db.posts.findById(id);
      }
      // ... etc
    }
  },

  // Union type resolver
  SearchResult: {
    __resolveType(obj, context, info) {
      if (obj.email) {
        return 'User';
      }
      if (obj.title) {
        return 'Post';
      }
      if (obj.text) {
        return 'Comment';
      }
      return null;
    }
  },

  // Interface type resolver
  Node: {
    __resolveType(obj, context, info) {
      if (obj.email) {
        return 'User';
      }
      if (obj.title) {
        return 'Post';
      }
      // Use __typename if available
      return obj.__typename;
    }
  },

  // Alternative: add __typename in field resolvers
  User: {
    __typename: 'User',
    // ... other fields
  },

  Post: {
    __typename: 'Post',
    // ... other fields
  }
};
```

### DataLoader Pattern (N+1 Prevention)

Batch and cache data loading:

```javascript
import DataLoader from 'dataloader';

// Create loaders
function createLoaders(db) {
  return {
    userLoader: new DataLoader(async (userIds) => {
      const users = await db.users.findByIds(userIds);
      // Return in same order as input
      return userIds.map(id =>
        users.find(user => user.id === id)
      );
    }),

    postLoader: new DataLoader(async (postIds) => {
      const posts = await db.posts.findByIds(postIds);
      return postIds.map(id =>
        posts.find(post => post.id === id)
      );
    }),

    // Batch load posts by author
    postsByAuthorLoader: new DataLoader(async (authorIds) => {
      const posts = await db.posts.findByAuthorIds(authorIds);
      return authorIds.map(authorId =>
        posts.filter(post => post.authorId === authorId)
      );
    })
  };
}

// Use in context
const context = ({ req }) => ({
  user: req.user,
  db,
  loaders: createLoaders(db)
});

// Use in resolvers
const resolvers = {
  Post: {
    author: async (post, args, context) => {
      // Batches multiple author requests
      return await context.loaders.userLoader.load(post.authorId);
    }
  },

  User: {
    posts: async (user, args, context) => {
      // Batches multiple posts-by-author requests
      return await context.loaders.postsByAuthorLoader.load(user.id);
    }
  }
};
```

## Error Handling

### GraphQL Errors

Standard error handling:

```javascript
import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      const user = await context.db.users.findById(id);

      if (!user) {
        throw new GraphQLError('User not found', {
          extensions: {
            code: 'USER_NOT_FOUND',
            userId: id
          }
        });
      }

      return user;
    }
  },

  Mutation: {
    createUser: async (parent, { input }, context) => {
      // Validation error
      if (!input.email.includes('@')) {
        throw new GraphQLError('Invalid email format', {
          extensions: {
            code: 'BAD_USER_INPUT',
            field: 'email'
          }
        });
      }

      // Authorization error
      if (!context.user) {
        throw new GraphQLError('Not authenticated', {
          extensions: {
            code: 'UNAUTHENTICATED'
          }
        });
      }

      if (context.user.role !== 'ADMIN') {
        throw new GraphQLError('Not authorized', {
          extensions: {
            code: 'FORBIDDEN'
          }
        });
      }

      try {
        return await context.db.users.create(input);
      } catch (error) {
        if (error.code === 'DUPLICATE_EMAIL') {
          throw new GraphQLError('Email already exists', {
            extensions: {
              code: 'DUPLICATE_EMAIL',
              email: input.email
            }
          });
        }
        throw error;
      }
    }
  }
};
```

### Error Response Format

GraphQL error response structure:

```json
{
  "errors": [
    {
      "message": "User not found",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": ["user"],
      "extensions": {
        "code": "USER_NOT_FOUND",
        "userId": "123"
      }
    }
  ],
  "data": {
    "user": null
  }
}
```

### Union-Based Error Handling

Type-safe errors using unions:

```graphql
type User {
  id: ID!
  name: String!
  email: String!
}

type ValidationError {
  message: String!
  fields: [String!]!
}

type NotFoundError {
  message: String!
  resourceId: ID!
}

type AuthenticationError {
  message: String!
}

union CreateUserResult = User | ValidationError | AuthenticationError
union GetUserResult = User | NotFoundError | AuthenticationError

type Query {
  user(id: ID!): GetUserResult!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserResult!
}
```

```javascript
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      if (!context.user) {
        return {
          __typename: 'AuthenticationError',
          message: 'Not authenticated'
        };
      }

      const user = await context.db.users.findById(id);
      if (!user) {
        return {
          __typename: 'NotFoundError',
          message: 'User not found',
          resourceId: id
        };
      }

      return {
        __typename: 'User',
        ...user
      };
    }
  },

  GetUserResult: {
    __resolveType(obj) {
      return obj.__typename;
    }
  },

  CreateUserResult: {
    __resolveType(obj) {
      return obj.__typename;
    }
  }
};
```

Query with union errors:

```graphql
query GetUser($id: ID!) {
  user(id: $id) {
    ... on User {
      id
      name
      email
    }
    ... on NotFoundError {
      message
      resourceId
    }
    ... on AuthenticationError {
      message
    }
  }
}
```

## Pagination Patterns

### Offset-Based Pagination

Simple offset/limit pagination:

```graphql
type Query {
  users(limit: Int = 10, offset: Int = 0): UserConnection!
}

type UserConnection {
  items: [User!]!
  totalCount: Int!
  hasMore: Boolean!
}
```

```javascript
const resolvers = {
  Query: {
    users: async (parent, { limit, offset }, context) => {
      const items = await context.db.users.find({}, { limit, offset });
      const totalCount = await context.db.users.count();
      const hasMore = offset + limit < totalCount;

      return { items, totalCount, hasMore };
    }
  }
};
```

### Cursor-Based Pagination (Relay)

Relay-style cursor pagination:

```graphql
type Query {
  users(
    first: Int
    after: String
    last: Int
    before: String
  ): UserConnection!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

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

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

```javascript
import { cursorToOffset, offsetToCursor } from './pagination';

const resolvers = {
  Query: {
    users: async (parent, args, context) => {
      const { first, after, last, before } = args;

      let offset = 0;
      let limit = first || last || 10;

      if (after) {
        offset = cursorToOffset(after) + 1;
      }

      const items = await context.db.users.find({}, { limit, offset });
      const totalCount = await context.db.users.count();

      const edges = items.map((node, index) => ({
        node,
        cursor: offsetToCursor(offset + index)
      }));

      const startCursor = edges.length > 0 ? edges[0].cursor : null;
      const endCursor = edges.length > 0 ? edges[edges.length - 1].cursor : null;

      return {
        edges,
        pageInfo: {
          hasNextPage: offset + limit < totalCount,
          hasPreviousPage: offset > 0,
          startCursor,
          endCursor
        },
        totalCount
      };
    }
  }
};

// Cursor utilities
function offsetToCursor(offset) {
  return Buffer.from(`cursor:${offset}`).toString('base64');
}

function cursorToOffset(cursor) {
  return parseInt(Buffer.from(cursor, 'base64').toString().split(':')[1]);
}
```

## Best Practices

### Schema Design

1. **Use Non-Null (!)**  wisely - Required fields should be non-null
2. **Input types for mutations** - Always use input types, not multiple arguments
3. **Consistent naming** - Use camelCase for fields, PascalCase for types
4. **Descriptive names** - `createUser` not `addU`, `userId` not `uid`
5. **Versioning via new fields** - Add new fields instead of changing existing ones
6. **Connection pattern for lists** - Use edges/nodes for paginated lists
7. **Single source of truth** - One field per concept, use aliases for different views

```graphql
# Good
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

# Avoid
type Mutation {
  createUser(name: String!, email: String!, age: Int): User!
  updateUser(id: ID!, name: String, email: String, age: Int): User!
}
```

### Query Design

1. **Request only needed fields** - Avoid over-fetching
2. **Use fragments** - DRY principle for repeated field sets
3. **Leverage aliases** - Fetch same field with different arguments
4. **Batch queries** - Multiple root fields in one request
5. **Named operations** - Always name queries and mutations

```graphql
# Good
query GetDashboard {
  me {
    ...UserFields
  }
  recentPosts(limit: 5) {
    ...PostFields
  }
}

fragment UserFields on User {
  id
  name
  email
}

fragment PostFields on Post {
  id
  title
  publishedAt
}

# Avoid
query {
  me {
    id
    name
    email
    posts {
      id
      title
      content
      comments {
        id
        text
        author {
          id
          name
        }
      }
    }
  }
}
```

### Resolver Best Practices

1. **Context for request-scoped data** - User, loaders, services
2. **Use DataLoaders** - Prevent N+1 queries
3. **Throw GraphQLError** - Consistent error handling
4. **Validate in resolvers** - Don't rely only on schema validation
5. **Keep resolvers thin** - Business logic in service layer
6. **Async/await** - Consistent async pattern

```javascript
// Good
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return await context.services.users.getById(id);
    }
  },

  User: {
    posts: async (user, args, context) => {
      return await context.loaders.postsByAuthor.load(user.id);
    }
  }
};

// Avoid
const resolvers = {
  Query: {
    user: (parent, { id }) => {
      // Direct database access, no context
      return db.users.findById(id);
    }
  },

  User: {
    posts: (user) => {
      // N+1 query problem
      return db.posts.findByAuthorId(user.id);
    }
  }
};
```

### Security Best Practices

1. **Query depth limiting** - Prevent deeply nested queries
2. **Query complexity analysis** - Assign costs to fields
3. **Rate limiting** - Per-user or per-IP limits
4. **Disable introspection in production** - Hide schema from attackers
5. **Validate input** - Check all user input
6. **Authentication in context** - Check auth before resolvers run
7. **Field-level authorization** - Control access to sensitive fields

```javascript
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5), // Max depth of 5
    createComplexityLimitRule(1000) // Max complexity of 1000
  ],
  introspection: process.env.NODE_ENV !== 'production'
});
```

### Performance Optimization

1. **DataLoader for batching** - Batch database queries
2. **Caching** - Response caching, persisted queries
3. **Field-level caching** - Cache expensive field resolvers
4. **Pagination** - Always paginate lists
5. **Query whitelisting** - Only allow known queries in production
6. **Database query optimization** - Use indexes, avoid N+1
7. **Response compression** - Enable GZIP compression

## Common Patterns

### Relay Global Object Identification

Standardized ID pattern:

```graphql
interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

type Query {
  node(id: ID!): Node
}
```

```javascript
// Encode type into ID
function toGlobalId(type, id) {
  return Buffer.from(`${type}:${id}`).toString('base64');
}

function fromGlobalId(globalId) {
  const [type, id] = Buffer.from(globalId, 'base64').toString().split(':');
  return { type, id };
}

const resolvers = {
  Query: {
    node: async (parent, { id }, context) => {
      const { type, id: localId } = fromGlobalId(id);

      if (type === 'User') {
        return await context.db.users.findById(localId);
      } else if (type === 'Post') {
        return await context.db.posts.findById(localId);
      }

      return null;
    }
  }
};
```

### File Upload

File upload using multipart request:

```graphql
scalar Upload

type Mutation {
  uploadAvatar(file: Upload!): User!
  uploadFiles(files: [Upload!]!): [File!]!
}

type File {
  filename: String!
  mimetype: String!
  encoding: String!
  url: String!
}
```

```javascript
import { GraphQLUpload } from 'graphql-upload';

const resolvers = {
  Upload: GraphQLUpload,

  Mutation: {
    uploadAvatar: async (parent, { file }, context) => {
      const { createReadStream, filename, mimetype } = await file;

      const stream = createReadStream();
      const url = await context.storage.upload(stream, filename);

      return await context.db.users.update(context.user.id, {
        avatar: url
      });
    },

    uploadFiles: async (parent, { files }, context) => {
      const uploadedFiles = [];

      for (const file of files) {
        const { createReadStream, filename, mimetype, encoding } = await file;
        const stream = createReadStream();
        const url = await context.storage.upload(stream, filename);

        uploadedFiles.push({
          filename,
          mimetype,
          encoding,
          url
        });
      }

      return uploadedFiles;
    }
  }
};
```

### Batch Mutations

Efficient bulk operations:

```graphql
type Mutation {
  createUsers(inputs: [CreateUserInput!]!): [User!]!
  updateUsers(updates: [UpdateUserInput!]!): [User!]!
  deleteUsers(ids: [ID!]!): BatchDeleteResult!
}

input UpdateUserInput {
  id: ID!
  name: String
  email: String
}

type BatchDeleteResult {
  success: Boolean!
  deletedCount: Int!
  deletedIds: [ID!]!
}
```

```javascript
const resolvers = {
  Mutation: {
    createUsers: async (parent, { inputs }, context) => {
      return await context.db.users.createMany(inputs);
    },

    updateUsers: async (parent, { updates }, context) => {
      const promises = updates.map(({ id, ...input }) =>
        context.db.users.update(id, input)
      );
      return await Promise.all(promises);
    },

    deleteUsers: async (parent, { ids }, context) => {
      const deletedIds = await context.db.users.deleteMany(ids);

      return {
        success: true,
        deletedCount: deletedIds.length,
        deletedIds
      };
    }
  }
};
```

## Troubleshooting

### Common Issues

**Query not returning data**
- Check resolver return value
- Verify field names match schema
- Check for errors in GraphQL response
- Validate variables are passed correctly

**N+1 Query Problem**
- Symptom: Many database queries for related data
- Solution: Use DataLoader to batch queries
- Example: Loading authors for 100 posts creates 101 queries without batching

**Type Resolution Errors**
- Interface/union types need `__resolveType`
- Return `__typename` field from resolvers
- Verify type names match schema exactly

**Authentication Errors**
- Check context.user is populated
- Verify auth middleware runs before GraphQL
- Use GraphQLError with appropriate code

**Subscription not receiving updates**
- Verify pubsub.publish() is called
- Check subscription filter matches
- Ensure WebSocket connection is established

## References

### Official Documentation
- [GraphQL Specification](https://spec.graphql.org/)
- [GraphQL.org](https://graphql.org/)
- [Apollo Server Documentation](https://www.apollographql.com/docs/apollo-server/)
- [GraphQL Tools](https://www.graphql-tools.com/)

### Tools & Libraries
- **Apollo Server** - GraphQL server for Node.js
- **GraphQL Yoga** - Fully-featured GraphQL server
- **DataLoader** - Batching and caching library
- **GraphQL Code Generator** - Generate types from schema
- **GraphQL Inspector** - Schema validation and comparison

### Related Skills
- `graphql-federation` - Multi-service GraphQL architectures
- `graphql-optimization` - Advanced performance patterns
- `graphql-security` - Authentication, authorization, and rate limiting
- `api-design` - General API design principles
- `lang-typescript-library-dev` - TypeScript for type-safe GraphQL

---

**When to use this skill**: Building GraphQL APIs, designing data graphs, implementing resolvers, consuming GraphQL endpoints, or learning GraphQL fundamentals.

**Skill maintenance**: Update when GraphQL specification changes, new directives are added, or best practices evolve.

Overview

This skill provides foundational GraphQL patterns for designing and implementing GraphQL APIs, covering schema design, operations, subscriptions, and resolver strategies. It is the entry point for building or consuming robust, maintainable GraphQL services. Use it to shape type systems, API contracts, and runtime behavior for production-grade graphs.

How this skill works

The skill explains and demonstrates core SDL constructs (types, enums, interfaces, unions, scalars, input types) and root operation types (Query, Mutation, Subscription). It describes common resolver patterns, variable usage, fragments, directives, and error-handling approaches so you can map data sources and enforce contracts. It also surfaces design best practices for pagination, filtering, polymorphism, and real-time events.

When to use it

  • Designing a new GraphQL schema for a service or product
  • Implementing resolvers that fetch and stitch data from multiple sources
  • Defining mutations, input types, and safe update/delete operations
  • Adding real-time subscriptions and event filters
  • Optimizing queries, avoiding N+1 problems, and planning pagination

Best practices

  • Model clear, versionable types and prefer non-null only where guaranteed to be present
  • Use input types for mutation inputs and separate create vs update inputs
  • Favor interfaces/unions for polymorphic fields and require inline fragments when querying them
  • Protect expensive fields with directives or auth checks and apply depth/rate limits
  • Return explicit error unions or structured error types instead of throwing opaque errors

Example use cases

  • Create an authoring API with User, Post, Comment types, and publish workflows via mutations and subscriptions
  • Build a search endpoint returning a SearchResult union for heterogeneous results
  • Implement cursor or offset pagination with CursorPaginationInput or PaginationInput for lists
  • Add @auth and @rateLimit directives to protect fields and apply per-field policies
  • Design a createUser mutation that returns either User or structured ValidationError union for predictable client behavior

FAQ

How do I handle partial failures in mutations?

Return a union type that includes success payloads and structured error types so clients can inspect field-level errors without losing context.

When should I use an interface vs a union?

Use interfaces when types share common fields you want to query directly. Use unions when types don’t share fields and clients must discriminate with inline fragments.