GraphQL Extension

Type-safe GraphQL APIs with automatic schema generation and full observability

GraphQL Extension

The GraphQL extension provides a production-ready GraphQL server powered by gqlgen, offering type-safe GraphQL APIs with automatic code generation, schema management, and comprehensive observability support.

Features

Type-Safe GraphQL

  • Automatic Code Generation: Generate resolvers and types from GraphQL schemas
  • Schema-First Development: Define your API with GraphQL Schema Definition Language
  • Type Safety: Compile-time type checking for resolvers and operations
  • Schema Validation: Automatic validation of GraphQL schemas and operations

Multiple Transports

  • HTTP Support: GET and POST requests for queries and mutations
  • WebSocket Subscriptions: Real-time data streaming (future enhancement)
  • File Uploads: Multipart form data support for file operations
  • Batch Queries: Execute multiple operations in a single request

Performance Optimization

  • Query Complexity Analysis: Prevent expensive queries with complexity limits
  • DataLoader Support: N+1 query optimization with batching and caching
  • Automatic Persisted Queries (APQ): Cache frequently used queries
  • Query Depth Limiting: Prevent deeply nested query attacks

Advanced Features

  • Custom Directives: Authentication, authorization, and custom logic
  • Apollo Federation v2: Microservices composition and schema stitching
  • GraphQL Playground: Interactive query exploration and testing
  • Introspection: Schema exploration and documentation
  • CORS Support: Cross-origin resource sharing configuration

Observability & Security

  • Full Observability: Logging, metrics, and tracing integration
  • Slow Query Detection: Monitor and log slow-performing queries
  • Error Handling: Structured error responses with proper HTTP status codes
  • Request Validation: Input validation and sanitization

Installation

Go Module

go get github.com/xraph/forge/extensions/graphql
go get github.com/99designs/gqlgen@v0.17.45

Docker

FROM xraph/forge:latest
# GraphQL extension is included

Package Manager

# Using Forge CLI
forge extension add graphql

# Using package manager
npm install @xraph/forge-graphql

Configuration

YAML Configuration

extensions:
  graphql:
    # Server settings
    endpoint: "/api/graphql"
    playground_endpoint: "/api/playground"
    enable_playground: true
    enable_introspection: true
    
    # Schema
    auto_generate_schema: true
    schema_file: "schema/schema.graphql"
    
    # Performance
    max_complexity: 1000
    max_depth: 15
    query_timeout: "30s"
    
    # DataLoader
    enable_dataloader: true
    dataloader_batch_size: 100
    dataloader_wait: "10ms"
    
    # Caching
    enable_query_cache: true
    query_cache_ttl: "5m"
    max_cache_size: 1000
    
    # Security
    enable_cors: true
    allowed_origins:
      - "https://example.com"
      - "https://app.example.com"
    max_upload_size: 10485760  # 10MB
    
    # Observability
    enable_metrics: true
    enable_tracing: true
    enable_logging: true
    log_slow_queries: true
    slow_query_threshold: "1s"

Environment Variables

# Server Configuration
FORGE_GRAPHQL_ENDPOINT=/api/graphql
FORGE_GRAPHQL_PLAYGROUND_ENDPOINT=/api/playground
FORGE_GRAPHQL_ENABLE_PLAYGROUND=true
FORGE_GRAPHQL_ENABLE_INTROSPECTION=true

# Performance Configuration
FORGE_GRAPHQL_MAX_COMPLEXITY=1000
FORGE_GRAPHQL_MAX_DEPTH=15
FORGE_GRAPHQL_QUERY_TIMEOUT=30s

# DataLoader Configuration
FORGE_GRAPHQL_ENABLE_DATALOADER=true
FORGE_GRAPHQL_DATALOADER_BATCH_SIZE=100
FORGE_GRAPHQL_DATALOADER_WAIT=10ms

