Improving

GraphQL and Apollo Workflow

Eddy Nguyen

❤️

Overview

  • React, GraphQL and TypeScript
  • Simple example
  • Understanding how it works
  • Improving workflow

React

  • JavaScript UI framework
  • Awesome

GraphQL

  • Schema based query language for APIs
  • Client asks server for data it needs 
  • Client libraries: Apollo Client, Relay, etc.
  • Server libraries: gqlgen, Apollo Server, etc.

TypeScript

  • Superset of JavaScript
  • Type checked at compile time
  • Generics 
function foo<T>(...): T {...}

const result = foo<string>(...); // result is 'string'
const result = foo<number>(...); // result is 'number'
// UserDisplay.tsx

import gql from 'graphql-tag';
import React from 'react';
import { useQuery } from '@apollo/react-hooks';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      firstName
      lastName
    }
  }
`;

function UserDisplay(){
  const { loading, error, data } = useQuery(GET_USER, { 
    variables: { id: '1' } 
  });
  
  ...
}

Example

console.log(GET_USER);

{
  "kind":"Document",
  "definitions":[{
    "kind":"OperationDefinition",
    "operation":"query",
    "name":{ "kind":"Name", "value":"GetUser"},
    "variableDefinitions":[{ 
      "kind":"VariableDefinition",
      "variable":{ 
        "kind":"Variable", "name":{ "kind":"Name", "value":"id" } 
      },
      "type":{ "kind":"NonNullType", "type":{ 
        "kind":"NamedType", "name":{ "kind":"Name", "value":"ID" } 
      }},
      "directives":[]
    }],
    "directives":[],
    "selectionSet":{ "kind":"SelectionSet",
      "selections":[{ 
        "kind":"Field", "name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"directives":[],
        "selectionSet":{
          "kind":"SelectionSet",
          "selections": [
            {"kind":"Field","name":{"kind":"Name","value":"id"},"arguments":[],"directives":[]},
            {"kind":"Field","name":{"kind":"Name","value":"firstName"},"arguments":[],"directives":[]},
            {"kind":"Field","name":{"kind":"Name","value":"lastName"},"arguments":[],"directives":[]}
          ]
        }
      }]
    }
  }]
};

Abstract Syntax Tree (AST)

  • Describes GraphQL operations and fields
  • Document Node is the object representation

Generating document nodes

graphql-tag

+ Easy to use

- gql sent to client
- Doc parsed at runtime

Webpack loader

+ Compiled on server

- Need webpack

Generating document nodes

graphql-tag

+ Easy to use

- gql sent to client
- Doc parsed at runtime

Pure document node

+ No external libraries

- Impossible to write 

Generating document nodes

Webpack loader

+ Compiled on server

- Need webpack

graphql-tag

+ Easy to use

- gql sent to client
- Doc parsed at runtime

Simple ways to improve

  • Webpack loader
  • Typescript
  • Abstract logic

Webpack

loader

// webpack.config.js

module: {
  rules: [
    {
      test: /\.(graphql|gql)$/,
      exclude: /node_modules/,
      loader: 'graphql-tag/loader',
    },
  ],
},

Webpack loader

# UserDisplay.graphql

query GetUser($id: ID!) {
  user(id: $id) {
    id
    firstName
    lastName
  }
}

Webpack loader

// UserDisplay.tsx

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import { GetUser } from './UserDisplay.graphql';

function UserDisplay(){
  const { loading, error, data } = useQuery(GetUser, { 
    variables: { id: '1' } 
  });
  
  ...
}

Webpack loader

Types

Types

// operations.d.ts

declare module '*/UserDisplay.graphql' {
  import { DocumentNode } from 'graphql';
  
  const defaultDocument: DocumentNode;
  
  export const GetUser: DocumentNode;

  export default defaultDocument;
}

Types

Types

Types

Types

Types

$ apollo client:codegen --target=typescript

Types

// UserDisplay.tsx

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import Query from './UserDisplay.graphql';
import { GetUser, GetUserVariables } from './__generated__';

function UserDisplay(){
  const { loading, error, data } = useQuery<GetUser, GetUserVariables>(Query.GetUser, { 
    variables: { id: '1' } 
  });
  
  ...
}

Types

Abstracting logic

// UserDisplay.boilerplate.tsx

import React from 'react';
import Query from './UserDisplay.graphql';
import { GetUser, GetUserVariables } from './__generated__';
import { useQuery, QueryHookOptions } from '@apollo/react-hooks';

export function useGetUserQuery(options?: QueryHookOptions<GetUser, GetUserVariables>){
  return useQuery<GetUser, GetUserVariables>(Query.GetUser, options);
}

export type GetUserQueryHookResult = ReturnType<typeof useGetUserQuery>;

Abstracting logic

// UserDisplay.tsx

import React from 'react';
import { useGetUserQuery } from './UserDisplay.boilerplate';

function UserDisplay(){
  const { loading, error, data } = useGetUserQuery({variables: { id: '1' } });

  ...
}

Abstracting logic

  • Fully typed

Pros

  • Compiled document node 
  • Readability and ease of use
  • So much boilerplate!
  • Boilerplate!
  • Sooo much boilerplate!!!

Cons

Old workflow

  • Write GraphQL document
  • Generate types
  • Create boilerplate file
  • Create wrapped hooks
  • Create alias types
  • Use wrapped hook and alias types

What if this...

  • Write GraphQL document
  • Generate types
  • Create boilerplate file
  • Create wrapped hooks
  • Create alias types
  • Use wrapped hook and alias types

... becomes this?

  • Write GraphQL document
  • Generate all the things!
  • Use wrapped hook and alias types
  • Parse GraphQL documents
  • Validate against GraphQL schema
  • Generate based on plugin config

GraphQL code generator

Config file

overwrite: true
schema: schema.graphql # Path to schema file OR GraphQL endpoint
generates:
  src/__generated__/types.ts:
    plugins:
      - typescript

  src/__generated__/operations.d.ts:
    documents: src/pages/**/*.graphql
    plugins:
      - typescript-graphql-files-modules

  src/pages/:
    documents: src/pages/**/*.graphql
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/types.ts
      extension: .generated.tsx
    plugins:
      - typescript-operations
      - typescript-react-apollo
    config:
      documentMode: external
      importDocumentNodeExternallyFrom: near-operation-file
      withHooks: true
      withComponent: false
      withHOC: false
      reactApolloVersion: 3

How it works

# UserDisplay.graphql

query GetUser($id: ID!) {
  user(id: $id) {
    id
    firstName
    lastName
  }
}

How it works

$ graphql-codegen 
// UserDisplay.tsx

import React from 'react';
import { useGetUserQuery } from './UserDisplay.generated';

function UserDisplay(){
  const { loading, error, data } = useGetUserQuery({variables: { id: '1' } });

  ...
}

How it works

How it works

overwrite: true
schema: schema.graphql # Path to schema file OR GraphQL endpoint
generates:
  src/__generated__/types.ts:
    plugins:
      - typescript

  src/__generated__/operations.d.ts:
    documents: src/pages/**/*.graphql
    plugins:
      - typescript-graphql-files-modules

  src/pages/:
    documents: src/pages/**/*.graphql
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/types.ts
      extension: .generated.tsx
    plugins:
      - typescript-operations
      - typescript-react-apollo
    config:
      documentMode: external
      importDocumentNodeExternallyFrom: near-operation-file
      withHooks: true
      withComponent: false
      withHOC: false
      reactApolloVersion: 3
// types.ts

export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
};

export type Query = {
   __typename?: 'Query',
  user: User,
};


export type QueryUserArgs = {
  id: Scalars['ID']
};

export type User = {
   __typename?: 'User',
  id: Scalars['ID'],
  firstName: Scalars['String'],
  lastName: Scalars['String'],
};

How it works

How it works

overwrite: true
schema: schema.graphql # Path to schema file OR GraphQL endpoint
generates:
  src/__generated__/types.ts:
    plugins:
      - typescript

  src/__generated__/operations.d.ts:
    documents: src/pages/**/*.graphql
    plugins:
      - typescript-graphql-files-modules

  src/pages/:
    documents: src/pages/**/*.graphql
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/types.ts
      extension: .generated.tsx
    plugins:
      - typescript-operations
      - typescript-react-apollo
    config:
      documentMode: external
      importDocumentNodeExternallyFrom: near-operation-file
      withHooks: true
      withComponent: false
      withHOC: false
      reactApolloVersion: 3
// operations.d.ts

declare module '*/UserDisplay.graphql' {
  import { DocumentNode } from 'graphql';
  const defaultDocument: DocumentNode;
  export const GetUser: DocumentNode;

  export default defaultDocument;
}

How it works

How it works

overwrite: true
schema: schema.graphql # Path to schema file OR GraphQL endpoint
generates:
  src/__generated__/types.ts:
    plugins:
      - typescript

  src/__generated__/operations.d.ts:
    documents: src/pages/**/*.graphql
    plugins:
      - typescript-graphql-files-modules

  src/pages/:
    documents: src/pages/**/*.graphql
    preset: near-operation-file
    presetConfig:
      baseTypesPath: __generated__/types.ts
      extension: .generated.tsx
    plugins:
      - typescript-operations
      - typescript-react-apollo
    config:
      documentMode: external # 'graphQLTag', 'external' or 'documentNode'
      importDocumentNodeExternallyFrom: near-operation-file # only works with 'external'
      withHooks: true
      withComponent: false
      withHOC: false
      reactApolloVersion: 3
// UserDisplay.generated.tsx

import * as Types from './types'; // This path is relative path to the types.ts file

import * as Operations from './UserDisplay.graphql';
import * as ApolloReactCommon from '@apollo/react-common';
import * as ApolloReactHooks from '@apollo/react-hooks';

export type GetUserQueryVariables = {
  id: Types.Scalars['ID']
};

export type GetUserQuery = (
  { __typename?: 'Query' }
  & { user: (
    { __typename?: 'User' }
    & Pick<Types.User, 'id' | 'firstName' | 'lastName'>
  ) }
);
export function useGetUserQuery(
  baseOptions?: ApolloReactHooks.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
){
  return ApolloReactHooks.useQuery<GetUserQuery, GetUserQueryVariables>
    (Operations.GetUser, baseOptions);
}
export function useGetUserLazyQuery(
  baseOptions?: ApolloReactHooks.LazyQueryHookOptions<GetUserQuery, GetUserQueryVariables>
){
  return ApolloReactHooks.useLazyQuery<GetUserQuery, GetUserQueryVariables>
    (Operations.GetUser, baseOptions);
}
export type GetUserQueryHookResult = ReturnType<typeof useGetUserQuery>;
export type GetUserQueryResult = 
  ApolloReactCommon.QueryResult<GetUserQuery, GetUserQueryVariables>;
  • Resolvers for Apollo Server
  • Apollo Client for Angular, Stencil
  • Urql
  • And many more...
  • Or write your own!

Other plugins

We are hiring!

Software Engineer

Thank you!

Website:

Github:

LinkedIn:
https://eddy.works

https://github.com/eddeee888

https://www.linkedin.com/in/eddeee

Improving GraphQL and Apollo Workflow

By Eddy Nguyen

Improving GraphQL and Apollo Workflow

  • 934