# genqlient, and the Go client that stops you shipping a typo in a query

> genqlient is a Go library that generates type-safe GraphQL client code from your schema, so a field that does not exist or a variable of the wrong type is a compile error rather than a runtime failure. It is MIT licensed, used in production at Khan Academy, and the cost is a code generation step.

**Khan/genqlient** — a truly type-safe Go GraphQL client

- Repository: https://github.com/Khan/genqlient
- Stars: 1,323 · Forks: 144
- Language: Go
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/khan-genqlient

## The three guarantees the project makes

genqlient is a Go library to easily generate type-safe code to query a GraphQL API, and the argument for it is that both GraphQL and Go are typed languages, so the compiler can check that your code is making a valid GraphQL query and using the result correctly, with a minimum of boilerplate.

Three guarantees are listed.

Compile-time validation of GraphQL queries, phrased as never shipping an invalid GraphQL query again. Type-safe response objects, because genqlient generates the right type for each query, so you know the response will unmarshal correctly and never need to use interface{}. And production readiness, which the project backs with the fact that genqlient is used in production at Khan Academy, where it supports millions of learners and teachers around the world.

That third point is the one to weigh carefully, because it is the only claim here that comes with a scale. The rest is a design argument, and the design argument is strong.

The audience is Go teams that have a GraphQL API and have been bitten by the failure mode this library removes. The tool is not a general GraphQL client in the sense of a runtime library you call; it is a code generator that produces a client, and the difference matters for everything from the build process to the debugging story.

## What goes wrong with a hand-written response struct

The project explains itself by showing the code it replaces, and the example is short enough that the three problems are visible.

```go
query := `query GetUser($id: ID!) { user(id: $id) { name } }`
variables := map[string]interface{}{"id": "123"}
var resp struct {
	Me struct {
		Name graphql.String
	}
}
client.Query(ctx, query, &resp, variables)
fmt.Println(resp.Me.Name)
```

The code works. The problems are what it cannot catch.

While the response struct is type-safe at the Go level, there is nothing to check that the schema looks like you expect. Maybe the field is called fullName, not name, or maybe you capitalized it wrong, since Go and GraphQL have different conventions, and you will not know until runtime.

The GraphQL variables are not type-safe at all. You could have passed {"id": true} and again you will not know until runtime.

And you have to write everything twice, or hide the query in complicated struct tags, or give up the type safety you do have and resort to interface{}.

The last point is the one that hurts most in practice, because the escape hatches are all downhill. A struct tag that holds a query is a string that nothing checks, and interface{} is a hole in the type system that spreads.

The project's answer is that these problems should be entirely avoidable, since GraphQL and Go are both typed languages and GraphQL servers expose their schema in a standard, machine-readable format. Write a query and have it automatically validated against the schema and turned into a Go struct.

## Query in, generated Go out, against a real schema

The usage instructions are brief. You download and run genqlient the usual way, with `go run github.com/Khan/genqlient`, and the getting started guide and the example directory in the repository show how to set a project up.

The example directory is the best way to see the shape of the whole thing, because it contains the artefacts involved. There is a schema.graphql, which is the schema of the API you are talking to, a genqlient.graphql file with your queries, a genqlient.yaml with the configuration, a generated.go with the code that was produced, and a main.go that uses it, plus a README.

That is the model. The schema is the input, the queries are the input, the configuration says where those inputs live and where the output goes, and the generated file is what your application imports. Everything is checked at generation time against the schema, and what lands in generated.go is ordinary Go code with ordinary Go types.

The repository also holds a docs directory with the getting started guide, the contribution guidelines and a design note, and the design note is where the prior art is discussed, including the popular server library 99designs/gqlgen that generates types and the Apollo codegen tool that generates similar client types for several other languages.

That comparison is the interesting one. genqlient is on the client side of the same idea, and the design note collects the thinking rather than leaving it in a pull request, which is what a library at version 0.8 should do if it is going to attract contributors.

## The dependencies tell you what it validates with

The module file describes a tool built on two GraphQL libraries and one testing library, and the choices are informative.

vektah/gqlparser is the GraphQL parser, and it is the component that reads the schema and parses the query documents. 99designs/gqlgen is the server side library that also generates types, and it is a dependency here, so genqlient is building on the same parsing and type generation machinery that the popular Go server framework uses.

For configuration, alexflint/go-arg gives the command line flags, and go.yaml.in/yaml/v3 reads the YAML configuration file. bmatcuk/doublestar/v4 is the glob library, which is how a project that keeps its queries in a directory or a set of files tells the tool to find them.

The testing dependency is the one that deserves a mention. bradleyjkemp/cupaloy is a snapshot testing library, and its presence tells you how the project checks its own output. The generated code is the product here, so the way to test a code generator is to run it and compare the result against a recorded snapshot, which is exactly what snapshot testing is for. When a schema or a query changes, the snapshot changes with it, and the diff in the pull request shows whether the generated code changed in a way the author intended.