# Caching Configuration
FORGE_GRAPHQL_ENABLE_QUERY_CACHE=true
FORGE_GRAPHQL_QUERY_CACHE_TTL=5m
FORGE_GRAPHQL_MAX_CACHE_SIZE=1000

# Security Configuration
FORGE_GRAPHQL_ENABLE_CORS=true
FORGE_GRAPHQL_ALLOWED_ORIGINS=https://example.com,https://app.example.com
FORGE_GRAPHQL_MAX_UPLOAD_SIZE=10485760

# Observability Configuration
FORGE_GRAPHQL_ENABLE_METRICS=true
FORGE_GRAPHQL_ENABLE_TRACING=true
FORGE_GRAPHQL_ENABLE_LOGGING=true
FORGE_GRAPHQL_LOG_SLOW_QUERIES=true
FORGE_GRAPHQL_SLOW_QUERY_THRESHOLD=1s

Programmatic Configuration

package main

import (
    "time"
    "github.com/xraph/forge"
    "github.com/xraph/forge/extensions/graphql"
)

func main() {
    app := forge.NewApp(forge.AppConfig{
        Name:    "my-app",
        Version: "1.0.0",
    })

    // Configure GraphQL extension
    config := graphql.Config{
        Endpoint:            "/api/graphql",
        PlaygroundEndpoint:  "/api/playground",
        EnablePlayground:    true,
        EnableIntrospection: true,
        AutoGenerateSchema:  true,
        MaxComplexity:       1000,
        MaxDepth:            15,
        QueryTimeout:        30 * time.Second,
        EnableDataLoader:    true,
        DataLoaderBatchSize: 100,
        DataLoaderWait:      10 * time.Millisecond,
        EnableQueryCache:    true,
        QueryCacheTTL:       5 * time.Minute,
        MaxCacheSize:        1000,
        EnableCORS:          true,
        AllowedOrigins:      []string{"https://example.com"},
        MaxUploadSize:       10 * 1024 * 1024, // 10MB
        EnableMetrics:       true,
        EnableTracing:       true,
        EnableLogging:       true,
        LogSlowQueries:      true,
        SlowQueryThreshold:  1 * time.Second,
    }

    // Register extension
    ext := graphql.NewExtensionWithConfig(config)
    app.RegisterExtension(ext)

    app.Start()
}

Usage Examples

Schema Definition

Create schema/schema.graphql:

directive @auth(requires: String) on FIELD_DEFINITION

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

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  createdAt: String!
}

type Query {
  # Public queries
  hello(name: String!): String!
  version: String!
  
  # User queries
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!
  
  # Post queries
  post(id: ID!): Post
  posts(limit: Int = 10, offset: Int = 0): [Post!]!
}

type Mutation {
  # User mutations
  createUser(input: CreateUserInput!): User! @auth(requires: "admin")
  updateUser(id: ID!, input: UpdateUserInput!): User! @auth(requires: "user")
  deleteUser(id: ID!): Boolean! @auth(requires: "admin")
  
  # Post mutations
  createPost(input: CreatePostInput!): Post! @auth(requires: "user")
  updatePost(id: ID!, input: UpdatePostInput!): Post! @auth(requires: "user")
  deletePost(id: ID!): Boolean! @auth(requires: "user")
}

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

input UpdateUserInput {
  name: String
  email: String
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
}

input UpdatePostInput {
  title: String
  content: String
}

Generate Code

# Generate GraphQL code
go run github.com/99designs/gqlgen generate

# This creates:
# - generated/generated.go (executable schema)
# - generated/models.go (type definitions)
# - schema.resolvers.go (resolver implementations)

Implement Resolvers

package graphql

import (
    "context"
    "fmt"
    "github.com/xraph/forge"
    "github.com/xraph/forge/extensions/database"
)

// Resolver implements the GraphQL resolvers
type Resolver struct {
    container forge.Container
    logger    forge.Logger
    metrics   forge.Metrics
    config    Config
}

