graphql

SKILL.md

GraphQL Development

GraphQL API design, implementation, and best practices.

Schema Design

Type Definitions

# Scalar types
type User {
    id: ID!
    email: String!
    name: String
    age: Int
    balance: Float
    isActive: Boolean!
    createdAt: DateTime!  # Custom scalar
}

# Enum types
enum UserRole {
    ADMIN
    EDITOR
    USER
}

enum OrderStatus {
    PENDING
    PROCESSING
    SHIPPED
    DELIVERED
    CANCELLED
}

# Input types (for mutations)
input CreateUserInput {
    email: String!
    name: String!
    password: String!
    role: UserRole = USER
}

input UpdateUserInput {
    name: String
    email: String
}

# Interface
interface Node {
    id: ID!
}

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

# Union types
union SearchResult = User | Post | Comment

Relationships

type User {
    id: ID!
    email: String!
    posts: [Post!]!                    # One-to-many
    profile: Profile                   # One-to-one (nullable)
    followers: [User!]!                # Self-referential
    following: [User!]!
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User!                      # Many-to-one
    tags: [Tag!]!                      # Many-to-many
    comments(first: Int, after: String): CommentConnection!
}

# Connection pattern for pagination
type CommentConnection {
    edges: [CommentEdge!]!
    pageInfo: PageInfo!
    totalCount: Int!
}

type CommentEdge {
    cursor: String!
    node: Comment!
}

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

Queries & Mutations

Query Types

type Query {
    # Single item
    user(id: ID!): User
    userByEmail(email: String!): User

    # Lists with filtering
    users(
        filter: UserFilter
        orderBy: UserOrderBy
        first: Int
        after: String
    ): UserConnection!

    # Search
    search(query: String!, types: [SearchType!]): [SearchResult!]!

    # Current user
    me: User
}

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

input UserOrderBy {
    field: UserSortField!
    direction: SortDirection!
}

enum UserSortField {
    CREATED_AT
    NAME
    EMAIL
}

enum SortDirection {
    ASC
    DESC
}

Mutation Types

type Mutation {
    # Create
    createUser(input: CreateUserInput!): CreateUserPayload!

    # Update
    updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!

    # Delete
    deleteUser(id: ID!): DeleteUserPayload!

    # Authentication
    login(email: String!, password: String!): AuthPayload!
    logout: Boolean!
    refreshToken(token: String!): AuthPayload!
}

# Payload pattern (recommended)
type CreateUserPayload {
    user: User
    errors: [Error!]
}

type Error {
    field: String
    message: String!
    code: ErrorCode!
}

enum ErrorCode {
    VALIDATION_ERROR
    NOT_FOUND
    UNAUTHORIZED
    FORBIDDEN
}

Subscriptions

type Subscription {
    # Real-time updates
    postCreated: Post!
    commentAdded(postId: ID!): Comment!
    userStatusChanged(userId: ID!): User!

    # With filtering
    messageReceived(roomId: ID!): Message!
}

Apollo Server (Node.js)

Setup

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';

const typeDefs = `#graphql
    type Query {
        users: [User!]!
        user(id: ID!): User
    }

    type User {
        id: ID!
        email: String!
        posts: [Post!]!
    }
`;

const resolvers = {
    Query: {
        users: async (_, __, { dataSources }) => {
            return dataSources.userAPI.getUsers();
        },
        user: async (_, { id }, { dataSources }) => {
            return dataSources.userAPI.getUser(id);
        },
    },
    User: {
        posts: async (parent, _, { dataSources }) => {
            return dataSources.postAPI.getPostsByAuthor(parent.id);
        },
    },
};

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

const app = express();
await server.start();

app.use(
    '/graphql',
    express.json(),
    expressMiddleware(server, {
        context: async ({ req }) => ({
            token: req.headers.authorization,
            dataSources: {
                userAPI: new UserAPI(),
                postAPI: new PostAPI(),
            },
        }),
    })
);

Resolvers

const resolvers = {
    Query: {
        // Arguments: parent, args, context, info
        user: async (_, { id }, { dataSources, user }) => {
            return dataSources.userAPI.getUser(id);
        },

        users: async (_, { filter, first, after }, { dataSources }) => {
            const users = await dataSources.userAPI.getUsers({
                filter,
                limit: first,
                cursor: after,
            });
            return formatConnection(users);
        },
    },

    Mutation: {
        createUser: async (_, { input }, { dataSources }) => {
            try {
                const user = await dataSources.userAPI.create(input);
                return { user, errors: null };
            } catch (error) {
                return {
                    user: null,
                    errors: [{ message: error.message, code: 'VALIDATION_ERROR' }],
                };
            }
        },
    },

    // Field-level resolvers
    User: {
        fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
        posts: async (parent, { first }, { dataSources }) => {
            return dataSources.postAPI.getByAuthor(parent.id, { limit: first });
        },
    },

    // Custom scalars
    DateTime: new GraphQLScalarType({
        name: 'DateTime',
        parseValue: (value) => new Date(value),
        serialize: (value) => value.toISOString(),
    }),
};

