graphql

package module
v1.13.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

Forge GraphQL, a gqlgen GraphQL endpoint and playground mounted on the router of a Go application built on Forge

Forge GraphQL: a gqlgen GraphQL server for Go apps on Forge

Forge GraphQL is the GraphQL extension for Forge, the Go backend framework. It runs a gqlgen server inside your app and mounts it on the Forge router, with a GraphQL Playground, query complexity limits, automatic persisted queries, file uploads, Apollo Federation's _service query, Forge metrics and slow query logging. It also ships a small DataLoader package for batching N+1 lookups in Golang resolvers.

Go CI Go Reference Apache 2.0

Documentation · Quick start · Status · Features · DataLoader · Configuration · FAQ

Quick start

You need Go 1.26 or later. Add Forge and the extension to your module:

go get github.com/xraph/forge@latest
go get github.com/xraph/forge/extensions/graphql@latest

Save this as main.go:

package main

import (
	"log"

	"github.com/xraph/forge"
	"github.com/xraph/forge/extensions/graphql"
)

func main() {
	app := forge.NewApp(forge.AppConfig{
		Name:        "graphql-demo",
		HTTPAddress: ":8080",
		Extensions: []forge.Extension{
			graphql.NewExtension(
				graphql.WithEndpoint("/graphql"),
				graphql.WithPlayground(true),
			),
		},
	})

	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}

Run it with go mod tidy && go run ., then send a query:

curl -X POST http://localhost:8080/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ hello(name: \"Forge\") version }"}'
# {"data":{"hello":"Hello, Forge!","version":"2.0.0"}}

Open http://localhost:8080/playground in a browser for the GraphQL Playground. The endpoint answers GET as well as POST, so curl -G localhost:8080/graphql --data-urlencode 'query={ version }' works too.

What the extension serves today

Read this before you build on the extension. The server runs the schema that is compiled into this module, schema/schema.graphql, and nothing else:

directive @auth(requires: String) on FIELD_DEFINITION

type Query {
  hello(name: String!): String!
  version: String!
}

type Mutation {
  echo(message: String!): String!
}

There isn't a way to hand it your own schema or resolvers yet. RegisterType, RegisterQuery, RegisterMutation and RegisterSubscription on the GraphQL interface only write a debug log line, Use does nothing, and ExecuteQuery returns an error response telling you to go through HTTPHandler(). The config keys auto_generate_schema and schema_file are accepted and ignored.

So what is it good for today? If you want to see gqlgen running inside a Forge app with the playground, persisted queries, complexity limits and metrics already wired, it shows you how, and the dataloader package is usable on its own in any resolver you write. If you need your own schema right now, run gqlgen in your own module and mount its handler.Server on a Forge route yourself, the same way extension.go does.

Features

You need Forge GraphQL gives you
A GraphQL endpoint on your existing router GET and POST routes at endpoint, registered when the extension starts
A browser IDE while you develop GraphQL Playground at playground_endpoint, switched off with WithPlayground(false)
Protection from expensive queries gqlgen's fixed complexity limit, from max_complexity
Smaller requests from repeat clients Automatic persisted queries, with gqlgen's default cache
File uploads gqlgen's multipart transport, with gqlgen's default limits
Federation Apollo Federation v2 code generated in, including the _service { sdl } query
Visibility graphql_operation_duration_seconds and graphql_operation_total, debug logs per operation, error logs, and a warning for queries slower than slow_query_threshold
N+1 batching in your own resolvers The dataloader package

The server also registers gqlgen's WebSocket, GET, POST, OPTIONS and multipart transports. The schema has no subscriptions, so the WebSocket transport has nothing to stream yet.

Batch lookups with DataLoader

The dataloader package works on its own, whether or not you use the extension. A Loader collects the keys that concurrent callers ask for during a short wait window, calls your batch function once with all of them, and caches the results:

users := dataloader.NewLoader(dataloader.DefaultLoaderConfig(),
	func(ctx context.Context, keys []any) ([]any, []error) {
		// One query for every key collected in this window.
		values := make([]any, len(keys))
		for i, k := range keys {
			values[i] = fmt.Sprintf("user-%v", k)
		}

		return values, make([]error, len(keys))
	})

user, err := users.Load(ctx, 42)

Order matters. Your batch function has to return a value slice and an error slice that line up with keys, one entry per key, or the wrong caller gets the wrong user. DefaultLoaderConfig() waits 10ms before it dispatches. Prime, Clear and ClearAll manage the cache. One caveat: LoadMany calls Load for one key after another, so it waits a full window per key and doesn't batch. Call Load from concurrent resolvers if you want the batching.

