graphql-go

repository·main·Indexed 26 days ago

https://github.com/graph-gophers/graphql-go

A Go library providing full support for the GraphQL specification (targeting September 2025). It features schema type-checking, parallel resolver execution, and compatibility with the Apollo Federation Subgraph specification (Federation 1 and 2). The library supports OpenTelemetry and OpenTracing, and allows for custom schema configuration via SchemaOpt, including options for field resolvers, depth limits, and memory pooling.

Tokens
5.8K
Snippets
17
Records
34
Agent score
88%

What's inside graphql-go

  1. Get started with graphql-go

    main

    To run a simple GraphQL server locally, create a main.go file with a schema definition and a resolver struct. Use graphql.MustParseSchema to initialize the schema and github.com/graph-gophers/graphql-go/relay to handle HTTP requests.

    package main
    
    import (
        "log"
        "net/http"
    
        graphql "github.com/graph-gophers/graphql-go"
        "github.com/graph-gophers/graphql-go/relay"
    )
    
    type query struct{}
    
    func (query) Hello() string { return "Hello, world!" }
    
    func main() {
        s := `
            type Query {
                    hello: String!
            }
        `
        schema := graphql.MustParseSchema(s, &query{})
        http.Handle("/query", &relay.Handler{Schema: schema})
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
  2. Use separate resolvers for Query, Mutation, and Subscription

    main

    To avoid name collisions when the same field name exists in different operations (e.g., Query.hello and Mutation.hello), you can define a root resolver that returns specialized resolvers for each operation using the Query(), Mutation(), and Subscription() methods.

    type RootResolver struct{}
    type QueryResolver struct{}
    type MutationResolver struct{}
    
    func(r *RootResolver) Query() *QueryResolver {
        return &QueryResolver{}
    }
    
    func(r *RootResolver) Mutation() *MutationResolver {
        return &MutationResolver{}
    }
    
    func (*QueryResolver) Hello() string {
        return "Hello query!"
    }
    
    func (*MutationResolver) Hello() string {
        return "Hello mutation!"
    }
    
    schema := graphql.MustParseSchema(sdl, &RootResolver{}, nil)
  3. Implement GraphQL resolvers

    main

    A resolver must have an exported method or field for each field in the GraphQL type. Method names must match the schema field names (case-insensitive).

    Method Signatures:

    • Simple: func (r *Resolver) Field() ReturnType
    • With Context: func (r *Resolver) Field(ctx context.Context) (ReturnType, error)
    • With Arguments: func (r *Resolver) Field(ctx context.Context, args *ArgsStruct) (ReturnType, error). The ArgsStruct must have exported fields matching the GraphQL argument names (case-insensitive).

    Using Struct Fields as Resolvers: By default, resolvers are methods. To use struct fields as resolvers, pass graphql.UseFieldResolvers() in the SchemaOpt slice. A struct field is used only if there is no matching method, no interface method implementation, and no arguments.

    // Simple resolver method
    func (r *helloWorldResolver) Hello() string {
        return "Hello world!"
    }
    
    // Resolver with context and error
    func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
        return "Hello world!", nil
    }
    
    // Enabling struct field resolvers
    opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
    schema := graphql.MustParseSchema(s, &query{}, opts...)
  4. Run the Apollo Federation integration example

    main

    This example demonstrates how to integrate graphql-go as an Apollo Federation subgraph. To run the full federation setup, you need to start two subgraphs and one gateway.

    Prerequisites:

    • Go v1.18
    • Node.js v16.14.2
    • yarn 1.22.18

    Steps:

    1. Start the first subgraph: go run ./example/apollo-federation/subgraph-one/server.go
    2. Start the second subgraph: go run ./example/apollo-federation/subgraph-two/server.go
    3. Start the gateway: cd example/apollo-federation/gateway && yarn start
    4. Access the gateway at localhost:4000 to interact with the federated graph.
    go run ./example/apollo-federation/subgraph-one/server.go
    go run ./example/apollo-federation/subgraph-two/server.go
    cd example/apollo-federation/gateway
    yarn start
  5. Configure Tracing with OpenTelemetry or OpenTracing

    main

    The library supports tracing via OpenTelemetry and OpenTracing. You can provide a tracer using the graphql.Tracer() option during schema parsing.

    OpenTelemetry Example:

    import (
        "github.com/graph-gophers/graphql-go"
        otelgraphql "github.com/graph-gophers/graphql-go/trace/otel"
    )
    
    _, err := graphql.ParseSchema(sdl, nil, graphql.Tracer(otelgraphql.DefaultTracer()))

    OpenTracing Example:

    import (
        "github.com/graph-gophers/graphql-go"
        "github.com/graph-gophers/graphql-go/trace/opentracing"
    )
    
    _, err := graphql.ParseSchema(sdl, nil, graphql.Tracer(opentracing.Tracer{}))
  6. Test the `_entities` resolver with GraphiQL

    main

    To manually test how the server handles federated entities, you can use the GraphiQL interface (typically at /graphiql). Use the _entities query with _Any type representations to verify that different keys and types are resolved correctly.

    query ($representations: [_Any!]!) {
        _entities(representations: $representations) {
            __typename
            ...on DeprecatedProduct { sku package reason }
            ...on Product { id sku createdBy { email name } }
            ...on ProductResearch { study { caseNumber description } }
            ...on User { email name }
        }
    }
    
    # Variables:
    {
        "representations": [
            {
                "__typename": "DeprecatedProduct",
                "sku": "apollo-federation-v1",
                "package": "@apollo/federation-v1"
            },
            {
                "__typename": "ProductResearch",
                "study": {
                    "caseNumber": "1234"
                }
            },
            { "__typename": "User", "email": "support@apollographql.com" },
            {
                "__typename": "Product",
                "id": "apollo-federation"
            },
            {
                "__typename": "Product",
                "sku": "federation",
                "package": "@apollo/federation"
            },
            {
                "__typename": "Product",
                "sku": "studio",
                "variation": { "id": "platform" }
            }
        ]
    }
  7. Inspect selected fields in resolvers

    main

    To avoid N+1 problems or build projection lists for databases, resolvers can inspect which immediate child fields were requested using the following helpers (requires field selection capturing to be enabled, which is the default):

    • graphql.SelectedFieldNames(ctx) []string: Returns names of direct child schema fields.
    • graphql.HasSelectedField(ctx, "name") bool: Returns true if a specific field was requested.
    • graphql.SortedSelectedFieldNames(ctx) []string: Returns a sorted copy of selected field names.

    Note: These helpers are shallow and exclude meta fields like __typename.

  8. Implement Custom Errors with Extensions

    main

    To include custom metadata in GraphQL error responses, implement the ResolverError interface. This allows you to add an extensions field to the error JSON.

    type ResolverError interface {
        error
        Extensions() map[string]any
    }
    
    type droidNotFoundError struct {
        Code    string `json:"code"`
        Message string `json:"message"`
    }
    
    func (e droidNotFoundError) Error() string {
        return fmt.Sprintf("error [%s]: %s", e.Code, e.Message)
    }
    
    func (e droidNotFoundError) Extensions() map[string]any {
        return map[string]any{
            "code":    e.Code,
            "message": e.Message,
        }
    }