graphql-resolvers
GraphQL resolver patterns including dataloader for N+1 prevention, context propagation, authorization, error handling, and validation. Use when implementing GraphQL resolvers.
When & Why to Use This Skill
This Claude skill provides expert guidance and best practices for implementing efficient, secure, and scalable GraphQL resolvers. It focuses on solving critical API development challenges such as N+1 query performance issues using the Dataloader pattern, implementing robust field-level authorization, and ensuring secure error handling and context propagation.
Use Cases
- Eliminating N+1 query performance bottlenecks in complex GraphQL schemas by implementing the Dataloader batching pattern.
- Securing sensitive API data by applying granular, field-level authorization checks within the resolver layer.
- Improving API security and developer experience by implementing standardized error wrapping that prevents internal system details from leaking to the client.
- Ensuring consistent request-scoped data availability by correctly propagating context through nested resolver chains.
- Streamlining API development with proven patterns for input validation and schema-level data integrity.
| 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
| Concern | Solution | Pattern |
|---|---|---|
| N+1 queries | Dataloader | Batch load relations |
| Authentication | Context middleware | Check before resolving |
| Authorization | Field-level checks | User can access this data |
| Validation | Schema layer | Input validation before resolvers |
| Error handling | Wrapped errors | Don't expose internal details |
| Context propagation | Pass through all levels | context.Context to nested resolvers |
What Do You Need?
- Dataloader - Batching relations to prevent N+1 queries
- Authorization - Checking access at field level
- Error handling - Proper GraphQL errors, no internal exposure
- Context - Propagating user, request-scoped data
- Validation - Schema-level validation approach
Specify a number or describe your resolver scenario.
Routing
| Response | Reference 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
| Issue | Severity | Impact | Fix |
|---|---|---|---|
| N+1 queries | Critical | Database overload, slow | Use dataloader |
| Missing authorization | Critical | Data exposure | Add auth checks |
| Exposing internal errors | High | Information disclosure | Wrap errors |
| Not propagating context | High | Breaks auth, timeout | Pass ctx through |
| No validation | Medium | Bad data in DB | Validate at schema |
| Circular resolver dependencies | High | Infinite loops | Restructure schema |
Reference Index
| File | Topics |
|---|---|
| dataloader.md | Batching, caching, implementation |
| authorization.md | Auth checks, role-based access |
| errors.md | Error wrapping, field errors |
| context.md | Propagation, request-scoped data |
| validation.md | Schema 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