Configuration

The extension reads extensions.graphql from your config, falling back to graphql. Options you pass in code override the file.

extensions:
  graphql:
    endpoint: /graphql
    playground_endpoint: /playground
    enable_playground: true
    max_complexity: 1000
    enable_metrics: true
    enable_logging: true
    log_slow_queries: true
    slow_query_threshold: 1s

These settings change what the server does:

Key Option Default What it does
endpoint WithEndpoint /graphql Path of the GraphQL endpoint
playground_endpoint none /playground Path of the playground
enable_playground WithPlayground true Serves the playground
max_complexity WithMaxComplexity 1000 Complexity limit, 0 turns it off
enable_metrics WithMetrics true Records the two operation metrics
enable_logging none true Logs operations and GraphQL errors
log_slow_queries none true Warns about slow operations
slow_query_threshold none 1s What counts as slow

The slow query warning only runs when you have metrics or logging on.

Config has more fields, and some have options, but nothing reads them yet. enable_query_cache (WithQueryCache), max_cache_size and max_upload_size are passed to a second persisted-query cache and a second multipart transport, but gqlgen's default server already registers its own, and those handle every request, so the defaults apply. enable_introspection (WithIntrospection) has no effect because gqlgen's default server always allows introspection, so you can't turn it off from config today. max_depth (WithMaxDepth), query_timeout (WithTimeout), enable_dataloader (WithDataLoader), dataloader_batch_size, dataloader_wait, query_cache_ttl, enable_cors and allowed_origins (WithCORS), enable_tracing (WithTracing), auto_generate_schema and schema_file are validated where there's a rule (max_depth has to be at least 1, query_timeout positive) and otherwise left alone.

WithConfig replaces the whole config, and WithRequireConfig(true) makes a missing config section an error at startup. In the container you'll find *graphql.GraphQLService and the graphql.GraphQL interface, both backed by one instance.

Documentation

Topic Where
What the extension does Features
Every config key Configuration
Exported functions and types Functions, pkg.go.dev
gqlgen itself gqlgen documentation
Extensions in general Forge extension system

FAQ

How do I add a GraphQL endpoint to a Go app built on Forge?

Add graphql.NewExtension() to your app's Extensions. You get /graphql and /playground on the Forge router. Read what the extension serves today first, because the schema is fixed for now.

Can I use my own GraphQL schema with Forge GraphQL?

Not through the extension yet. The registration methods are placeholders. Generate your schema with gqlgen in your own module and register its handler on a Forge route.

Does it support Apollo Federation?

The generated code includes Federation v2, and { _service { sdl } } returns the module's schema. Entity resolvers aren't there, because the built-in schema has no entities.

How do I fix the N+1 problem in Go GraphQL resolvers?

Use the dataloader package: create one Loader per request and call Load from each resolver, so lookups that happen together turn into one batch call.

Contributing

The extension is its own Go module inside the Forge repository. Its go.mod has no replace for the Forge root, so it builds against the published Forge version its require line names, even inside this repository. Test it from its directory:

cd extensions/graphql
GOWORK=off go test -race ./...
GOWORK=off golangci-lint run ./...

After you edit schema/schema.graphql, regenerate the code with go run github.com/99designs/gqlgen generate from the same directory. AGENTS.md has the full build, lint and release rules for the repository, and CONTRIBUTING.md has the contribution guidelines.

License

Forge GraphQL is part of Forge and is available under the Apache License 2.0.

Documentation

Index

Constants

View Source
const (
	// ServiceKey is the DI key for the GraphQL service.
	ServiceKey = "graphql"
)

DI container keys for GraphQL extension services.

Variables

View Source
var (
	ErrNotInitialized     = errors.New("graphql: not initialized")
	ErrInvalidQuery       = errors.New("graphql: invalid query")
	ErrInvalidSchema      = errors.New("graphql: invalid schema")
	ErrTypeNotFound       = errors.New("graphql: type not found")
	ErrResolverNotFound   = errors.New("graphql: resolver not found")
	ErrExecutionFailed    = errors.New("graphql: execution failed")
	ErrComplexityExceeded = errors.New("graphql: query complexity exceeded")
	ErrDepthExceeded      = errors.New("graphql: query depth exceeded")
	ErrTimeout            = errors.New("graphql: query timeout")
	ErrInvalidConfig      = errors.New("graphql: invalid configuration")
)