DataLoader (N+1 Prevention)

import DataLoader from 'dataloader';

// Create loader
const userLoader = new DataLoader(async (userIds) => {
    const users = await db.users.findMany({
        where: { id: { in: userIds } },
    });
    // Return in same order as input
    return userIds.map((id) => users.find((u) => u.id === id));
});

// In resolver
const resolvers = {
    Post: {
        author: (parent, _, { loaders }) => {
            return loaders.userLoader.load(parent.authorId);
        },
    },
};

// Context setup
const context = ({ req }) => ({
    loaders: {
        userLoader: new DataLoader(batchUsers),
    },
});

Authentication & Authorization

Context-based Auth

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: async ({ req }) => {
        const token = req.headers.authorization?.replace('Bearer ', '');
        let user = null;

        if (token) {
            try {
                user = await verifyToken(token);
            } catch (e) {
                // Invalid token, user stays null
            }
        }

        return { user };
    },
});

// In resolver
const resolvers = {
    Query: {
        me: (_, __, { user }) => {
            if (!user) throw new AuthenticationError('Not authenticated');
            return user;
        },
    },
};

Directive-based Auth

directive @auth(requires: Role = USER) on FIELD_DEFINITION

type Query {
    publicPosts: [Post!]!
    myPosts: [Post!]! @auth
    allUsers: [User!]! @auth(requires: ADMIN)
}
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

function authDirective(directiveName) {
    return {
        authDirectiveTransformer: (schema) =>
            mapSchema(schema, {
                [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
                    const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
                    if (directive) {
                        const { resolve = defaultFieldResolver } = fieldConfig;
                        fieldConfig.resolve = async function (source, args, context, info) {
                            if (!context.user) {
                                throw new AuthenticationError('Not authenticated');
                            }
                            const requiredRole = directive.requires;
                            if (requiredRole && context.user.role !== requiredRole) {
                                throw new ForbiddenError('Not authorized');
                            }
                            return resolve(source, args, context, info);
                        };
                    }
                    return fieldConfig;
                },
            }),
    };
}

Error Handling

import { GraphQLError } from 'graphql';

// Custom errors
class NotFoundError extends GraphQLError {
    constructor(message) {
        super(message, {
            extensions: {
                code: 'NOT_FOUND',
                http: { status: 404 },
            },
        });
    }
}

class ValidationError extends GraphQLError {
    constructor(errors) {
        super('Validation failed', {
            extensions: {
                code: 'VALIDATION_ERROR',
                validationErrors: errors,
                http: { status: 400 },
            },
        });
    }
}

// Usage in resolver
const resolvers = {
    Query: {
        user: async (_, { id }) => {
            const user = await db.users.findUnique({ where: { id } });
            if (!user) {
                throw new NotFoundError(`User ${id} not found`);
            }
            return user;
        },
    },
};

Performance

Query Complexity

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

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [
        createComplexityLimitRule(1000, {
            scalarCost: 1,
            objectCost: 10,
            listFactor: 20,
        }),
    ],
});

Depth Limiting

import depthLimit from 'graphql-depth-limit';

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

Caching

type Query {
    user(id: ID!): User @cacheControl(maxAge: 60)
    posts: [Post!]! @cacheControl(maxAge: 30, scope: PUBLIC)
}

type User @cacheControl(maxAge: 120) {
    id: ID!
    email: String! @cacheControl(maxAge: 0, scope: PRIVATE)
}

Testing

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

describe('User Queries', () => {
    let server;

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

    it('should return user by id', async () => {
        const response = await server.executeOperation({
            query: `
                query GetUser($id: ID!) {
                    user(id: $id) {
                        id
                        email
                    }
                }
            `,
            variables: { id: '1' },
        });

        expect(response.body.singleResult.errors).toBeUndefined();
        expect(response.body.singleResult.data?.user).toEqual({
            id: '1',
            email: 'test@example.com',
        });
    });
});
Weekly Installs
1
Installed on
windsurf1
opencode1
codex1
claude-code1
antigravity1
gemini-cli1