Automated code generation from GraphQL operations

At Nextdoor, we’re constantly iterating our frontend web product to give neighbors the best experience, often releasing new versions with significant code changes multiple times per day. Two of the tools we use to keep our development velocity high are GraphQL (a popular query language for APIs) and Flow (a static type checker for JavaScript). These tools ensure that our React web app can anticipate the shape of network responses from our web server, helping us make changes quickly and safely.

In this article, we’ll go over how to implement a system to generate Flow types for GraphQL. Using the GraphQL.js library and @babel/generator, we’ll walk through a step-by-step process to translate a GraphQL operation to Flow type definitions. The code we’ll show you here is the basis for the system we use in production.

GraphQL, Babel, and Flow, which we’ll be using here

The Problem

In 2019, we adopted GraphQL and were encouraged by the increase in developer speed we observed, but ran into problems generating our frontend Flow types in a way that kept our types and pull requests (PRs) easy to read. Existing solutions for generating Flow types from GraphQL operations didn’t quite fit our needs. In particular, we needed:

  1. A type generator written for Flow, as opposed to TypeScript (a more popular alternative to Flow). We found that existing type generators were written with TypeScript in mind, which led to syntax errors when using Flow.
  2. A type generator that would keep our PRs easy to read. We also found that existing type generators created a lot of duplicate code when we changed a commonly-used fragment, which would make code reviews difficult to read. For example, Nextdoor has a common Post fragment that is used across many operations. Updating that fragment would update a number of generated files for each operation rather than a single generated file specific to that fragment.
  3. Generated code that is easy to read and import from. We often define React prop types and test data that use specific field selections from an operation, and we wanted the ability to import Flow types for those selections.

Let’s look at a concrete example with those requirements in mind. A simple GraphQL schema might look something like this:

A simple operation on this schema might query for a neighborhood’s name by its numeric ID. Let’s use a fragment definition to make the generated types more interesting:

Given this operation, our Flow type generator should output Flow types that look like this:

This satisfies our requirements above:

  1. Valid flow code
  2. Human-readable
  3. Each field exported separately, so that they can individually be used in React prop types or when defining test data

Writing a Type Generator

We can write a type generator to achieve this output in just a few hundred lines of JavaScript. Here, we’ll write a generator in a single file generator.mjs and run it in Node.js to test it out. The approach we’ll follow looks like this:

The first input to our type generator is our GraphQL schema, which we need in order to understand the shape of our data graph. This lets us know the type of each field in our operations, and whether those types are interfaces or concrete. We can get a GraphQLSchema object from a schema string using the buildSchema function from GraphQL.js:

The second input to our type generator is the AST (abstract syntax tree) of the GraphQL operation that we’re generating types for. An AST is a tree of nodes where each node denotes a structural part of our GraphQL operation. Representing the operation as a tree allows us to process it into generated types more easily. We can get the AST for an operation using the parse function from GraphQL.js:

We can convert the value of operationAST to JSON and then print it to inspect the tree. Alternatively, we can paste our GraphQL document into the handy tool at astexplorer.net (click the link to try it out!) which provides a nice UI to expand or collapse individual nodes in the tree:

We can see that specific parts of our GraphQL operation have been turned into nodes in our tree. For example $id: ID! corresponds to a VariableDefinition with a NonNullType containing a NamedType. Our FragmentDefinition on Neighborhood contains a SelectionSet with two Field children. We’ll pass this whole tree to our generator function.

Given these inputs, we can start writing our generator function, which we’ll name generateTypesForOperation:

Our generator is based around the visit function from GraphQL.js, which walks depth-first through the operation AST we built. By default, the visit function doesn’t have knowledge of the schema the operation is defined against. We need to give it that knowledge by passing it a TypeInfo object built from our schema, which helps us keep track of what type we’re currently looking at as we traverse the tree. This is necessary to generate __typename fields in our Flow types.

What should our visitor function do as it walks the tree? Just as we can use an AST to represent a GraphQL operation as a tree, we can use an AST to represent JavaScript code as a tree, including Flow types. With that in mind, let’s write our visitor function to translate our GraphQL AST into a JavaScript AST that contains the Flow types we need.

To figure out what our JavaScript AST should look like, we can paste our desired output (which we defined in our problem statement above) into astexplorer.net (click the link to try it out!). This AST is a bit more complicated than our GraphQL operation AST, but we can click through it to figure out what we need to output:

Some of the key nodes here are ExportNamedDeclaration, which correspond to our export type statements, and ObjectTypeProperty, which map an identifier to a value, such as id: string.

Let’s go ahead and start writing the translation logic to output this AST for our operation. We’ll use the @babel/types library to create our JavaScript AST:

The enter and leave functions that we return from visitor are called by gql.visit whenever we enter or leave a node while walking our AST, respectively. On entering a node, all we need to do is update our TypeInfo object (which, remember, we’ll use to keep track of what the type of the current node is). Most of our program generation goes in the leave function, which means we’ll start translating our GraphQL AST to a JavaScript AST from the leaves at the bottom of the tree, then move on upwards.

From here on, it’s just a matter of writing the functions that translate from our GraphQL AST nodes to corresponding JavaScript AST nodes. Because we have to handle each type of GraphQL node in our switch statement, our visitor function can end up becoming a bit large, but a runnable implementation of some basic logic is available here.

With that logic in place, we just need to convert our output JavaScript AST into a string, which we can do using the @babel/generator library:

Now our generateTypesForOperation function should successfully be returning generated Flow types for our operation. We can write a build script to run this function for every operation in our codebase and save the results to a set of generated files. We can also write some Jest tests to ensure that the function continues to work as we expect.

Although our code here omits some GraphQL language features, this is the basic system we use to generate Flow types day-to-day for Nextdoor’s frontend.

Future Improvements

The type generator we’ve built here works for our simple GraphQL operation, but there are other problems that arise. A couple of improvements we built on this base at Nextdoor include:

  • Support for more GraphQL language features, like aliases, inline fragments, nullability, custom scalars, list types, and more complex selection sets.
  • Handling dependencies between different operation type files. At Nextdoor, we use @graphql-codegen’s near-operation-file-preset to run our custom code generator for each GraphQL operation, and to generate import statements.

We could also use these tools to generate other interesting artifacts. For example, we could generate automatic test data for each of our GraphQL operations, for use in mocked network responses during tests. Or we could generate a graph visualization of operations across our codebase and which fields they access.

Conclusion

GraphQL helps us quickly develop features for Nextdoor’s web frontend, but existing solutions to generate Flow types from our GraphQL operations didn’t quite fit our needs. With a few hundred lines of JavaScript, we were able to translate our GraphQL operations into Flow type files that we now use across our frontend codebase. The same tools to parse GraphQL operations into ASTs may also be useful for other things, like generating test data or linting. These ASTs can also be helpful in understanding how the different parts of a GraphQL operation fit together.

If you’re interested in writing React frontends and using GraphQL to power local experiences, we’re hiring — come join us!

首页 - Wiki
Copyright © 2011-2024 iteam. Current version is 2.137.1. UTC+08:00, 2024-11-22 20:19
浙ICP备14020137号-1 $访客地图$