Common GraphQL errors.

Functions

func NewExtension

func NewExtension(opts ...ConfigOption) forge.Extension

NewExtension creates a new GraphQL extension.

func NewExtensionWithConfig

func NewExtensionWithConfig(config Config) forge.Extension

NewExtensionWithConfig creates a new GraphQL extension with a complete config.

Types

type ArgumentDefinition

type ArgumentDefinition struct {
	Name         string
	Description  string
	Type         string
	DefaultValue any
}

ArgumentDefinition defines an argument.

type Config

type Config struct {
	// Server settings
	Endpoint            string `json:"endpoint"             mapstructure:"endpoint"             yaml:"endpoint"`
	PlaygroundEndpoint  string `json:"playground_endpoint"  mapstructure:"playground_endpoint"  yaml:"playground_endpoint"`
	EnablePlayground    bool   `json:"enable_playground"    mapstructure:"enable_playground"    yaml:"enable_playground"`
	EnableIntrospection bool   `json:"enable_introspection" mapstructure:"enable_introspection" yaml:"enable_introspection"`

	// Schema
	AutoGenerateSchema bool   `json:"auto_generate_schema"  mapstructure:"auto_generate_schema" yaml:"auto_generate_schema"`
	SchemaFile         string `json:"schema_file,omitempty" mapstructure:"schema_file"          yaml:"schema_file,omitempty"`

	// Performance
	MaxComplexity       int           `json:"max_complexity"        mapstructure:"max_complexity"        yaml:"max_complexity"`
	MaxDepth            int           `json:"max_depth"             mapstructure:"max_depth"             yaml:"max_depth"`
	QueryTimeout        time.Duration `json:"query_timeout"         mapstructure:"query_timeout"         yaml:"query_timeout"`
	EnableDataLoader    bool          `json:"enable_dataloader"     mapstructure:"enable_dataloader"     yaml:"enable_dataloader"`
	DataLoaderBatchSize int           `json:"dataloader_batch_size" mapstructure:"dataloader_batch_size" yaml:"dataloader_batch_size"`
	DataLoaderWait      time.Duration `json:"dataloader_wait"       mapstructure:"dataloader_wait"       yaml:"dataloader_wait"`

	// Caching
	EnableQueryCache bool          `json:"enable_query_cache" mapstructure:"enable_query_cache" yaml:"enable_query_cache"`
	QueryCacheTTL    time.Duration `json:"query_cache_ttl"    mapstructure:"query_cache_ttl"    yaml:"query_cache_ttl"`
	MaxCacheSize     int           `json:"max_cache_size"     mapstructure:"max_cache_size"     yaml:"max_cache_size"`

	// Security
	EnableCORS     bool     `json:"enable_cors"               mapstructure:"enable_cors"     yaml:"enable_cors"`
	AllowedOrigins []string `json:"allowed_origins,omitempty" mapstructure:"allowed_origins" yaml:"allowed_origins,omitempty"`
	MaxUploadSize  int64    `json:"max_upload_size"           mapstructure:"max_upload_size" yaml:"max_upload_size"`

	// Observability
	EnableMetrics      bool          `json:"enable_metrics"       mapstructure:"enable_metrics"       yaml:"enable_metrics"`
	EnableTracing      bool          `json:"enable_tracing"       mapstructure:"enable_tracing"       yaml:"enable_tracing"`
	EnableLogging      bool          `json:"enable_logging"       mapstructure:"enable_logging"       yaml:"enable_logging"`
	LogSlowQueries     bool          `json:"log_slow_queries"     mapstructure:"log_slow_queries"     yaml:"log_slow_queries"`
	SlowQueryThreshold time.Duration `json:"slow_query_threshold" mapstructure:"slow_query_threshold" yaml:"slow_query_threshold"`

	// Config loading flags
	RequireConfig bool `json:"-" mapstructure:"-" yaml:"-"`
}

Config contains configuration for the GraphQL extension.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns default GraphQL configuration.

func (*Config) Validate

func (c *Config) Validate() error

Validate validates the configuration.

type ConfigOption

type ConfigOption func(*Config)

ConfigOption is a functional option for Config.

func WithCORS

func WithCORS(origins ...string) ConfigOption

func WithConfig

func WithConfig(config Config) ConfigOption

func WithDataLoader

func WithDataLoader(enable bool) ConfigOption

func WithEndpoint

func WithEndpoint(endpoint string) ConfigOption

