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.45Docker
FROM xraph/forge:latest
# GraphQL extension is includedPackage Manager
# Using Forge CLI
forge extension add graphql
# Using package manager
npm install @xraph/forge-graphqlConfiguration
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=1sProgrammatic 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/*.graphqlResolver 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 sizeDebugging
Enable Debug Logging
logging:
level: debug
loggers:
graphql: debug
graphql.resolver: debug
graphql.dataloader: debugQuery 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
- Explore gRPC Extension for high-performance service communication
- Learn about Events Extension for event-driven architecture
- Check out Streaming Extension for real-time data streaming
- Review WebRTC Extension for peer-to-peer communication
- See Database Extension for data persistence
- Visit Auth Extension for authentication integration
How is this guide?
Last updated on