Skip to content
Back to skills

1223 Graphql Patterns 889a4ecc

ASecurity

Advanced GraphQL schema design, resolver optimization, subscriptions, federation, and production best practices.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
testingtypescriptgonodeexpresstestinggitdatabasesecurityperformancedocumentation

Works with

  • cursor
  • cli

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1223-graphql-patterns_889a4ecc --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1223 Graphql Patterns 889a4ecc?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1223 Graphql Patterns 889a4ecc
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1223-graphql-patterns-889a4ecc/badge)](https://www.skillsdirectory.com/skills/tools-only-1223-graphql-patterns-889a4ecc)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# GraphQL Patterns - Deep Dive

Advanced GraphQL schema design, resolver optimization, subscriptions, federation, and production best practices.

## Schema Design Best Practices

### Type System Fundamentals

**Scalar Types**:
```graphql
type User {
  id: ID!              # Unique identifier
  email: String!       # Required string
  age: Int             # Optional integer
  balance: Float       # Optional float
  isActive: Boolean!   # Required boolean
}
```

**Custom Scalars**:
```graphql
scalar DateTime
scalar URL
scalar EmailAddress
scalar JSON

type Post {
  id: ID!
  publishedAt: DateTime!
  website: URL
  content: JSON
}
```

**Implementation** (GraphQL Scalars library):
```typescript
import { DateTimeResolver, URLResolver, EmailAddressResolver, JSONResolver } from 'graphql-scalars';

const resolvers = {
  DateTime: DateTimeResolver,
  URL: URLResolver,
  EmailAddress: EmailAddressResolver,
  JSON: JSONResolver,
};
```

### Object Types and Interfaces

**Interface** (shared fields):
```graphql
interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

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

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

**Query interface implementations**:
```graphql
query {
  node(id: "123") {
    id
    ... on User {
      email
      name
    }
    ... on Post {
      title
      author { name }
    }
  }
}
```

### Union Types

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

type Query {
  search(query: String!): [SearchResult!]!
}
```

**Query with fragments**:
```graphql
query {
  search(query: "graphql") {
    ... on User {
      id
      name
      email
    }
    ... on Post {
      id
      title
      content
    }
    ... on Comment {
      id
      text
      author { name }
    }
  }
}
```

### Enums

```graphql
enum UserRole {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

type User {
  id: ID!
  role: UserRole!
  orders: [Order!]!
}

type Order {
  id: ID!
  status: OrderStatus!
}
```

### Input Types

```graphql
input CreateUserInput {
  email: String!
  name: String
  role: UserRole = USER  # Default value
}

input UpdateUserInput {
  email: String
  name: String
  role: UserRole
}

input UserFilterInput {
  role: UserRole
  isActive: Boolean
  createdAfter: DateTime
}

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

type Query {
  users(filter: UserFilterInput): [User!]!
}
```

### Nullable vs Non-Null Fields

**Design philosophy**: Nullable by default, non-null where guaranteed

✅ **Good: Defensive nullability**
```graphql
type User {
  id: ID!                 # Always present
  email: String!          # Required, validated
  name: String            # Optional (nullable)
  profile: Profile        # May not exist
  posts: [Post!]!         # Array never null, posts never null
  # Can return empty array []
}
```

❌ **Bad: Over-promising with non-null**
```graphql
type User {
  id: ID!
  email: String!
  lastLoginAt: DateTime!  # What if never logged in?
  favoritePost: Post!     # What if no favorite?
  # Breaking change if needs to be nullable later
}
```

**Nullability rules**:
- `String`: Nullable string
- `String!`: Non-null string
- `[String]`: Nullable array of nullable strings
- `[String!]`: Nullable array of non-null strings
- `[String!]!`: Non-null array of non-null strings
- `[String]!`: Non-null array of nullable strings

## Resolver Patterns

### Basic Resolvers

```typescript
const resolvers = {
  Query: {
    user: async (_parent, { id }, context) => {
      return context.db.users.findUnique({ where: { id } });
    },
    users: async (_parent, { filter }, context) => {
      return context.db.users.findMany({ where: filter });
    },
  },

  User: {
    // Field resolver (computed field)
    fullName: (user) => `${user.firstName} ${user.lastName}`,

    // Async field resolver (database fetch)
    posts: async (user, _args, context) => {
      return context.db.posts.findMany({ where: { authorId: user.id } });
    },
  },

  Mutation: {
    createUser: async (_parent, { input }, context) => {
      return context.db.users.create({ data: input });
    },
  },
};
```

### DataLoader (N+1 Solution)

**Problem**: N+1 query pattern
```typescript
// BAD: Triggers separate query for each user's posts
const resolvers = {
  User: {
    posts: async (user, _args, context) => {
      // Called once PER user in result set
      return context.db.posts.findMany({ where: { authorId: user.id } });
    },
  },
};

// Query for 100 users = 1 query + 100 queries for posts = 101 queries!
```

**Solution**: DataLoader batches requests
```typescript
import DataLoader from 'dataloader';

// Create loader in context (per-request)
const createLoaders = (db) => ({
  postsLoader: new DataLoader(async (userIds: string[]) => {
    // Single query for all users
    const posts = await db.posts.findMany({
      where: { authorId: { in: userIds } },
    });

    // Group by userId
    const postsByUser = userIds.map(userId =>
      posts.filter(post => post.authorId === userId)
    );

    return postsByUser;
  }),

  userLoader: new DataLoader(async (userIds: string[]) => {
    const users = await db.users.findMany({
      where: { id: { in: userIds } },
    });

    // Maintain order matching userIds
    return userIds.map(id => users.find(user => user.id === id));
  }),
});

// Context setup
const context = ({ req }) => ({
  db: prisma,
  loaders: createLoaders(prisma),
  userId: req.userId,
});

// Resolver using DataLoader
const resolvers = {
  User: {
    posts: (user, _args, context) => {
      return context.loaders.postsLoader.load(user.id);
    },
  },
  Post: {
    author: (post, _args, context) => {
      return context.loaders.userLoader.load(post.authorId);
    },
  },
};

// Query for 100 users = 1 query + 1 batched query for posts = 2 queries!
```

### Resolver Chain and Parent

```typescript
const resolvers = {
  Query: {
    user: async (_parent, { id }, context) => {
      // Returns user object passed to User resolvers
      return context.db.users.findUnique({ where: { id } });
    },
  },

  User: {
    // parent is the user object from Query.user
    fullName: (parent) => `${parent.firstName} ${parent.lastName}`,

    // Can access parent fields
    posts: async (parent, _args, context) => {
      return context.db.posts.findMany({
        where: { authorId: parent.id },
      });
    },

    // Nested resolver chain
    profile: async (parent, _args, context) => {
      // Returns profile object passed to Profile resolvers
      return context.db.profiles.findUnique({
        where: { userId: parent.id },
      });
    },
  },

  Profile: {
    // parent is the profile object from User.profile
    avatarUrl: (parent) => {
      return parent.avatar
        ? `https://cdn.example.com/${parent.avatar}`
        : 'https://cdn.example.com/default-avatar.png';
    },
  },
};
```

### Error Handling

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

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

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

      return user;
    },
  },

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

      try {
        return await context.db.users.create({ data: input });
      } catch (error) {
        // Database unique constraint
        if (error.code === 'P2002') {
          throw new GraphQLError('Email already exists', {
            extensions: {
              code: 'DUPLICATE_EMAIL',
              field: 'email',
            },
          });
        }

        // Unexpected error
        throw new GraphQLError('Failed to create user', {
          extensions: {
            code: 'INTERNAL_ERROR',
          },
        });
      }
    },
  },
};
```

