GraphQL Router / API Gateway Framework written in Golang
We're hiring!
Are you interested in working on graphql-go-tools?
We're looking for experienced Go developers and DevOps or Platform Engineering specialists to help us run Cosmo Cloud.
If you're more interested in working with Customers on their GraphQL Strategy,
we also offer Solution Architect positions.
Check out the currently open positions.
Replacement for Apollo Router
If you're looking for a complete ready-to-use Open Source Router for Federation,
have a look at the Cosmo Router which is based on this library.
Cosmo Router wraps this library and provides a complete solution for Federated GraphQL including the following features:
Notes
This library is used in production at WunderGraph.
We've recently introduced a v2 module that is not completely backwards compatible with v1, hence the major version bump.
The v2 module contains big rewrites in the engine package, mainly to better support GraphQL Federation.
Please consider the v1 module as deprecated and move to v2 as soon as possible.
We have customers who pay us to maintain this library and steer the direction of the project.
Contact us if you're looking for commercial support, features or consulting.
Performance
The architecture of this library is designed for performance, high-throughput and low garbage collection overhead.
The following benchmark measures the "overhead" of loading and resolving a GraphQL response from four static in-memory Subgraphs at 0,007459 ms/op.
In more complete end-to-end benchmarks, we've measured up to 8x more requests per second and 8x lower p99 latency compared to Apollo Router, which is written in Rust.
cd v2/pkg/engine
go test -run=nothing -bench=Benchmark_NestedBatchingWithoutChecks -memprofile memprofile.out -benchtime 3s && go tool pprof memprofile.out
goos: darwin
goarch: arm64
pkg: github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve
Benchmark_NestedBatchingWithoutChecks-10 473186 7134 ns/op 52.00 MB/s 2086 B/op 36 allocs/op
Tutorial
If you're here to learn how to use this library to build your own custom GraphQL Router or API Gateway,
here's a speed run tutorial for you, based on how we use this library in Cosmo Router.
package main
import (
"bytes"
"context"
"fmt"
"github.com/cespare/xxhash/v2"
"github.com/wundergraph/graphql-go-tools/v2/pkg/ast"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astnormalization"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astparser"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter"
"github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation"
"github.com/wundergraph/graphql-go-tools/v2/pkg/astvisitor"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/staticdatasource"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan"
"github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve"
"github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport"
)
func ExampleParsePrintDocument() {
input := []byte(`query { hello }`)
report := &operationreport.Report{}
document := ast.NewSmallDocument()
parser := astparser.NewParser()
printer := &astprinter.Printer{}
document.Input.ResetInputBytes(input)
parser.Parse(document, report)
if report.HasErrors() {
panic(report.Error())
}
out := &bytes.Buffer{}
err := printer.Print(document, nil, out)
if err != nil {
panic(err)
}
fmt.Println(out.String())
}
func ExampleParseComplexDocument() {
input := []byte(`
query {
hello
foo {
bar
}
}
`)
report := &operationreport.Report{}
document := ast.NewSmallDocument()
parser := astparser.NewParser()
printer := &astprinter.Printer{}
document.Input.ResetInputBytes(input)
parser.Parse(document, report)
if report.HasErrors() {
panic(report.Error())
}
out := &bytes.Buffer{}
err := printer.Print(document, nil, out)
if err != nil {
panic(err)
}
fmt.Println(out.String())
}
func ExamplePrintWithIndentation() {
input := []byte(`
query {
hello
foo {
bar
}
}
`)
report := &operationreport.Report{}
document := ast.NewSmallDocument()
parser := astparser.NewParser()
document.Input.ResetInputBytes(input)
parser.Parse(document, report)
if report.HasErrors() {
panic(report.Error())
}
out, err := astprinter.PrintStringIndent(document, nil, " ")
if err != nil {
panic(err)
}
fmt.Println(out)
}
func ExampleParseOperationNameAndType() {
input := []byte(`
query MyQuery {
hello
foo {
bar
}
}
`)
report := &operationreport.Report{}
document := ast.NewSmallDocument()
parser := astparser.NewParser()
document.Input.ResetInputBytes(input)
parser.Parse(document, report)
if report.HasErrors() {
panic(report.Error())
}
operationCount := 0
var (
operationNames []string
operationTypes []ast.OperationType
)
for _, node := range document.RootNodes {
if node.Kind != ast.NodeKindOperationDefinition {
continue
}
operationCount++
name := document.RootOperationTypeDefinitionNameString(node.Ref)
operationNames = append(operationNames, name)
operationType := document.RootOperationTypeDefinitions[node.Ref].OperationType
operationTypes = append(operationTypes, operationType)
}
fmt.Println(operationCount)
fmt.Println(operationNames)
}
func ExampleNormalizeDocument() {
input := []byte(`
query MyQuery {
hello
hello
foo {
bar
bar
}
...MyFragment
}
fragment MyFragment on Query {
hello
foo {
bar
}
}
`)
schema := []byte(`
type Query {
hello: String
foo: Foo
}
type Foo {
bar: String
}
`)
report := &operationreport.Report{}
document := ast.NewSmallDocument()
parser := astparser.NewParser()
document.Input.ResetInputBytes(input)
parser.Parse(document, report)
if report.HasErrors() {
panic(report.Error())
}
schemaDocument := ast.NewSmallDocument()
schemaParser := astparser.NewParser()
schemaDocument.Input.ResetInputBytes(schema)
schemaParser.Parse(schemaDocument, report)
if report.HasErrors() {
panic(report.Error())
}
err := asttransform.MergeDefinitionWithBaseSchema(schemaDocument)
if err != nil {
panic(err)
}
normalizer := astnormalization.NewWithOpts(
astnormalization.WithExtractVariables(),
astnormalization.WithInlineFragmentSpreads(),
astnormalization.WithRemoveFragmentDefinitions(),
astnormalization.WithRemoveNotMatchingOperationDefinitions(),
)
normalizer.NormalizeNamedOperation(document, schemaDocument, []byte("MyQuery"), report)
if report.HasErrors() {
panic(report.Error())
}
out, err := astprinter.PrintStringIndent(document, nil, " ")
if err != nil {
panic(err)
}
fmt.Println(out)
}
func ExampleValidateDocument() {
schemaDocument := ast.NewSmallDocument()
operationDocument := ast.NewSmallDocument()
report := &operationreport.Report{}
validator := astvalidation.DefaultOperationValidator()
validator.Validate(schemaDocument, operationDocument, report)
if report.HasErrors() {
panic(report.Error())
}
}
func ExampleGenerateCacheKey() {
operationDocument := ast.NewSmallDocument()
schemaDocument := ast.NewSmallDocument()
report := &operationreport.Report{}
normalizer := astnormalization.NewWithOpts(
astnormalization.WithExtractVariables(),
astnormalization.WithInlineFragmentSpreads(),
astnormalization.WithRemoveFragmentDefinitions(),
astnormalization.WithRemoveNotMatchingOperationDefinitions(),
)
normalizer.NormalizeNamedOperation(operationDocument, schemaDocument, []byte("MyQuery"), report)
printer := &astprinter.Printer{}
keyGen := xxhash.New()
err := printer.Print(operationDocument, schemaDocument, keyGen)
if err != nil {
panic(err)
}
_, err = keyGen.Write(operationDocument.Input.Variables)
if err != nil {
panic(err)
}
key := keyGen.Sum64()
fmt.Printf("%x", key)
}
func ExampleGenerateCacheKeyWithStaticOperationName() {
staticOperationName := []byte("O")
operationDocument := ast.NewSmallDocument()
schemaDocument := ast.NewSmallDocument()
report := &operationreport.Report{}
normalizer := astnormalization.NewWithOpts(
astnormalization.WithExtractVariables(),
astnormalization.WithInlineFragmentSpreads(),
astnormalization.WithRemoveFragmentDefinitions(),
astnormalization.WithRemoveNotMatchingOperationDefinitions(),
)
nameRef := operationDocument.Input.AppendInputBytes(staticOperationName)
for _, node := range operationDocument.RootNodes {
if node.Kind != ast.NodeKindOperationDefinition {
continue
}
name := operationDocument.OperationDefinitionNameString(node.Ref)
if name != "MyQuery" {
continue
}
operationDocument.OperationDefinitions[node.Ref].Name = nameRef
}
normalizer.NormalizeNamedOperation(operationDocument, schemaDocument, staticOperationName, report)
printer := &astprinter.Printer{}
keyGen := xxhash.New()
err := printer.Print(operationDocument, schemaDocument, keyGen)
if err != nil {
panic(err)
}
_, err = keyGen.Write(operationDocument.Input.Variables)
if err != nil {
panic(err)
}
key := keyGen.Sum64()
fmt.Printf("%x", key)
}
func ExamplePlanOperation() {
staticDataSource, err := plan.NewDataSourceConfiguration[staticdatasource.Configuration](
"StaticDataSource",
&staticdatasource.Factory[staticdatasource.Configuration]{},
&plan.DataSourceMetadata{
RootNodes: []plan.TypeField{
{
TypeName: "Query",
FieldNames: []string{"hello"},
},
},
},
staticdatasource.Configuration{
Data: `{"hello":"world"}`,
},
)
if err != nil {
panic(err)
}
config := plan.Configuration{
DataSources: []plan.DataSource{
staticDataSource,
},
Fields: []plan.FieldConfiguration{
{
TypeName: "Query",
FieldName: "hello",
DisableDefaultMapping: true,
Path: []string{"hello"},
},
},
IncludeInfo: true,
}
operationDocument := ast.NewSmallDocument()
schemaDocument := ast.NewSmallDocument()
report := &operationreport.Report{}
operationName := "O"
planner := plan.NewPlanner(context.Background(), config)
executionPlan := planner.Plan(operationDocument, schemaDocument, operationName, report)
if report.HasErrors() {
panic(report.Error())
}
fmt.Printf("%+v", executionPlan)
}
func ExampleExecuteOperation() {
var preparedPlan plan.Plan
resolver := resolve.New(context.Background(), true)
ctx := resolve.NewContext(context.Background())
switch p := preparedPlan.(type) {
case *plan.SynchronousResponsePlan:
out := &bytes.Buffer{}
err := resolver.ResolveGraphQLResponse(ctx, p.Response, nil, out)
if err != nil {
panic(err)
}
fmt.Println(out.String())
case *plan.SubscriptionResponsePlan:
}
}
type visitor struct {
walker *astvisitor.Walker
operation, definition *ast.Document
typeFields [][]string
}
func (v *visitor) EnterField(ref int) {
enclosingTypeName := v.walker.EnclosingTypeDefinition.NameString(v.definition)
fieldName := v.operation.FieldNameString(ref)
definitionRef, exists := v.walker.FieldDefinition(ref)
if !exists {
return
}
fieldTypeName := v.definition.FieldDefinitionTypeNameString(definitionRef)
v.typeFields = append(v.typeFields, []string{enclosingTypeName, fieldName, fieldTypeName})
}
func ExampleWalkAST() {
operationDocument := ast.NewSmallDocument()
schemaDocument := ast.NewSmallDocument()
report := &operationreport.Report{}
walker := astvisitor.NewWalker(24)
vis := &visitor{
walker: &walker,
operation: operationDocument,
definition: schemaDocument,
}
walker.RegisterEnterFieldVisitor(vis)
walker.Walk(operationDocument, schemaDocument, report)
if report.HasErrors() {
panic(report.Error())
}
fmt.Printf("%+v", vis.typeFields)
}
I hope this tutorial gave you a good overview of what you can do with this library.
If you have any questions, feel free to open an issue.
Following, here's a list of all the important packages in this library and what problems they solve.
- ast: the GraphQL AST and all the logic to work with it.
- astimport: import GraphQL documents from one AST into another
- astnormalization: normalize a GraphQL document
- astparser: parse a string into a GraphQL AST
- astprinter: print a GraphQL AST into a string
- asttransform: transform a GraphQL AST, e.g. merge it with a base schema
- astvalidation: validate a GraphQL AST against a schema
- astvisitor: walk through a GraphQL AST and execute callbacks for every node
- engine/datasource: the DataSource interface and some implementations
- engine/datasource/graphql_datasource: the GraphQL DataSource implementation, including support for Federation
- engine/plan: plan the execution of a GraphQL document
- engine/resolve: execute the plan
- introspection: convert a GraphQL Schema into an introspection JSON document
- lexer: turn a string containing a GraphQL document into a list of tokens
- playground: add a GraphQL Playground to your Go HTTP server
- subscription: implements GraphQL Subscriptions over WebSockets and SSE
Contributors
- Jens Neuse (Project Lead & Active Maintainer)
- Initial version of graphql-go-tools
- Currently responsible for the loader and resolver implementation
- Sergiy Petrunin 🇺🇦 (Active Maintainer)
- Helped cleaning up the API of the pipeline package
- Refactored the ast package into multiple files
- Author of the introspection converter (introspection JSON -> AST)
- Fixed various bugs in the parser & visitor & printer
- Refactored and enhanced the astimport package
- Current maintainer of the plan package
- Patric Vormstein (Active Maintainer)
- Fixed lexer on windows
- Author of the graphql package to simplify the usage of the library
- Refactored the http package to simplify usage with http servers
- Author of the starwars package to enhance testing
- Refactor of the Subscriptions Implementation
- Mantas Vidutis (Inactive)
- Contributions to the http proxy & the Context Middleware
- Jonas Bergner (Inactive)
- Contributions to the initial version of the parser, contributions to the tests
- Implemented Type Extension merging (deprecated)
- Vasyl Domanchuk (Inactive)
- Implemented the logic to generate a federation configuration
- Added federation example
- Added the initial version of the batching implementation
Contributions
Feel free to file an issue in case of bugs.
We're open to your ideas to enhance the repository.
You are open to contribute via PR's.
Please open an issue to discuss your idea before implementing it so we can have a discussion.
Make sure to comply with the linting rules.
You must not add untested code.