Building a GraphQL API

Learn how to build a GraphQL API using Go and Encore

Encore has great support for GraphQL with its type-safe approach to building APIs.

The best way to use GraphQL with Encore is using gqlgen, which has similar goals as Encore (type-safe APIs, minimal boilerplate, code generation, etc).

Encore's automatic tracing also makes it easy to find and fix performance issues that often arise in GraphQL APIs (like the N+1 problem).

The final code will look like this:

Project

1. Create your Encore application

This tutorial uses the REST API tutorial as a starting point.

You can either follow that tutorial first, or you can create a new Encore application using the url-shortener template by running:

$ encore app create --example=url-shortener

2. Initialize gqlgen

To get started, initialize gqlgen by creating a tools.go file in the application root:

tools.go
//go:build tools package tools import ( _ "github.com/99designs/gqlgen" _ "github.com/99designs/gqlgen/graphql/introspection" )

Then run go mod tidy to download the dependencies.

Next, create a gqlgen.yml file in the application root containing:

gqlgen.yml
# Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - graphql/*.graphqls # Where should the generated server code go? exec: filename: graphql/generated/generated.go package: generated # Where should any generated models go? model: filename: graphql/model/models_gen.go package: model # Where should the resolver implementations go? resolver: layout: follow-schema dir: graphql package: graphql # gqlgen will search for any type names in the schema in these go packages # if they match it will use them, otherwise it will generate them. autobind: - "encore.app/url" # This section declares type mapping between the GraphQL and go type systems # # The first line in each type will be used as defaults for resolver arguments and # modelgen, the others will be allowed when binding to fields. Configure them to # your liking models: ID: model: - github.com/99designs/gqlgen/graphql.ID - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Int: model: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32

3. Create Encore service

Now it's time to create our Encore service that will provide the GraphQL API.

First generate the gqlgen boilerplate:

$ mkdir -p graphql/generated graphql/model
$ echo "package model" > graphql/model/model.go
$ go run github.com/99designs/gqlgen generate

This will create a bunch of files in the graphql directory.

Next, create a graphql/service.go file containing:

graphql/service.go
// Service graphql exposes a GraphQL API. package graphql import ( "net/http" "encore.app/graphql/generated" "encore.dev" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" ) //go:generate go run github.com/99designs/gqlgen generate //encore:service type Service struct { srv *handler.Server playground http.Handler } func initService() (*Service, error) { srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{}})) pg := playground.Handler("GraphQL Playground", "/graphql") return &Service{srv: srv, playground: pg}, nil } //encore:api public raw path=/graphql func (s *Service) Query(w http.ResponseWriter, req *http.Request) { s.srv.ServeHTTP(w, req) } //encore:api public raw path=/graphql/playground func (s *Service) Playground(w http.ResponseWriter, req *http.Request) { // Disable playground in production if encore.Meta().Environment.Type == encore.EnvProduction { http.Error(w, "Playground disabled", http.StatusNotFound) return } s.playground.ServeHTTP(w, req) }

This creates an Encore service that exposes the /graphql and /graphql/playground endpoints.

It also adds a //go:generate directive that lets you re-run the gqlgen code generation by running go generate ./graphql.

4. Add GraphQL schema

Now it's time to define the GraphQL schema. Create a graphql/schema.graphqls file containing:

graphql/url.graphqls
type Query { urls: [URL!]! get(id: ID!): URL! } type Mutation { shorten(input: String!): URL! } type URL { id: ID! # shortened id url: String! # full URL }

Then, re-run the code generation to generate the resolver stubs:

$ go generate ./graphql

The stubs will be written to graphql/url.resolvers.go and will contain a bunch of unimplemented resolver methods that look something like this:

// Shorten is the resolver for the shorten field. func (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) { panic(fmt.Errorf("not implemented: Shorten - shorten")) }

5. Implement resolvers

Now, modify the resolvers to call the url service. Since the GraphQL API uses the same types (thanks to the autobind directive in gqlgen.yml) as the Encore API exposes, we can just call the endpoints directly. Implement the resolvers in graphql/url.resolvers.go like this:

graphql/url.resolvers.go
// Shorten is the resolver for the shorten field. func (r *mutationResolver) Shorten(ctx context.Context, input string) (*url.URL, error) { return url.Shorten(ctx, &url.ShortenParams{URL: input}) } // Urls is the resolver for the urls field. func (r *queryResolver) Urls(ctx context.Context) ([]*url.URL, error) { resp, err := url.List(ctx) if err != nil { return nil, err } return resp.URLs, nil } // Get is the resolver for the get field. func (r *queryResolver) Get(ctx context.Context, id string) (*url.URL, error) { return url.Get(ctx, id) }

As you can see, the resolvers are just thin wrappers around the Encore API endpoints themselves.

6. Trying it out

With that, the GraphQL API is done! Try it out by running encore run and opening up the playground.

Enter the query:

mutation { shorten(input: "https://encore.dev") { id } }

You should get back an id like MnTWA8Jo. Pass the id you got (it will be something different) to a get query:

query { get(id: "<your-id-here>") { url } }

And you should get back https://encore.dev.

7. Deploy to the cloud

Push your changes and deploy your application to Encore's free development cloud by running:

$ git add -A .
$ git commit -m 'Initial commit'
$ git push encore

Encore will now build and test your app, provision the needed infrastructure, and deploy your application to the cloud.

After triggering the deployment, you will see a URL where you can view its progress in Encore's Cloud Dashboard. It will look something like: https://app.encore.dev/$APP_ID/deploys/...

From there you can also see metrics, traces, link your app to a GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for production deployment.

Celebrate with fireworks

Now that your app is running in the cloud, let's celebrate with some fireworks:

🥐 In the Cloud Dashboard, open the Command Menu by pressing Cmd + K (Mac) or Ctrl + K (Windows/Linux).

From here you can easily access all Cloud Dashboard features and for example jump straight to specific services in the Service Catalog or view Traces for specific endpoints.

🥐 Type fireworks in the Command Menu and press enter. Sit back and enjoy the show!

Fireworks

Conclusion

We've now built a GraphQL API gateway that forwards requests to the application's underlying Encore services in a type-safe way with minimal boilerplate.

Note that the concepts discussed here are general and can be easily adapted to any GraphQL schema.

Whenever you make a change to the schema or configuration, re-run go generate ./graphql to regenerate the GraphQL boilerplate. And for more information on how to use gqlgen, see the gqlgen documentation.