// Query resolver
func (r *queryResolver) Hello(ctx context.Context, name string) (string, error) {
    return fmt.Sprintf("Hello, %s!", name), nil
}

func (r *queryResolver) Version(ctx context.Context) (string, error) {
    return "1.0.0", nil
}

func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
    // Get database from DI container
    db := forge.Must[database.Database](r.container, "database")
    
    var user User
    err := db.QueryRow(ctx, "SELECT id, name, email FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return nil, fmt.Errorf("user not found: %w", err)
    }
    
    return &user, nil
}

func (r *queryResolver) Users(ctx context.Context, limit *int, offset *int) ([]*User, error) {
    db := forge.Must[database.Database](r.container, "database")
    
    // Set defaults
    if limit == nil {
        defaultLimit := 10
        limit = &defaultLimit
    }
    if offset == nil {
        defaultOffset := 0
        offset = &defaultOffset
    }
    
    rows, err := db.Query(ctx, 
        "SELECT id, name, email FROM users ORDER BY id LIMIT $1 OFFSET $2", 
        *limit, *offset)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var users []*User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
            return nil, err
        }
        users = append(users, &user)
    }
    
    return users, nil
}

// Mutation resolver
func (r *mutationResolver) CreateUser(ctx context.Context, input CreateUserInput) (*User, error) {
    db := forge.Must[database.Database](r.container, "database")
    
    var user User
    err := db.QueryRow(ctx,
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
        input.Name, input.Email).Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return nil, fmt.Errorf("failed to create user: %w", err)
    }
    
    r.logger.Info("user created", forge.F("user_id", user.ID))
    return &user, nil
}

// Field resolver for User.Posts (with DataLoader)
func (r *userResolver) Posts(ctx context.Context, obj *User) ([]*Post, error) {
    // Use DataLoader to batch post loading
    loader := GetPostLoader(ctx)
    posts, err := loader.LoadMany(ctx, []interface{}{obj.ID})
    if err != nil {
        return nil, err
    }
    
    result := make([]*Post, len(posts))
    for i, post := range posts {
        result[i] = post.(*Post)
    }
    
    return result, nil
}

DataLoader Implementation

package graphql

import (
    "context"
    "time"
    "github.com/xraph/forge"
    "github.com/xraph/forge/extensions/graphql/dataloader"
    "github.com/xraph/forge/extensions/database"
)

// PostLoader loads posts by user ID
type PostLoader struct {
    *dataloader.Loader
    db database.Database
}

// NewPostLoader creates a new post loader
func NewPostLoader(db database.Database) *PostLoader {
    config := dataloader.DefaultLoaderConfig()
    config.Wait = 10 * time.Millisecond
    config.MaxBatch = 100
    
    loader := dataloader.NewLoader(config, func(ctx context.Context, keys []interface{}) ([]interface{}, []error) {
        return loadPostsByUserIDs(ctx, db, keys)
    })
    
    return &PostLoader{
        Loader: loader,
        db:     db,
    }
}

func loadPostsByUserIDs(ctx context.Context, db database.Database, userIDs []interface{}) ([]interface{}, []error) {
    // Convert keys to string slice
    ids := make([]string, len(userIDs))
    for i, id := range userIDs {
        ids[i] = id.(string)
    }
    
    // Query posts for all user IDs
    query := `
        SELECT id, title, content, author_id, created_at 
        FROM posts 
        WHERE author_id = ANY($1)
        ORDER BY created_at DESC
    `
    
    rows, err := db.Query(ctx, query, ids)
    if err != nil {
        errors := make([]error, len(userIDs))
        for i := range errors {
            errors[i] = err
        }
        return nil, errors
    }
    defer rows.Close()
    
    // Group posts by user ID
    postsByUser := make(map[string][]*Post)
    for rows.Next() {
        var post Post
        if err := rows.Scan(&post.ID, &post.Title, &post.Content, &post.AuthorID, &post.CreatedAt); err != nil {
            continue
        }
        postsByUser[post.AuthorID] = append(postsByUser[post.AuthorID], &post)
    }
    
    // Return results in the same order as keys
    results := make([]interface{}, len(userIDs))
    errors := make([]error, len(userIDs))
    
    for i, userID := range userIDs {
        posts := postsByUser[userID.(string)]
        if posts == nil {
            posts = []*Post{} // Return empty slice instead of nil
        }
        results[i] = posts
    }
    
    return results, errors
}