**Error response**:
```json
{
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND",
        "argumentName": "id"
      }
    }
  ],
  "data": {
    "user": null
  }
}
```

## Pagination Patterns

### Offset Pagination (Simple)

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

type UserConnection {
  nodes: [User!]!
  totalCount: Int!
  pageInfo: PageInfo!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}
```

**Resolver**:
```typescript
const resolvers = {
  Query: {
    users: async (_parent, { limit, offset }, context) => {
      const [nodes, totalCount] = await Promise.all([
        context.db.users.findMany({ take: limit, skip: offset }),
        context.db.users.count(),
      ]);

      return {
        nodes,
        totalCount,
        pageInfo: {
          hasNextPage: offset + limit < totalCount,
          hasPreviousPage: offset > 0,
        },
      };
    },
  },
};
```

### Cursor Pagination (Relay Connection)

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

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

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

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

**Resolver** (using graphql-relay):
```typescript
import { connectionFromArraySlice, cursorToOffset } from 'graphql-relay';

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

      // Decode cursors to offsets
      const afterOffset = after ? cursorToOffset(after) + 1 : 0;
      const beforeOffset = before ? cursorToOffset(before) : undefined;

      // Calculate limit and offset
      const limit = first || last || 10;
      const offset = afterOffset;

      // Fetch data
      const [users, totalCount] = await Promise.all([
        context.db.users.findMany({
          take: limit + 1, // Fetch one extra to check hasNextPage
          skip: offset,
        }),
        context.db.users.count(),
      ]);

      // Build connection
      const hasNextPage = users.length > limit;
      const nodes = hasNextPage ? users.slice(0, -1) : users;

      return connectionFromArraySlice(nodes, args, {
        sliceStart: offset,
        arrayLength: totalCount,
      });
    },
  },
};
```

**Query**:
```graphql
query {
  users(first: 10, after: "cursor123") {
    edges {
      cursor
      node {
        id
        name
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

## Mutations

### Input Object Pattern

✅ **Good: Single input object**
```graphql
input CreatePostInput {
  title: String!
  content: String!
  tags: [String!]
  publishedAt: DateTime
}

type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}
```

❌ **Bad: Multiple arguments**
```graphql
type Mutation {
  createPost(
    title: String!
    content: String!
    tags: [String!]
    publishedAt: DateTime
  ): Post!
}
```

### Payload Object Pattern

**Include user errors and edge for optimistic updates**:
```graphql
type CreatePostPayload {
  post: Post
  postEdge: PostEdge
  errors: [UserError!]
  clientMutationId: String
}

type UserError {
  message: String!
  field: String
  code: String!
}

type PostEdge {
  cursor: String!
  node: Post!
}

type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}
```

**Resolver**:
```typescript
const resolvers = {
  Mutation: {
    createPost: async (_parent, { input }, context) => {
      // Validation
      const errors = [];
      if (input.title.length < 3) {
        errors.push({
          message: 'Title must be at least 3 characters',
          field: 'title',
          code: 'TITLE_TOO_SHORT',
        });
      }

      if (errors.length > 0) {
        return { post: null, postEdge: null, errors };
      }

      // Create post
      const post = await context.db.posts.create({
        data: {
          ...input,
          authorId: context.userId,
        },
      });

      return {
        post,
        postEdge: {
          cursor: encodeCursor(post.id),
          node: post,
        },
        errors: [],
      };
    },
  },
};
```

### Optimistic Updates (Client)

```typescript
const [createPost] = useMutation(CREATE_POST, {
  optimisticResponse: {
    createPost: {
      __typename: 'CreatePostPayload',
      post: {
        __typename: 'Post',
        id: 'temp-id',
        title: variables.input.title,
        content: variables.input.content,
        createdAt: new Date().toISOString(),
      },
      errors: [],
    },
  },
  update: (cache, { data }) => {
    // Update cache with new post
    const existing = cache.readQuery({ query: GET_POSTS });
    cache.writeQuery({
      query: GET_POSTS,
      data: {
        posts: {
          ...existing.posts,
          edges: [
            data.createPost.postEdge,
            ...existing.posts.edges,
          ],
        },
      },
    });
  },
});
```

## Subscriptions (Real-Time)

### Schema

```graphql
type Subscription {
  postAdded: Post!
  postUpdated(id: ID!): Post!
  commentAdded(postId: ID!): Comment!
  userStatusChanged(userId: ID!): UserStatus!
}

type UserStatus {
  userId: ID!
  isOnline: Boolean!
  lastSeen: DateTime
}
```

### Resolver (with PubSub)

```typescript
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const resolvers = {
  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator(['POST_ADDED']),
    },

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

    commentAdded: {
      subscribe: (_parent, { postId }, context) => {
        // Auth check
        if (!context.userId) {
          throw new GraphQLError('Unauthorized');
        }
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
      },
      // Optional resolve function
      resolve: (payload) => payload.comment,
    },
  },

  Mutation: {
    createPost: async (_parent, { input }, context) => {
      const post = await context.db.posts.create({ data: input });

      // Trigger subscription
      pubsub.publish('POST_ADDED', { postAdded: post });

      return { post };
    },

    updatePost: async (_parent, { id, input }, context) => {
      const post = await context.db.posts.update({
        where: { id },
        data: input,
      });

      pubsub.publish(`POST_UPDATED_${id}`, { postUpdated: post });

      return { post };
    },
  },
};
```

### Redis PubSub (Production)

```typescript
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  retryStrategy: (times) => Math.min(times * 50, 2000),
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});
```

### WebSocket Setup (Apollo Server)

```typescript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';

const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = createServer(app);

// WebSocket server
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

const serverCleanup = useServer({ schema }, wsServer);

// Apollo Server
const server = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose();
          },
        };
      },
    },
  ],
});

await server.start();

app.use('/graphql', express.json(), expressMiddleware(server));

httpServer.listen(4000);
```

### Client Subscription (Apollo Client)

```typescript
import { useSubscription } from '@apollo/client';

const POST_ADDED = gql`
  subscription OnPostAdded {
    postAdded {
      id
      title
      author { name }
    }
  }
`;

function RecentPosts() {
  const { data, loading } = useSubscription(POST_ADDED, {
    onData: ({ client, data }) => {
      // Update cache
      client.cache.modify({
        fields: {
          posts: (existing) => ({
            ...existing,
            edges: [
              { node: data.postAdded, cursor: '' },
              ...existing.edges,
            ],
          }),
        },
      });
    },
  });

  return <div>New post: {data?.postAdded.title}</div>;
}
```

## Directives

### Built-in Directives

```graphql
query GetUser($includeEmail: Boolean!, $skipProfile: Boolean!) {
  user(id: "123") {
    id
    name
    email @include(if: $includeEmail)
    profile @skip(if: $skipProfile) {
      bio
    }
  }
}
```

### Custom Directives

**Schema**:
```graphql
directive @auth(requires: UserRole!) on FIELD_DEFINITION
directive @deprecated(reason: String) on FIELD_DEFINITION
directive @length(min: Int, max: Int) on INPUT_FIELD_DEFINITION

type Query {
  users: [User!]! @auth(requires: ADMIN)
  me: User!
}

type User {
  id: ID!
  email: String! @deprecated(reason: "Use contactEmail instead")
  contactEmail: String!
}

input CreateUserInput {
  name: String! @length(min: 3, max: 50)
  email: String!
}
```

**Implementation** (using graphql-tools):
```typescript
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

function authDirective(schema, directiveName) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];

      if (authDirective) {
        const { requires } = authDirective;
        const { resolve = defaultFieldResolver } = fieldConfig;

        fieldConfig.resolve = async (source, args, context, info) => {
          if (!context.user || context.user.role !== requires) {
            throw new GraphQLError('Unauthorized', {
              extensions: { code: 'FORBIDDEN' },
            });
          }

          return resolve(source, args, context, info);
        };
      }

      return fieldConfig;
    },
  });
}

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = authDirective(schema, 'auth');
```

## Performance Optimization

### Query Complexity Analysis

```typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  schema,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 10,
      listFactor: 10,
      introspectionListFactor: 10,
      onCost: (cost) => {
        console.log('Query cost:', cost);
      },
    }),
  ],
});
```

**Custom cost per field**:
```typescript
const typeDefs = gql`
  type Query {
    users: [User!]! @cost(complexity: 100)
    expensiveAnalytics: Analytics! @cost(complexity: 500)
  }
`;
```

### Query Depth Limiting

```typescript
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(10)],
});
```

### Persisted Queries

**Benefits**: Reduce payload size, prevent arbitrary queries in production

```typescript
import { ApolloServer } from '@apollo/server';

const server = new ApolloServer({
  schema,
  persistedQueries: {
    cache: new Map(), // Use Redis in production
  },
  allowBatchedHttpRequests: false,
  introspection: process.env.NODE_ENV !== 'production',
});
```

**Client sends hash**:
```http
POST /graphql
{
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "abc123..."
    }
  }
}
```

### Response Caching

**HTTP caching**:
```typescript
import responseCachePlugin from '@apollo/server-plugin-response-cache';

const server = new ApolloServer({
  schema,
  plugins: [
    responseCachePlugin({
      sessionId: (context) => context.user?.id || null,
    }),
  ],
});
```

**Cache hints**:
```graphql
type Query {
  user(id: ID!): User @cacheControl(maxAge: 60, scope: PRIVATE)
  publicPosts: [Post!]! @cacheControl(maxAge: 300, scope: PUBLIC)
}
```

## Schema Stitching and Federation

### Apollo Federation

**Service 1 (Users)**:
```graphql
type User @key(fields: "id") {
  id: ID!
  email: String!
  name: String
}

extend type Query {
  user(id: ID!): User
}
```

**Service 2 (Posts)**:
```graphql
type Post @key(fields: "id") {
  id: ID!
  title: String!
  author: User!
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post!]!
}

extend type Query {
  posts: [Post!]!
}
```

**Gateway**:
```typescript
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { ApolloServer } from '@apollo/server';

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'posts', url: 'http://localhost:4002/graphql' },
    ],
  }),
});

const server = new ApolloServer({ gateway });
```

## Testing

### Unit Testing Resolvers

```typescript
import { resolvers } from './resolvers';

describe('User Resolvers', () => {
  it('fetches user by ID', async () => {
    const mockDb = {
      users: {
        findUnique: jest.fn().mockResolvedValue({
          id: '123',
          email: 'test@example.com',
        }),
      },
    };

    const result = await resolvers.Query.user(
      {},
      { id: '123' },
      { db: mockDb }
    );

    expect(mockDb.users.findUnique).toHaveBeenCalledWith({
      where: { id: '123' },
    });
    expect(result).toEqual({
      id: '123',
      email: 'test@example.com',
    });
  });
});
```

### Integration Testing

```typescript
import { ApolloServer } from '@apollo/server';

const server = new ApolloServer({ typeDefs, resolvers });

it('creates a user', async () => {
  const response = await server.executeOperation({
    query: `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          user {
            id
            email
          }
          errors {
            message
          }
        }
      }
    `,
    variables: {
      input: {
        email: 'test@example.com',
        name: 'Test User',
      },
    },
  });

  expect(response.body.kind).toBe('single');
  expect(response.body.singleResult.errors).toBeUndefined();
  expect(response.body.singleResult.data?.createUser.user).toHaveProperty('id');
});
```

## Best Practices Summary

✅ **Nullable by default**: Only use non-null (`!`) when guaranteed
✅ **Use DataLoader**: Batch queries to prevent N+1
✅ **Pagination**: Use cursor-based for large lists
✅ **Input objects**: Group mutation arguments
✅ **Payload objects**: Return errors with data
✅ **Custom scalars**: Use DateTime, Email, URL, JSON
✅ **Interfaces**: Share common fields across types
✅ **Query complexity**: Limit expensive queries
✅ **Persisted queries**: Reduce payload, improve security
✅ **Error handling**: Return specific error codes and fields

❌ **Avoid over-fetching**: Let clients request exact fields
❌ **Don't expose internal IDs**: Use opaque IDs or UUIDs
❌ **Don't ignore N+1**: Always use DataLoader for relationships
❌ **Don't make everything non-null**: Breaks schema evolution
❌ **Don't use query strings for mutations**: Use input objects
❌ **Don't skip authorization**: Check permissions in resolvers

## Additional Resources

- [GraphQL Official Documentation](https://graphql.org/learn/)
- [Apollo Server Documentation](https://www.apollographql.com/docs/apollo-server/)
- [GraphQL Best Practices](https://graphql.org/learn/best-practices/)
- [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
- [Apollo Federation](https://www.apollographql.com/docs/federation/)
- [DataLoader Documentation](https://github.com/graphql/dataloader)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.