func WithIntrospection

func WithIntrospection(enable bool) ConfigOption

func WithMaxComplexity

func WithMaxComplexity(maxComplexity int) ConfigOption

func WithMaxDepth

func WithMaxDepth(maxDepth int) ConfigOption

func WithMetrics

func WithMetrics(enable bool) ConfigOption

func WithPlayground

func WithPlayground(enable bool) ConfigOption

func WithQueryCache

func WithQueryCache(enable bool, ttl time.Duration) ConfigOption

func WithRequireConfig

func WithRequireConfig(require bool) ConfigOption

func WithTimeout

func WithTimeout(timeout time.Duration) ConfigOption

func WithTracing

func WithTracing(enable bool) ConfigOption

type DataLoader

type DataLoader interface {
	Load(ctx context.Context, key any) (any, error)
	LoadMany(ctx context.Context, keys []any) ([]any, error)
	Prime(key any, value any)
	Clear(key any)
	ClearAll()
}

DataLoader optimizes N+1 queries.

type Error

type Error struct {
	Message    string         `json:"message"`
	Path       []any          `json:"path,omitempty"`
	Locations  []Location     `json:"locations,omitempty"`
	Extensions map[string]any `json:"extensions,omitempty"`
}

Error represents a GraphQL error.

type ExecutorFunc

type ExecutorFunc func(ctx context.Context, req *Request) (*Response, error)

ExecutorFunc executes a GraphQL operation.

type Extension

type Extension struct {
	*forge.BaseExtension
	// contains filtered or unexported fields
}

Extension implements forge.Extension for GraphQL functionality. The extension is now a lightweight facade that loads config and registers services.

func (*Extension) Health

func (e *Extension) Health(ctx context.Context) error

Health checks the extension health.

func (*Extension) Register

func (e *Extension) Register(app forge.App) error

Register registers the GraphQL extension with the app. This method loads configuration and registers service constructors.

func (*Extension) Start

func (e *Extension) Start(ctx context.Context) error

Start starts the GraphQL extension, resolves and starts the service, and registers routes.

func (*Extension) Stop

func (e *Extension) Stop(ctx context.Context) error

Stop stops the GraphQL service and marks the extension as stopped.

type FederationConfig

type FederationConfig struct {
	// Enable federation support
	Enabled bool
	// Version of Apollo Federation (1 or 2)
	Version int
}

FederationConfig holds federation-specific configuration.

func DefaultFederationConfig

func DefaultFederationConfig() FederationConfig

DefaultFederationConfig returns default federation configuration.

type FieldDefinition

type FieldDefinition struct {
	Name              string
	Description       string
	Type              string
	Args              []*ArgumentDefinition
	Resolver          FieldResolverFunc
	Deprecated        bool
	DeprecationReason string
}

FieldDefinition defines a field in a type.

type FieldResolver

type FieldResolver interface {
	Resolve(ctx context.Context, source any, args map[string]any) (any, error)
}

FieldResolver resolves a specific field.

type FieldResolverFunc

type FieldResolverFunc func(ctx context.Context, args map[string]any) (any, error)

FieldResolverFunc handles field resolution (for dynamic registration).

type GraphQL

type GraphQL interface {
	// Schema management
	RegisterType(name string, obj any) error
	RegisterQuery(name string, resolver FieldResolverFunc) error
	RegisterMutation(name string, resolver FieldResolverFunc) error
	RegisterSubscription(name string, resolver SubscriptionResolverFunc) error

	// Schema generation
	GenerateSchema() (string, error)
	GetSchema() *GraphQLSchema

	// Execution
	ExecuteQuery(ctx context.Context, query string, variables map[string]any) (*Response, error)

	// HTTP handlers
	HTTPHandler() http.Handler
	PlaygroundHandler() http.Handler

	// Middleware
	Use(middleware Middleware)

	// Introspection
	EnableIntrospection(enable bool)

	// Health
	Ping(ctx context.Context) error
}

GraphQL represents a unified GraphQL server interface (wraps gqlgen).

func NewGraphQLServer

func NewGraphQLServer(config Config, logger forge.Logger, metrics forge.Metrics, container forge.Container) (GraphQL, error)

NewGraphQLServer creates a new GraphQL server with gqlgen.

type GraphQLDirective

type GraphQLDirective struct {
	Name        string
	Description string
	Locations   []string
	Args        []*ArgumentDefinition
}

GraphQLDirective defines a GraphQL directive (wrapper for introspection).

