Marketplace

graphql-resolvers

GraphQL resolver patterns including dataloader for N+1 prevention, context propagation, authorization, error handling, and validation. Use when implementing GraphQL resolvers.

$ 安裝

git clone https://github.com/jovermier/cc-stack-marketplace /tmp/cc-stack-marketplace && cp -r /tmp/cc-stack-marketplace/plugins/cc-graphql/skills/graphql-resolvers ~/.claude/skills/cc-stack-marketplace

// tip: Run this command in your terminal to install the skill


name: graphql-resolvers description: GraphQL resolver patterns including dataloader for N+1 prevention, context propagation, authorization, error handling, and validation. Use when implementing GraphQL resolvers.

GraphQL Resolvers

Expert guidance for implementing efficient, secure GraphQL resolvers.

Quick Reference

ConcernSolutionPattern
N+1 queriesDataloaderBatch load relations
AuthenticationContext middlewareCheck before resolving
AuthorizationField-level checksUser can access this data
ValidationSchema layerInput validation before resolvers
Error handlingWrapped errorsDon't expose internal details
Context propagationPass through all levelscontext.Context to nested resolvers

What Do You Need?

  1. Dataloader - Batching relations to prevent N+1 queries
  2. Authorization - Checking access at field level
  3. Error handling - Proper GraphQL errors, no internal exposure
  4. Context - Propagating user, request-scoped data
  5. Validation - Schema-level validation approach

Specify a number or describe your resolver scenario.

Routing

ResponseReference to Read
1, "dataloader", "n+1", "batch", "relation"dataloader.md
2, "auth", "authorization", "access", "permission"authorization.md
3, "error", "wrapped", "internal"errors.md
4, "context", "user", "request"context.md
5, "validation", "input", "schema"validation.md

Critical Rules

  • Always use dataloader for relations: Prevents N+1 queries
  • Authorize at resolver level: Check user can access the data
  • Never expose internal errors: Wrap before returning
  • Propagate context through resolver chain: All nested resolvers need it
  • Validate at schema layer: Use input validation, not in resolvers
  • No circular dependencies: Be aware of resolver chains

Dataloader Pattern

// Bad: N+1 query pattern
func (r *queryResolver) Users(ctx context.Context) ([]*User, error) {
    users, _ := r.db.Users()  // 1 query
    for _, user := range users {
        posts, _ := r.db.PostsByUser(user.ID)  // N queries!
        user.Posts = posts
    }
    return users, nil
}

// Good: Using dataloader
func (r *queryResolver) Users(ctx context.Context) ([]*User, error) {
    users, err := r.db.Users()
    if err != nil {
        return nil, err
    }

    // Batch load posts using dataloader
    loaders := dataloader.For(ctx)
    for _, user := range users {
        user.Posts, err = loaders.PostsByUser.Load(user.ID)
        if err != nil {
            return nil, err
        }
    }

    return users, nil
}

Authorization Pattern

// Good: Authorization check in resolver
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
    // Check authentication
    viewer := auth.FromContext(ctx)
    if viewer == nil {
        return nil, fmt.Errorf("authentication required")
    }

    // Fetch user
    user, err := r.db.FindUser(id)
    if err != nil {
        return nil, err
    }

    // Check authorization (users can view own profile, admins can view any)
    if user.ID != viewer.ID && !viewer.IsAdmin {
        return nil, fmt.Errorf("access denied")
    }

    return user, nil
}

Error Handling Pattern

// Bad: Exposing internal errors
func (r *mutationResolver) CreateUser(ctx context.Context, input CreateUserInput) (*CreateUserPayload, error) {
    if err := r.db.CreateUser(input); err != nil {
        return nil, fmt.Errorf("database error: %v", err)  // Leaks DB details!
    }
    // ...
}

// Good: Wrapped errors
func (r *mutationResolver) CreateUser(ctx context.Context, input CreateUserInput) (*CreateUserPayload, error) {
    if err := r.db.CreateUser(input); err != nil {
        if errors.Is(err, db.ErrDuplicate) {
            return &CreateUserPayload{
                Errors: []UserError{{
                    Field:   []string{"email"},
                    Message: "Email already exists",
                }},
            }, nil
        }
        return nil, fmt.Errorf("failed to create user")
    }
    // ...
}

Common Resolver Issues

IssueSeverityImpactFix
N+1 queriesCriticalDatabase overload, slowUse dataloader
Missing authorizationCriticalData exposureAdd auth checks
Exposing internal errorsHighInformation disclosureWrap errors
Not propagating contextHighBreaks auth, timeoutPass ctx through
No validationMediumBad data in DBValidate at schema
Circular resolver dependenciesHighInfinite loopsRestructure schema

Reference Index

FileTopics
dataloader.mdBatching, caching, implementation
authorization.mdAuth checks, role-based access
errors.mdError wrapping, field errors
context.mdPropagation, request-scoped data
validation.mdSchema validation, input types

Success Criteria

Resolvers are correct when:

  • Dataloader used for all relations (no N+1 queries)
  • Authorization checked before data access
  • Internal errors wrapped, not exposed
  • Context propagated through resolver chain
  • Validation happens at schema layer
  • No circular dependencies in resolver chains
  • Field-level authorization for sensitive data