This article was published on Wednesday, May 20, 2020 by Dotan Simha @ The Guild Blog
In this article we'll try to explain and demonstrate common patterns for frontend development with
GraphQL and GraphQL Code Generator.
Most patterns are general and can be applied to most popular frontend frameworks (React, Angular,
Vue, Stencil), and to popular GraphQL client libraries (Apollo / urql), due to extended support of
GraphQL Code Generator, and it's flexibility.
In this article, we'll cover the development workflow of frontend applications with TypeScript and
GraphQL Code Generator, suggest best-practices for GraphQL development for frontend developers,
and try to explain the idea behind it.
Why Do I Need GraphQL Code Generator in My Project?
Let's start by understanding the need for GraphQL Code Generator in your project.
If you are using TypeScript for frontend development, you probably aim to get the most out of the
TypeScript type-system, that means, that your preference should be to have typed variables all
across your application.
It starts with the code you write - UI components, services and business logic. You can also have
type safety for your third-party libraries (some built-in, and some with @types/...
packages).
The idea behind type-safety is to make sure that your code can be statically analyzed and built,
before running it. It's useful because this way your can detect potential bugs before they happen in
runtime.
But What about the Data Your Fetch from External Services?
So if you are already using GraphQL, you probably know that your GraphQL API is typed, and built as
a GraphQL schema.
And it doesn't matter which language or platform is used to write your GraphQL API or schema - you
fetch it the same way into your frontend application - with GraphQL operations (query
/ mutation
/ subscriptions
, and fragment
) and probably over HTTP.
So if your GraphQL schema is typed already, and your GraphQL operations allow you to choose specific
fields from it (called Selection Set), why not leverage the schema and selection set and turn it
into TypeScript types?
Basic Data Fetching with GraphQL
Let's assume that we have the following simple GraphQL schema:
scalar Date
type Query {
upcomingEvents: [Event!]!
}
type Event {
id: ID!
title: String!
date: Date!
location: Location!
}
type Location {
name: String!
address: String!
}
And the client-side application consumes it with the following query
:
query listEvents {
upcomingEvents {
id
title
date
}
}
If you client-side application only needs id
, title
and date
from the Event
type - you can
expect to have those fields in your GraphQL response.
You can also use it in your component code:
export const ListEvents = listEvents => {
return (
<ul className="list-events">
{listEvents.map(event => (
<li key={event.id}>
{event.title} ({event.date})
</li>
))}
</ul>
)
}
In the example above we have a few issues that might be bugs in the future:
- We don't know the type of
listEvents
- and we can't really know it without creating a type for it manually (but this could also break, because the API could change). - We can't be sure what are the actual types of
id
,title
anddate
fields - it'sany
. - We can't count of the fields to be there because they GraphQL query can change, and it's not connected to our code at all.
- If you'll try to access
location
of the event - you'll just getundefined
because it's not part of the selection set.
With GraphQL Code Generator, you can have a full type safety, based on your GraphQL schema and
your GraphQL operations, and that means:
- You can tell what is the exact structure of
listEvents
, what could benull
and enjoy auto-complete in your IDE. - You can tell what is the data type of all fields.
- If your selection set changes, it being reflected automatically and you can detect issues while developing or building (instead while running).
- Trying to access fields that are not defined in your selection set will show an error in build time and in your IDE.
So those are the basic types that codegen can generate for your, and you can get those by using the
@graphql-codegen/typescript
and @graphql-codegen/typescript-operations
plugins of GraphQL Code
Generator.
But that's not all - you can generate much more - you can get React Hooks, Angular Services and
more.
How Do I Start?
You can start by
trying GraphQL Code Generator plugin in the live-demo here and
with the
Getting started with GraphQL Code Generator.
Tips & Best Practices When Using GraphQL Code Generator and TypeScript
Now that you understand why and how GraphQL Code Generator can help you, it's time to learn new
concepts that might simplify the way your consume GraphQL API, and improve your code quality.
Watch Mode
GraphQL Code Generator also comes with a built-in watch mode. You can use it from the CLI:
graphql-codegen --watch
Or, set it in your codegen.yml
file:
watch: true
schema: ...
This way, each time you have changes for your GraphQL schema or GraphQL operations, GraphQL Code
Generator will be executed again and update the generated files.
Generate More than Just Types
GraphQL Code Generator can generate more than just TypeScript types. It can automate some of your
GraphQL development workflow, generate common practices for data fetching, and add type-safety to
code you usually need to write manually.
Beside TypeScript types, here's a list and examples of part of GraphQL Codegen capabilities:
Dump Remote Schema to a Local File
If your GraphQL schema is only available for you using an HTTP endpoint, you can always get a copy
of it locally. This is useful for better IDE experience.
You can do it with the @graphql-codegen/schema-ast
plugin, and the following configuration:
schema: http://YOUR_SERVER/graphql
generates:
./src/schema.graphql:
plugins:
- schema-ast
Save Local GraphQL Introspection
GraphQL schema can be represented in many ways. One of the is
introspection.
You can save a local copy of your schema introspection using @graphql-codegen/introspection
and
the following:
schema: YOUR_SCHEMA_PATH
generates:
./src/schema.json:
plugins:
- introspection
Add Custom Content to Output Files
I wish you to add custom content to the codegen output files, you can use @graphql-codegen/add
plugin, and add your content this way:
schema: YOUR_SCHEMA_PATH
generates:
./src/types.ts:
plugins:
- add: '// THIS FILE IS GENERATED, DO NOT EDIT!'
- typescript
React & Apollo: Generate Hooks
You can generate ready-to-use React hooks for your GraphQL operations, with the following
configuration:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.tsx:
config:
withHooks: true
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
And then use it in your code:
import React from 'react'
import { useMyQuery } from './generated-types'
export const MyComponent: React.FC = () => {
const { data, loading, error } = useMyQuery()
// `data` is now fully typed based on your GraphQL query
return <> ... </>
}
React & Apollo: Generate HOC (High-Order-Component)
You can generate ready-to-use React HOC for your GraphQL operations, with the following
configuration:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.tsx:
config:
withHOC: true
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
And then use it in your code:
import React from 'react';
import { withMyQuery } from './generated-types';
const MyViewComponent: React.FC = ({ data, loading, error }) => {
// `data` is now fully typed based on your GraphQL query
return (<> ... </>);
};
export const MyComponent = withMyQuery({
variables: { ... }
})(MyViewComponent);
React & Apollo: Generate Components
You can generate ready-to-use React data components for your GraphQL operations, with the following
configuration:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.tsx:
config:
withComponent: true
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
And then use it in your code:
import React from 'react';
import { MyQueryComponent } from './generated-types';
export const MyComponent: React.FC = ({ data, loading, error }) => {
return (
<MyQueryComponent variables={...}>
{
({ data, loading, error }) => {
// `data` is now fully typed based on your GraphQL query
return (<> ... </>)
}
}
</MyQueryComponent>
);
};
Angular & Apollo: Generate Services
You can generate ready-to-use Angular Service
s for your GraphQL operations, with the following
configuration:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.ts:
config:
withHooks: true
plugins:
- typescript
- typescript-operations
- typescript-apollo-angular
And then use it in your code:
import { MyFeedGQL, MyFeedQuery } from './generated-types'
@Component({
selector: 'feed',
template: `
<h1>Feed:</h1>
<ul>
<li *ngFor="let item of feed | async">{{ item.id }}</li>
</ul>
`
})
export class FeedComponent {
feed: Observable<MyFeedQuery['feed']>
constructor(feedGQL: MyFeedGQL) {
this.feed = feedGQL.watch().valueChanges.pipe(map(result => result.data.feed))
}
}
React & urql: Generate Hooks
If you are using urql
as your GraphQL client, you can
generate ready-to-use React hooks for your GraphQL operations, with the following configuration:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.tsx:
config:
withHooks: true
plugins:
- typescript
- typescript-operations
- typescript-urql
And then use it in your code:
import React from 'react'
import { useMyQuery } from './generated-types'
export const MyComponent: React.FC = () => {
const { data, loading, error } = useMyQuery()
// `data` is now fully typed based on your GraphQL query
return <> ... </>
}
This plugin can also generate HOC or data Component, based on your preference ;)
Vue.js & Apollo: Generate Composition Functions
If you are using Vue.js
with @vue/apollo-composable
your GraphQL client,
you can generate composition functions based on your GraphQL operations:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.ts:
config:
withHooks: true
plugins:
- typescript
- typescript-operations
- typescript-vue-apollo
And then use it in your code:
<template>
<div>
{{ result.feed.id }}
</div>
</template>
<script lang="ts">
import { createComponent } from '@vue/composition-api'
import { useTestQuery } from '../generated-types'
export default createComponent({
setup() {
const { result } = useMessagesQuery()
return { result }
}
})
</script>
Apollo: Type-Safe refetchQueries
If you are using Apollo Client, and you wish to refetch a query when a mutation is done, you can do
add
@graphql-codegen/named-operations-object
plugin to your setup.
It will generate a const
object that contains a list of your GraphQL operation names, as found by
the codegen. This is useful because if you'll change the name of your operation, you'll know about
it in build time, and you'll be able to update it:
This is how to configure it:
schema: SCHEMA_PATH_HERE
documents: './src/**/*.graphql'
generates:
src/generated-types.ts:
plugins:
- typescript
- typescript-operations
- named-operations-object
And then use it in your code:
import { client } from './apollo'; // this is your Apollo Client instance, for example
import { addTodoMutation, namedOperations } from './generated-types';
client.mutate({
query: addTodoMutation,
variables: { ... },
refetchQueries: [
// If you'll change or remove that operation, this will fail during build time!
namedOperations.Query.listTodo,
]
})
You can use it with any other wrapper of Apollo-Client, such as apollo-angular
or
react-apollo
.
Apollo: Auto-Generated fragmentMatcher
/ possibleTypes
If you are using Apollo-Client and your schema contains GraphQL union
or interface
, you'll need
to provide fragmentMatcher
to your Apollo store instance.
This is needed in order to improve performance of Apollo store.
You can read more about this here.
You can generate it using the following configuration:
schema: YOUR_SCHEMA_PATH
generates:
./src/fragment-matcher.ts:
plugins:
- fragment-matcher
And then pass it directly to your Apollo instance:
import { InMemoryCache } from '@apollo/client'
// generated by Fragment Matcher plugin
import introspectionResult from '../fragment-matcher'
const cache = new InMemoryCache({
possibleTypes: introspectionResult.possibleTypes
})
Name Your Operations
It's highly important to name your GraphQL operations, because otherwise it will be difficult for
your GraphQL client to cache and manage it. It will also make it difficult for the codegen to create
easy-to-use types, and it will fallback to Unnamed_Operation_
.
✅ Do:
query myOperationNameHere {
...
}
❌ Don't:
query {
...
}
Tip: Duplicate Names
Ensure you have unique names for your operations.
Libraries like Apollo Client will have issues and unexpected behavior if you re-use the same
operation name, and GraphQL Code Generator will throw an error in case of name duplications.
Write Your Operations and Fragments in .graphql
Files
You can manage your GraphQL operations in .graphql
files, without worrying about loading it into
your application with Webpack loaders or anything else. Also, Most IDEs has better support for
autocomplete inside .graphql
files.
GraphQL Code Generator plugins for frontend frameworks integrations (such as
typescript-react-apollo
/ typescript-apollo-angular
) are automatically creates an executable
copy (DocumentNode
) of your GraphQL operations in the generated code file, and it will
automatically include it withing your wrapper call.
It will add that to the output file with Document
suffix, and FragmentDoc
for fragments.
So you can maintain your operations in .graphql
files, but import it from generated code file:
// MyQueryDocument and MyUserFragmentDoc are parsed `DocumentNode`
import { MyQueryDocument, MyUserFragmentDoc } from './generated-types'
Tip: No need to handle imports
If you have a query that uses a fragment, you can just use the fragment spread as-is, without the
need to import it or maintain it in the same file.
For example:
```graphql filename="user.query.graphql"
query user {
userById {
...UserFields # We don't need to import this, just use the name
}
}
```graphql filename="userfields.fragment.graphql"
fragment UserFields on User {
id
name
}
And if you'll import UserQueryDocument
from your generated file, it will have the fragment
concatenated automatically.
Fragment per Component
If you wish to have a simple way to manage your application complexity with multiple queries and
fragments, consider to have small fragments that defines the needs of your components.
Consider the following structure for example (for a list and item implementation):
src/
├── generated-types.tsx
├── list/
├──── todo-list.tsx
├──── todo-list.query.graphql
├── list-item/
├──── todo-item.tsx
├──── todo-item.fragment.graphql
├── todo-details/
├──── todo-details.tsx
├──── todo-details.fragment.graphql
├── user-profile/
├──── profile-page.tsx
├──── me.query.graphql
├──── authenticated-user.fragment.graphql
Then, your GraphQL query
files can just build it's self based on the nested fragments it needs:
```graphql filename="todo-list.query.graphql"
query todoList {
todos {
...TodoItemFields
...TodoDetailsFields
}
}
```graphql filename="me.query.graphql"
query me {
me {
...AuthenticatedUserFields
}
}
And then, GraphQL Code Generator will generate a matching TypeScript type per each component, based
on the fragment or query that it needs.
So you can use the generated fragment type as the input for your components, and pass it directly
from the parent component easily, with type-safety:
```tsx filename="todo-list.tsx"
import React from 'react'
import { useTodoList } from '../generated-types'
import { TodoItem } from './todo-item'
export const TodoList: React.FC = () => {
const { data, loading, error } = useTodoList()
return (
<>
{data.todos.map(todo => (
))}
</>
)
}
```tsx filename="todo-item.tsx"
import React from 'react'
import { TodoItemFieldsFragment } from '../generated-types'
export const TodoItem: React.FC = (todo: TodoItemFieldsFragment) => {
return <div>{todo.title}</div>
}
Please have some judgment before creating fragments, it should represent data structure that is
specific per component. Don't abuse this mechanism by creating fragments with a single field. Try
to group it in a way that matches your components needs.
Access to Nested Generated Types
If you are already familiar with plugins such as @graphql-codegen/typescript-operations
output
structure, you probably already know that it's built on operations and fragments.
It means that each GraphQL query
and each GraphQL fragment
that you have, will be converted into
a single TypeScript type
.
That means, that accessing nested fields in your generated TypeScript types might looks a bit
complex at the beginning.
Consider the following query:
query userById($userId: ID!) {
user(id: $userId) {
id
profile {
age
name {
first
last
}
}
}
}
The @graphql-codegen/typescript-operations
plugin output for that query
will be:
export type UserByIdQuery = { __typename?: 'Query' } & {
user?: Maybe<
{ __typename?: 'User' } & Pick<User, 'id'> & {
profile?: Maybe<
{ __typename?: 'Profile' } & Pick<Profile, 'age'> & {
name: { __typename?: 'Name' } & Pick<Name, 'first' | 'last'>
}
>
}
>
}
Accessing the actual TypeScript type of user.profile.name.first
might look a bit intimidating, but
there are several things you can do to simplify the access to it:
- Best solution: use fragments - if you'll use fragments for the
User
fields and forProfile
fields, you'll break down the types into smaller pieces (see previous tip). - Use TypeScript type system:
type FirstName = UserByIdQuery['user']['profile']['name']['first']
. - You can also do it with
Pick
:type FirstName = Pick<UserByIdQuery, ['user', 'profile', 'name', 'first']>
.
Tip: Hate Pick
in your generated files?
The @graphql-codegen/typescript-operations
is the TypeScript representation of your GraphQL
selection set. Just like selection set chooses fields from the GraphQL schema,
typescript-operations
picks fields from typescript
plugin (which is the representation of your
GraphQL schema).
If you wish to have simpler TypeScript output, you can set preResolveTypes: true
in your
configuration, and it will prefer to use the primitive TypeScript type when possible.
Top comments (0)