Skip to content

Instantly share code, notes, and snippets.

@rricard
Last active August 11, 2020 12:17
Show Gist options
  • Save rricard/6cf2394ce4ecb7cb610451ccf4be7198 to your computer and use it in GitHub Desktop.
Save rricard/6cf2394ce4ecb7cb610451ccf4be7198 to your computer and use it in GitHub Desktop.
The GraphQL Developer Experience

The GraphQL Developer Experience

Develop better and faster with the GraphQL type system

by Robin Ricard, community contributor to the Apollo project (Github, Twitter, Website)

A few months ago, Sashko Stubailo wrote an excellent article about the benefits of static GraphQL queries. After seeing that article, I immediately wanted to get that great development experience he was talking about in my own projects. Unfortunately, all of this tooling was at the time only available for Swift and XCode, until now...

Today, I'm happy to announce that a complete GraphQL developer experience for Typescript is available! (with Flow coming soon...)

TypeScript DX with typesystem checks against query and schema as well as query checking

Automatic named fragments loading in static query projects

In my latest job, I worked on a large GraphQL project where we had a lot of fragments. This rapidly became a pain to work with. We had many issues to import correctly those fragments in files scattered around the project. This made most of our fragment resolution not static at all. graphql-document-collector has been created to solve this problem: instead of storing queries and fragments in js files, we can now store them in static graphql files around the project. The collector will gather all documents (fragments, queries, mutations, ...) found in the project and put them in a single file containing all of the documents of the project with their fragments auto-resolved!

This is usually done using a single command:

graphql-document-collector '**/*.graphql' > documents.json

This JSON file contains all of the documents you may want to use, let's see how I would use that in my project:

// ...
import {graphql} from 'react-apollo';
const graphqlDocuments = require('./documents.json');

// ...

const FeedWithData = graphql(graphqlDocuments['Feed.graphql'])(Feed);

// ...

That's pretty straightforward, you don't even need to use the gql tag. In this case, Feed.graphql can use fragments located in a file named fragments/Item.graphql, the collector will handle that for us. The only limitation is: you can't have two fragments with same name in your project.

Automatic type annotation generation for TypeScript

An another issue we had was to guarantee the data in our UI was correct, and this, even when we changed our queries. We added, alongside our operations and fragments, type annotations corresponding to the graphql document. Unfortunately, with time, the annotations and the queries stopped matching each other. Either we forgot to report a field removal or we just misinterpreted the type from the schema, making, for instance, something that should be nullable, non-nullable.

Since GraphQL is a completely typed language and our queries are now completely static, we can now generate those annotations automatically! In order to do that in a TypeScript project, I added to apollo-codegen a generator that converts GraphQL documents and schema to typescript interfaces.

Let's take this simple query with a fragment:

HeroName.graphql

query HeroName($episode: Episode) {
  hero(episode: $episode) {
    ...DescribeHero
  }
}

DescribeHero.graphql

fragment DescribeHero on Character {
  name
  appearsIn
}

With the following schema:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]
}

type Query {
  hero(episode: Episode): Character!
}

With those, using apollo-codegen for TypeScript, we can generate this annotations file:

//  This file was automatically generated and should not be edited.

// The episodes in the Star Wars trilogy
export type Episode =
  "NEWHOPE" | // Star Wars Episode IV: A New Hope, released in 1977.
  "EMPIRE" | // Star Wars Episode V: The Empire Strikes Back, released in 1980.
  "JEDI"; // Star Wars Episode VI: Return of the Jedi, released in 1983.


export interface HeroNameQueryVariables {
  episode: Episode | null;
}

export interface HeroNameQuery {
  hero: DescribeHeroFragment;
}

export interface DescribeHeroFragment {
  name: string;
  appearsIn: Array< Episode | null >;
}

All you have to do is pull your schema with an introspection query from your server:

apollo-codegen download http://localhost:8080/graphql --output schema.json

Once you have the schema, generate the annotations:

apollo-codegen generate **/*.graphql --schema schema.json --target ts --output schema.ts

You can now import and use those annotations in your codebase to ensure your GraphQL results are used safely!

Create correct GraphQL documents from your editor

Since we just downloaded a schema.json, it's good to know we can use it for typechecking our GraphQL documents as well. Just use the eslint-plugin-graphql against your .graphql documents!

Wrapping up!

As you can see, your development experience can be improved using static GraphQL documents. However, for now, all of this requires some important setup work that can be daunting at first. We'll work on streamlining this process soon but in the meantime, you can always try our sample repository with all of the tooling you need to try out those features: typed-graphql-client-example. You should try to open it with VSCode in which you'll get the best out of the typesystem.

Have fun developing awesome GraphQL apps!

fragment DescribeHero on Character {
name
appearsIn
}
```graphql
query HeroName($episode: Episode) {
hero(episode: $episode) {
...DescribeHero
}
}
// This file was automatically generated and should not be edited.
// The episodes in the Star Wars trilogy
export type Episode =
"NEWHOPE" | // Star Wars Episode IV: A New Hope, released in 1977.
"EMPIRE" | // Star Wars Episode V: The Empire Strikes Back, released in 1980.
"JEDI"; // Star Wars Episode VI: Return of the Jedi, released in 1983.
export interface HeroNameQueryVariables {
episode: Episode | null;
}
export interface HeroNameQuery {
hero: DescribeHeroFragment;
}
export interface DescribeHeroFragment {
name: string;
appearsIn: Array< Episode | null >;
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]
}
type Query {
hero(episode: Episode): Character!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment