Skip to content

GraphQL Router / API Gateway framework written in Golang, focussing on correctness, extensibility, and high-performance. Supports Federation v1 & v2, Subscriptions & more.

License

Notifications You must be signed in to change notification settings

wundergraph/graphql-go-tools

Repository files navigation

GoDoc v2-ci

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:

  • 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

State of the packages

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

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"
)

/*
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