// Context key for DataLoader
type contextKey string

const postLoaderKey contextKey = "postLoader"

// GetPostLoader gets or creates a PostLoader from context
func GetPostLoader(ctx context.Context) *PostLoader {
    if loader, ok := ctx.Value(postLoaderKey).(*PostLoader); ok {
        return loader
    }
    
    // This should be set by middleware
    panic("PostLoader not found in context")
}

// DataLoader middleware
func DataLoaderMiddleware(db database.Database) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()
            
            // Create loaders
            postLoader := NewPostLoader(db)
            
            // Add to context
            ctx = context.WithValue(ctx, postLoaderKey, postLoader)
            
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Authentication & Authorization

package graphql

import (
    "context"
    "net/http"
    "strings"
)

// AuthMiddleware adds user information to context
func AuthMiddleware() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()
            
            // Extract token from Authorization header
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                next.ServeHTTP(w, r)
                return
            }
            
            token := strings.TrimPrefix(authHeader, "Bearer ")
            if token == authHeader {
                next.ServeHTTP(w, r)
                return
            }
            
            // Validate token and get user info
            user, roles, err := validateToken(token)
            if err != nil {
                next.ServeHTTP(w, r)
                return
            }
            
            // Add user and roles to context
            ctx = context.WithValue(ctx, "user", user)
            ctx = context.WithValue(ctx, "roles", roles)
            
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

func validateToken(token string) (interface{}, []string, error) {
    // Implement your token validation logic here
    // Return user object and roles
    return map[string]interface{}{
        "id":   "user-123",
        "name": "John Doe",
    }, []string{"user"}, nil
}

Advanced Features

Custom Directives

package directives

import (
    "context"
    "fmt"
    "time"
    "github.com/99designs/gqlgen/graphql"
)

// RateLimit directive limits the number of requests
func RateLimit(ctx context.Context, obj interface{}, next graphql.Resolver, limit int, window string) (interface{}, error) {
    // Parse window duration
    duration, err := time.ParseDuration(window)
    if err != nil {
        return nil, fmt.Errorf("invalid window duration: %w", err)
    }
    
    // Get client identifier (IP, user ID, etc.)
    clientID := getClientID(ctx)
    
    // Check rate limit
    if !checkRateLimit(clientID, limit, duration) {
        return nil, fmt.Errorf("rate limit exceeded: %d requests per %s", limit, window)
    }
    
    return next(ctx)
}

// Cache directive caches resolver results
func Cache(ctx context.Context, obj interface{}, next graphql.Resolver, ttl string) (interface{}, error) {
    // Parse TTL
    duration, err := time.ParseDuration(ttl)
    if err != nil {
        return nil, fmt.Errorf("invalid TTL: %w", err)
    }
    
    // Generate cache key
    cacheKey := generateCacheKey(ctx, obj)
    
    // Check cache
    if value, found := getFromCache(cacheKey); found {
        return value, nil
    }
    
    // Execute resolver
    result, err := next(ctx)
    if err != nil {
        return nil, err
    }
    
    // Cache result
    setCache(cacheKey, result, duration)
    
    return result, nil
}

Apollo Federation