golang.org/x/tools is there for the code analysis the generator does on the query files, and gorilla/websocket and google/uuid are the smaller runtime pieces.

The module requires Go 1.23.0 with a toolchain directive of go1.23.3, and it is a pre-1.0 module, so the versioning convention means the minor version can carry breaking changes.

## Development workflow: go generate, snapshot tests and a pinned linter

The Makefile is short and shows the project's own loop.

```bash
go generate ./...
go run ./example
```

The example target runs the generator over the project and then runs the example, which is the smoke test: if the generated code does not compile or the example fails, the change is broken.

The lint target is unusual in a good way.

```bash
( cd internal/lint && go build -o golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint )
internal/lint/golangci-lint run ./... --fix
```

Instead of depending on a released golangci-lint binary, the project builds the linter from source into internal/lint and runs that. The linter version is therefore pinned by the module file, which removes the class of problem where a new linter release changes the diagnostics and turns every pull request red. There is a .golangci.yml at the top level for the configuration, and a .golangci.yml alongside the generated code would not be needed, because the generated output is excluded from most lint rules in a project of this kind.

The check target depends on lint and then runs the tests with the race detector and coverage, followed by go mod tidy to keep the module file clean.

```bash
go test -race -cover ./...
go mod tidy
```

The top level layout matches that workflow, with a generate directory for the generator's own sources, a graphql directory for the query handling, an internal directory for the implementation and the linter build, an example directory, and main.go at the root.

## Version 0.8.1, and what code generation costs you

The releases are v0.8.1 from 2025-05-18, v0.8.0 from 2025-02-03 and v0.7.0 from 2024-03-05. The repository is not archived, and the last push was on 2026-09-16, so the project is being worked on between releases.

The version number is worth pausing on. genqlient is still below 1.0, which in Go's module semantics means the minor version can carry breaking changes, and the dependency list shows a 0.x library in a production codebase at Khan Academy. That is a normal trade for a code generator, since the generated output is checked in and the generator version is a build time concern rather than a runtime one, but it is a fact to be aware of when you upgrade the tool and regenerate.

The licence is MIT, the code of conduct is the Contributor Covenant 2.1, and the project has a Go reference badge, a test status badge and a Go report card badge in the header, which is the standard trio for a Go library.

The cost that comes with this approach is the one worth spelling out. A generated client is only as current as the schema it was generated against, so a project needs a way to know when the schema has moved. That means either regenerating on a schedule or, better, generating inside the build so a schema change breaks the build rather than the runtime. The example target in the Makefile is that idea applied to the project itself, and a team adopting genqlient should copy it rather than regenerating by hand when someone remembers.

There is also the ordinary cost of a code generation step in a build pipeline, and the ordinary benefit, which is that a query that no longer matches the schema stops being a runtime error and becomes a failed build.

## Conclusion

Adopt genqlient if you have a GraphQL API with a schema you control or can fetch, and you want the compiler to catch a misspelled field or a wrongly typed variable before it reaches production. Do not adopt it if your queries are assembled at runtime from user input, since the whole model is that the query is written once and validated once. Before you commit, run the example in the repository, which contains a schema, a query file and a generated file, and check where the schema comes from in your setup, because a generated client is only as current as the schema it was generated against.

## FAQ

### What does genqlient generate and how do I run it?

It generates type-safe Go code for your GraphQL queries, validated against the schema. You run it the usual way with `go run github.com/Khan/genqlient`, and the getting started guide and the example directory show the project layout, which includes a schema file, a queries file, a YAML configuration and the generated output.

### What problems does genqlient solve compared with a hand-written response struct?

A hand-written struct cannot catch a field that does not exist in the schema, cannot check the type of the GraphQL variables, and forces you to write the query twice or fall back to interface{}. genqlient generates the response type for each query and validates the query at compile time.

### Is genqlient used in production?

Yes. The project states that genqlient is used in production at Khan Academy, where it supports millions of learners and teachers around the world.

### How does genqlient check the GraphQL queries it generates from?

It parses the schema and the query documents with the vektah/gqlparser library, and it depends on 99designs/gqlgen, the popular Go GraphQL server library that generates types. Its own output is checked with cupaloy snapshot tests, so a change in the generated code shows up in a diff.

### What is the current version of genqlient?

The releases are v0.8.1 from 2025-05-18, v0.8.0 from 2025-02-03 and v0.7.0 from 2024-03-05, and the last push to the repository was on 2026-09-16. The module requires Go 1.23.0 and is MIT licensed.

## Sources

- [Issues](https://github.com/Khan/genqlient/issues)
- [Khan/genqlient on GitHub](https://github.com/Khan/genqlient)
- [License: MIT](https://github.com/Khan/genqlient/blob/main/LICENSE)
- [README](https://github.com/Khan/genqlient/blob/main/README.md)
- [Releases](https://github.com/Khan/genqlient/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/khan-genqlient
