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.
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:
- Federation Gateway
- OpenTelemetry Metrics & Distributed Tracing
- Prometheus Metrics
- GraphQL Schema Usage Exporter
- Health Checks
- GraphQL Playground
- Execution Tracing Exporter & UI in the Playground
- Federated Subscriptions over WebSockets (graphql-ws & graphql-transport-ws protocol support) and SSE
- Authentication using JWKS & JWT
- Highly available & scalable using S3 as a backend for the Router Config
- Persisted Operations / Trusted Documents
- Traffic Shaping (Timeouts, Retries, Header & Body Size Limits, Subgraph Header forwarding)
- Custom Modules & Middleware
This repository contains multiple packages joined via workspace.
Package | Description | Package dependencies | Maintenance state |
---|---|---|---|
graphql-go-tools v2 | GraphQL engine implementation consisting of lexer, parser, ast, ast validation, ast normalization, datasources, query planner and resolver. Supports GraphQL Federation. Has built-in support for batching federation entity calls | - | actual version, active development |
execution | Execution helpers for the request handling and engine configuration builder | depends on graphql-go-tools v2 and composition | actual version |
examples/federation | Example implementation of graphql federation gateway. This example is not production ready. For production ready solution please consider using cosmo router | depends on execution package | actual federation gateway example |
graphql-go-tools v1 | Legacy GraphQL engine implementation. This version 1 package is in maintenance mode and accepts only pull requests with critical bug fixes. All new features will be implemented in the version 2 package only. | - | deprecated, maintenance mode |
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.
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
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"
)
/*
ExampleParsePrintDocument shows you the most basic usage of the library.
It parses a GraphQL document and prints it back to a writer.
*/
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()) // Output: query { hello }
}
/*
Okay, that was easy, but also not very useful.
Let's try to parse a more complex document and print it back to a writer.
*/
// ExampleParseComplexDocument shows a special feature of the printer
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()) // Output: query { hello foo { bar } }
}
/*
You'll notice that the printer removes all whitespace and newlines.
But what if we wanted to print the document with indentation?
*/
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)
// Output: query {
// hello
// foo {
// bar
// }
// }
}
/*
Okay, fantastic. We can parse and print GraphQL documents.
As a next step, we could analyze the document and extract some information from it.
What if we wanted to know the name of the operation in the document, if any?
And what if we wanted to know about the Operation type?
*/
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) // Output: 1
fmt.Println(operationNames) // Output: [MyQuery]
}
/*
We've now seen how to analyze the document and learn a bit about it.
We could now add some validation to our application,
e.g. we could check for the number of operations in the document,
and return an error if there are multiple anonymous operations.
We could also validate the Operation content against a schema.
But before we do this, we need to normalize the document.
This is important because validation relies on the document being normalized.
It was much easier to build the validation and many other features on top of a normalized document.
Normalization is the process of transforming the document into a canonical form.
This means that the document is transformed in a way that makes it easier to reason about it.
We inline fragments, we remove unused fragments,
we remove duplicate fields, we remove unused variables,
we remove unused operations etc...
So, let's normalize the document!
*/
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())
}
// graphql-go-tools is very strict about the schema
// the above GraphQL Schema is not fully valid, e.g. the `schema { query: Query }` part is missing
// we can fix this automatically by merging the schema with a base schema
err := asttransform.MergeDefinitionWithBaseSchema(schemaDocument)
if err != nil {
panic(err)
}
// you can customize what rules the normalizer should apply
normalizer := astnormalization.NewWithOpts(
astnormalization.WithExtractVariables(),
astnormalization.WithInlineFragmentSpreads(),
astnormalization.WithRemoveFragmentDefinitions(),
astnormalization.WithRemoveNotMatchingOperationDefinitions(),
)
// It's generally recommended to always give your operation a name
// If it doesn't have a name, just add one to the AST before normalizing it
// This is not strictly necessary, but ensures that all normalization rules work as expected
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)
// Output: query MyQuery {
// hello
// foo {
// bar
// }
// }
}
/*
Okay, that was a lot of work, but now we have a normalized document.
As you can see, all the duplicate fields have been removed and the fragment has been inlined.
What can we do with it?
Well, the possibilities are endless,
but why don't we start with validating the document against a schema?
Alright. Let's do it!
*/
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())
}
}
/*
Fantastic, we've now got a GraphQL document that is valid against a schema.
As a next step, we could generate a cache key for the document.
This is very useful if we want to start doing expensive operations afterward that could be de-duplicated or cached.
At the same time, generating a cache key from a normalized document is not as trivial as it sounds.
Let's take a look!
*/
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)
}
// you might be thinking that we're done now, but we're not
// we've extracted