# gqlgen.yml
schema:
  - schema/*.graphql

exec:
  filename: generated/generated.go
  package: generated

model:
  filename: generated/models.go
  package: generated

resolver:
  layout: follow-schema
  dir: .
  package: graphql

# Enable Apollo Federation v2
federation:
  filename: generated/federation.go
  package: generated
  version: 2
# schema/schema.graphql with Federation
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}

type Post @key(fields: "id") {
  id: ID!
  title: String!
  content: String!
  author: User!
}

File Upload Support

# Add to schema
scalar Upload

type Mutation {
  uploadFile(file: Upload!): String!
  uploadMultipleFiles(files: [Upload!]!): [String!]!
}
func (r *mutationResolver) UploadFile(ctx context.Context, file graphql.Upload) (string, error) {
    // Save file
    filename := fmt.Sprintf("uploads/%s", file.Filename)
    
    dst, err := os.Create(filename)
    if err != nil {
        return "", err
    }
    defer dst.Close()
    
    _, err = io.Copy(dst, file.File)
    if err != nil {
        return "", err
    }
    
    return filename, nil
}

Subscription Support (Future Enhancement)

type Subscription {
  postAdded: Post!
  userOnline(userID: ID!): Boolean!
}
func (r *subscriptionResolver) PostAdded(ctx context.Context) (<-chan *Post, error) {
    ch := make(chan *Post, 1)
    
    // Subscribe to events
    go func() {
        defer close(ch)
        // Listen for post creation events
        for {
            select {
            case post := <-postChannel:
                ch <- post
            case <-ctx.Done():
                return
            }
        }
    }()
    
    return ch, nil
}

Best Practices

Schema Design

  • Use descriptive names: Clear field and type names improve API usability
  • Implement pagination: Use cursor-based pagination for large datasets
  • Version your schema: Use deprecation instead of breaking changes
  • Keep mutations atomic: Each mutation should represent a single business operation

Performance Optimization

  • Use DataLoaders: Prevent N+1 queries with batching and caching
  • Implement query complexity analysis: Prevent expensive queries
  • Cache frequently accessed data: Use Redis or in-memory caching
  • Optimize database queries: Use proper indexes and query optimization

Security Considerations

  • Implement authentication: Secure your GraphQL endpoints
  • Use authorization directives: Control access to sensitive fields
  • Validate input data: Sanitize and validate all input parameters
  • Rate limiting: Prevent abuse with request rate limiting

Error Handling

  • Use structured errors: Provide meaningful error messages
  • Implement error codes: Use consistent error classification
  • Log errors properly: Include context for debugging
  • Handle partial failures: Return partial data when possible

Troubleshooting

Common Issues

Schema Generation Errors

# Regenerate schema
go run github.com/99designs/gqlgen generate

# Check for syntax errors in schema files
graphql-schema-linter schema/*.graphql

Resolver Not Found

// Ensure resolver methods match schema exactly
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
    // Implementation
}

DataLoader Issues

// Ensure DataLoader is properly initialized in context
func DataLoaderMiddleware(db database.Database) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := r.Context()
            
            // Create and add loaders to context
            postLoader := NewPostLoader(db)
            ctx = context.WithValue(ctx, postLoaderKey, postLoader)
            
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Performance Issues

# Tune performance settings
extensions:
  graphql:
    max_complexity: 500      # Reduce complexity limit
    max_depth: 10           # Reduce depth limit
    query_timeout: "15s"    # Reduce timeout
    dataloader_batch_size: 50  # Reduce batch size

Debugging

Enable Debug Logging

logging:
  level: debug
  loggers:
    graphql: debug
    graphql.resolver: debug
    graphql.dataloader: debug

Query Analysis

// Add query logging middleware
func QueryLoggingMiddleware(logger forge.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // Log request
            logger.Info("graphql request started",
                forge.F("method", r.Method),
                forge.F("path", r.URL.Path))
            
            next.ServeHTTP(w, r)
            
            // Log completion
            duration := time.Since(start)
            logger.Info("graphql request completed",
                forge.F("duration", duration))
        })
    }
}

Next Steps

How is this guide?

Last updated on