type GraphQLSchema

type GraphQLSchema struct {
	Types         map[string]*TypeDefinition
	Queries       map[string]*FieldDefinition
	Mutations     map[string]*FieldDefinition
	Subscriptions map[string]*FieldDefinition
	Directives    []*GraphQLDirective
}

GraphQLSchema represents a GraphQL schema (wrapper for introspection).

type GraphQLService

type GraphQLService struct {
	// contains filtered or unexported fields
}

GraphQLService wraps a GraphQL server implementation and provides lifecycle management. It implements vessel's di.Service interface so Vessel can manage its lifecycle.

func NewGraphQLService

func NewGraphQLService(config Config, container forge.Container, logger forge.Logger, metrics forge.Metrics) (*GraphQLService, error)

NewGraphQLService creates a new GraphQL service with the given configuration. This is the constructor that will be registered with the DI container.

func (*GraphQLService) ExecuteQuery

func (s *GraphQLService) ExecuteQuery(ctx context.Context, query string, variables map[string]any) (*Response, error)

func (*GraphQLService) GenerateSchema

func (s *GraphQLService) GenerateSchema() (string, error)

func (*GraphQLService) GetSchema

func (s *GraphQLService) GetSchema() *GraphQLSchema

func (*GraphQLService) HTTPHandler

func (s *GraphQLService) HTTPHandler() http.Handler

func (*GraphQLService) Health

func (s *GraphQLService) Health(ctx context.Context) error

Health checks if the GraphQL service is healthy.

func (*GraphQLService) Name

func (s *GraphQLService) Name() string

Name returns the service name for Vessel's lifecycle management.

func (*GraphQLService) Ping

func (s *GraphQLService) Ping(ctx context.Context) error

func (*GraphQLService) PlaygroundHandler

func (s *GraphQLService) PlaygroundHandler() http.Handler

func (*GraphQLService) RegisterMutation

func (s *GraphQLService) RegisterMutation(name string, resolver FieldResolverFunc) error

func (*GraphQLService) RegisterQuery

func (s *GraphQLService) RegisterQuery(name string, resolver FieldResolverFunc) error

func (*GraphQLService) RegisterSubscription

func (s *GraphQLService) RegisterSubscription(name string, resolver SubscriptionResolverFunc) error

func (*GraphQLService) RegisterType

func (s *GraphQLService) RegisterType(name string, obj any) error

func (*GraphQLService) Server

func (s *GraphQLService) Server() GraphQL

Server returns the underlying GraphQL server implementation.

func (*GraphQLService) Start

func (s *GraphQLService) Start(ctx context.Context) error

Start starts the GraphQL service. This is called automatically by Vessel during container.Start().

func (*GraphQLService) Stop

func (s *GraphQLService) Stop(ctx context.Context) error

Stop stops the GraphQL service. This is called automatically by Vessel during container.Stop().

func (*GraphQLService) Use

func (s *GraphQLService) Use(middleware Middleware)

type Location

type Location struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

Location represents an error location.

type Middleware

type Middleware func(next ExecutorFunc) ExecutorFunc

Middleware wraps GraphQL execution.

type Request

type Request struct {
	Query         string         `json:"query"`
	OperationName string         `json:"operationName,omitempty"`
	Variables     map[string]any `json:"variables,omitempty"`
}

Request represents a GraphQL request.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver is the root resolver struct for GraphQL operations.

func NewResolver

func NewResolver(container forge.Container, logger forge.Logger, metrics forge.Metrics, config Config) *Resolver

NewResolver creates a new resolver with dependencies.

func (*Resolver) Mutation

func (r *Resolver) Mutation() generated.MutationResolver

func (*Resolver) Query

func (r *Resolver) Query() generated.QueryResolver

type Response

type Response struct {
	Data       any            `json:"data,omitempty"`
	Errors     []Error        `json:"errors,omitempty"`
	Extensions map[string]any `json:"extensions,omitempty"`
}

Response represents a GraphQL response.

type SubscriptionResolverFunc

type SubscriptionResolverFunc func(ctx context.Context, args map[string]any) (<-chan any, error)

SubscriptionResolverFunc handles subscription events (for dynamic registration).

type TypeDefinition

type TypeDefinition struct {
	Name        string
	Description string
	Kind        string // OBJECT, INTERFACE, UNION, ENUM, INPUT_OBJECT, SCALAR
	Fields      []*FieldDefinition
	Interfaces  []string
}

TypeDefinition defines a GraphQL type.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL