GraphQL

Full Stack

Rachèl Heimbach

Principal Consultant @ OpenValue

Agenda

  • GraphQL
  • Apollo
  • Schema design
  • Server
  • Client
  • Federation

https://graphql.org/learn/thinking-in-graphs/

https://www.apollographql.com/docs/apollo-server/

Schema design

https://www.apollographql.com/

Schema 

  • Type
type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
}

Non-Nullable!

Schema 

  • Type

 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

Schema 

  • Type
  • Query

 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

type Query {
  products: [Product!]!
}
{
  products {
    id
    title
    price
    rating {
      rate
    }
  }
}
{
  "products": [
    {
      "id": "1",
      "title": "T-shirt",
      "price": 10.99,
      "rating": {
        "rate": 4.9
      }
    },
    ...
    {
      "id": "2000",
      ...
    }
  ]
}

Non-Nullable list

Non-Nullable product

Schema 

  • Type
  • Query
  • Parameters

 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

type Query {
  products(offset: Int! limit: Int!): [Product!]!
}
{
  products(offset: 0, limit: 20) {
    id
    title
    price
    rating {
      rate
    }
  }
}
{
  "products": [
    {
      "id": "1",
      "title": "T-shirt",
      "price": 10.99,
      "rating": {
        "rate": 4.9
      }
    },
    ...
    {
      "id": "20",
      ...
    }
  ]
}
    
    

Schema 

  • Type
  • Query
  • Parameters

 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
  reviews: (offset: Int! limit: Int!): [Review!]!
}

type Rating {
  count: Int!
  rate: Float!
}

type Query {
  products(offset: Int! limit: Int!): [Product!]!
}
{
  products(offset: 0, limit: 20) {
    id
    title
    price
    reviews(offset: 0, limit: 1) {
      id
      title
    }
  }
}
{
  "products": [
    {
      "id": "1",
      "title": "T-shirt",
      "price": 10.99,
      "reviews": [
        {
          "id": "123123",
          "title": "This is great!"
        }
      ]
    },
    ...
  ]
}

Schema 

  • Type
  • Query
  • Parameters
  • Input

 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

input PaginationInput {
  offset: Int!
  limit: Int!
}

type Query {
  products(pagination: PaginationInput!): [Product!]!
}
{
  products(pagination: { offset: 0, limit: 20 }) {
    id
    title
    price
  }
}

Schema 

type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

type Query {
  product(id: ID!): Product
}

Nullable

  • Union

Schema 

  • Union
type Product {
  id: ID!
  title: String!
  price: Float!
  image: String!
  rating: Rating!
}

type Rating {
  count: Int!
  rate: Float!
}

type NotFound {
  id: ID!
  reason: String!
}

union ProductResult = Product | NotFound

type Query {
  product(id: ID!): ProductResult!
}
{
  product(id: "1") {
    ... on Product {
      id
      title
    }
    ... on NotFound {
      id
      reason
    }
  }
}

Server

https://www.apollographql.com/

Resolvers

A resolver is a function that's responsible for populating the data for a single field in your schema. 

Resolver chain

{
  products(offset: 0, limit: 20) {
    id
    title
  }
}

Query.products()

Product.title()

{
  products(offset: 0, limit: 20) {
    id
    title
    reviews(offset: 0, limit: 1) {
      id
      title
    }
  }
}

Query.products()

Product.title()

Product.reviews()

Review.title()

Resolver chain

Optional

{
  products(offset: 0, limit: 20) {
    id
    title
    image
    price
    reviews(offset: 0, limit: 1) {
      id
      title
    }
  }
}

Query.products()

Product.price()

Product.reviews()

Resolver chain

REST /products

Review microservice

Price microservice

Query.products()

Product.price()

Product.reviews()

N+1 problem

1 req -> 20 products

20 req -> 20 prices

20 req -> 20 reviews

41 requests!

?

Dataloader

Batching

async function batchFunction(keys) {
  const results = await db.fetchAllKeys(keys)
  return keys.map(key => results[key] || new Error(`No result for ${key}`))
}

const loader = new DataLoader(batchFunction)

Query.products()

Product.price()

Product.reviews()

Dataloader

1 req -> 20 products

1 req -> 20 prices

1 req -> 20 reviews

3 requests!

Review.user()

1 req -> 1-20 users

Request context

Query.products()

Product.price()

Product.reviews()

Dataloader

1 req -> 20 products

1 req -> 20 prices

1 req -> 20 reviews

Review.user()

1 req -> 1-20 users

Request context

Query.products()

0 req -> 20 products

Product.price()

0 req -> 20 prices

3 requests!

Let's create a server

Server setup

import { ApolloServer, gql } from "apollo-server";

const typeDefs = gql``;

const resolvers = {};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded",
});

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});
import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products: [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: () => [
      {
        id: '1',
        title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
        ...
      },
      ...
    ]
  }
};

...

Simple query

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";
import { ProductsService } from "./services/products.service";

const typeDefs = gql`
  ...
  type Query {
    products: [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, _args, context, _info) => 
      context.services.products.get()
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  ...
  context: () => ({
    services: {
      products: new ProductsService()
    }
  })
});

Query resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    ...
  },
  Product: {
    price: () => 100
  }
};

...
{
  "products": [
    {
      "id": "1",
      "price": 100,
    },
    {
      "id": "2",
      "price": 100,
    },
    {
      "id": "3",
      "price": 100,
    },
    ...
    {
      "id": "20",
      "price": 100,
    }
  ]
}
    
    

Type resolver

5 requests!

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, args, context, _info) => 
      context.services.products.get(args.pagination)
  },
  Product: {
    price: (product, _args, context, _info) => 
      context.dataloaders.price.load(product.id)
  }
};

...

Type resolver

2 requests!

Graph

Modules

type Product {
  id: ID!
  title: String!
  category: String!
  description: String!
  image: String!
  inCart: Boolean!
  price: Float!
  rating: Rating!
}
---- Product subgraph ----
type Product {
  id: ID!
  title: String!
  category: String!
  description: String!
  image: String!
}

---- Cart subgraph ----
extend type Product {
  inCart: Boolean!
}

---- Price subgraph ----
extend type Product {
  price: Float!
}

---- Review subgraph ----
type Rating {
  count: Int!
  rate: Float!
}

extend type Product {
  rating: Rating!
}

Client

Apollo Client

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI.

Apollo Client

import { InMemoryCache, ApolloClient, HttpLink } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://...' }),
  cache: new InMemoryCache(options)
});

Apollo Client

import { InMemoryCache, ApolloClient, HttpLink, gql } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://...' }),
  cache: new InMemoryCache(options)
});

client.query({
  query: gql`
    query ProductsList($page: Int!, $size: Int!) {
      products(pagination: { page: $page, size: $size }) {
        results {
          id
          title
          # ...
        }
      }
    }
  `,
  variables: {
    page: 1,
    size: 20,
  },
});

Apollo Client

import { InMemoryCache, ApolloClient, HttpLink, gql } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://...' }),
  cache: new InMemoryCache(options)
});

client.query({
  query: gql`
    query ProductsList($page: Int!, $size: Int!) {
      products(pagination: { page: $page, size: $size }) {
        results {
          id
          title
          # ...
        }
      }
    }
  `,
  variables: {
    page: 1,
    size: 20,
  },
});

client.mutate({
  mutation: gql`
    mutation AddToCart($id: ID!, $quantity: Int!) {
      addToCart(id: $id, quantity: $quantity)
    }
  `,
  variables: {
    id: '1',
    quantity: 4,
  },
});

https://the-guild.dev/graphql/codegen

Angular

query Products($pagination: Params!) {
  products(pagination: $pagination) {
    results {
      id
      title
      price
      description
      category
      image
      rating {
        count
        rate
      }
      inCart
    }
  }
}
@Component({
  selector: 'ov-smart-products',
  styleUrls: ['./smart-products.component.scss'],
  template: `













  `,
})
export class SmartProductsComponent implements OnInit {
  products$ = this.productsGQL
    .watch(PAGINATATION)
    .valueChanges.pipe(
      map(({ data, loading, error }) => ({
        loading,
        error,
        data: data.products?.results,
      }))
    );
  
  constructor(private productsGQL: ProductsGQL) {}
}
<ng-container *ngIf="products$ | async as products">
  <p *ngIf="products.loading; else notLoading">
    Loading...
  </p>
  <ng-template #notLoading>
    <ov-products 
      *ngIf="products.data" 
      [products]="products.data"></ov-products>
    <ov-message 
      *ngIf="products.error" 
      [error]="products.error"></ov-message>
  </ng-template>
</ng-container>

React

query Products($pagination: Params!) {
  products(pagination: $pagination) {
    results {
      id
      title
      price
      description
      category
      image
      rating {
        count
        rate
      }
      inCart
    }
  }
}
export const SmartProductsComponent = () => {
  const { loading, data, error } = useProductsQuery({
    variables: {
      pagination: {
        page: 1,
        size: 6,
      },
    },
  });

  const products = productsQuery?.products.results;
  
  if(loading) {
    return <p>Loading...</p>;
  }
  
  if(error) {
    return <Message error={error}></Message>;
  }

  if(products) {
    return <Products products={products}></Products>;
  }
  
  return <p>No data?<p>;
}

Error

Performance

Cache

React

query Products($pagination: Params!) {
  products(pagination: $pagination) {
    results {
      id
      title
      price
      description
      category
      image
      rating {
        count
        rate
      }
      inCart
    }
  }
}
export const SmartProductsComponent = () => {
  const { loading, data, error } = useProductsQuery({
    fetchPolicy: 'cache-first',
    variables: {
      pagination: {
        page: 1,
        size: 6,
      },
    },
  });

  const products = productsQuery?.products.results;
  
  if(loading) {
    return <p>Loading...</p>;
  }
  
  if(error) {
    return <Message error={error}></Message>;
  }

  if(products) {
    return <Products products={products}></Products>;
  }
  
  return <p>No data?<p>;
}

fetch policy

cache-first (default)

Data

Data

fetch policy

cache-only

Data

Error

fetch policy

cache-and-network

Data

Data

fetch policy

network-only

Data

fetch policy

no-cache

Data

Stale data

Stale data

refetchQueries

Ok

Mutate

Data

1 + n requests

Stale data

update

Mutate

Update

1 request

Ok

Mutate

Update

Ok

Stale data

update + optimistic response

https://principledgraphql.com/

Apollo Gateway vs Router

Questions?

https://github.com/Rachnerd/webshop

Branch: graphql-assignment-1

Demo

Setup + basic query

UI

UI

UI

UI

Smart

Smart

Page

Template

Apollo

Apollo Server

You can use Apollo Server as:

  • A gateway for a federated supergraph.
  • The GraphQL server for a subgraph in a federated supergraph.
  • An add-on to your application's existing Node.js middleware (such as Express or Fastify).
  • A stand-alone GraphQL server, including in a serverless environment.

VSCode Ext.

codegen.yml

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
...

export type Resolvers<ContextType = Context> = {
  Product?: ProductResolvers<ContextType>;
  Query?: QueryResolvers<ContextType>;
};

export type ProductResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Product'] = ResolversParentTypes['Product']> = {
  id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
  price?: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
  title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

export type QueryResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
  products?: Resolver<Array<ResolversTypes['Product']>, ParentType, ContextType>;
};
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Product = {
  __typename?: 'Product';
  id: Scalars['ID'];
  price: Scalars['Float'];
  title: Scalars['String'];
};

Typed resolvers

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products: [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: () => [
      {
        id: '1',
        title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"
      },
      ...
    ]
  }
};

...

Demo

Code generation

Connect a datasource

DataService

import fetch from "node-fetch";
import { Products } from "../generated/graphql";

export class ProductsService {
  async get(): Promise<Product[]> {
    const res = await fetch(
      `http://localhost:8080/products`
    );
    
    const { results } =
      await res.json();

    return results;
  }
}

Query Resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";
import { ProductsService } from "./services/products.service";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  Query: {
    products: () => new ProductsService().get()
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded"
});

Context

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";
import { ProductsService } from "./services/products.service";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, _args, context, _info) => ...
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded",
  context: () => ({}),
});

Context

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: ../context#Context
export interface Context {}

Context

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: ../context#Context
import { ProductsService } from "./services/products.service";

export interface Context {
  services: {
    products: ProductsService;
  };
}

Context

Query Resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";
import { ProductsService } from "./services/products.service";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, _args, context, _info) => 
      context.services.products.get()
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded",
  context: () => ({
    services: {
      products: new ProductsService()
    }
  })
});

Query Resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";
import { ProductsService } from "./services/products.service";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, _args, { services: { products }}, _info) => 
      products.get()
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded",
  context: () => ({
    services: {
      products: new ProductsService()
    }
  })
});

Resolver arguments

...

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  input PaginationParams {
    page: Int!
    size: Int!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, args, { services: { products }}, _info) => 
      products.get(args.pagination)
  }
};

...

Resolver arguments

...

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  input PaginationParams {
    page: Int!
    size: Int!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    products: (_obj, { pagination }, { services: { products }}, _info) => 
      products.get(pagination)
  }
};

...

Demo

Contex, pagination

Type Resolver

Type Resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    ...
  },
  Product: {
    price: () => 100
  }
};

...
{
  "products": [
    {
      "id": "1",
      "price": 100,
    },
    {
      "id": "2",
      "price": 100,
    },
    {
      "id": "3",
      "price": 100,
    },
    ...
    {
      "id": "20",
      "price": 100,
    }
  ]
}
    
    

Context

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: ../context#Context
import { ProductsService } from "./services/products.service";

export interface Context {
  services: {
    products: ProductsService;
  };
}

Context

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: ../context#Context
import { ProductsService } from "./services/products.service";
import { PriceService } from "./services/price.service";

export interface Context {
  services: {
    products: ProductsService;
    prices: PricesService;
  };
}

Type Resolver

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  type Product {
    id: ID!
    title: String!
    price: Float!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  Query: {
    ...
  },
  Product: {
    price: (product, _args, { services: { prices }}, _info) => 
      prices.getById(product.id);
  }
};

...

Demo

Price resolver

N + 1 problem

Dataloader

Resolver

Resolver

Resolver

input 1

input 2

input 3

request

response

output 2

output 3

output 1

Context

overwrite: true
schema: "http://localhost:4000"
documents: null
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: ../context#Context
import { ProductsService } from "./services/products.service";
import { PriceService } from "./services/price.service";

export interface Context {
  services: {
    products: ProductsService;
    prices: PricesService;
  };
  dataloaders: {
    price: DataLoader<string, number>;
  };
}

PricesService

import fetch from "node-fetch";
import { Products } from "../generated/graphql";

export class PricesService {
  async getById(id: string): Promise<number> {
    const res = await fetch(
      `http://localhost:8080/price/${id}`
    );
    
    return await res.json();
  }
  
  async getByIds(ids: readonly string[]): Promise<number[]> {
    const res = await fetch(
      `http://localhost:8080/price?ids=${ids.join(",")}`
    );
    
    return await res.json();
  }
}

Dataloader

...
import DataLoader from "dataloader";

...

const resolvers: Resolvers = {
  ...
  Product: {
    price: (product, _args, { dataloaders: { prices } }) => 
      prices.load(product.id),
  }
};

const server = new ApolloServer({
  ...
  context: () => {
    const pricesService = new PricesService();
    return {
      services: {
        products: new ProductsService(),
        prices: pricesService,
      },
      dataloaders: {
        prices: new DataLoader(pricesService.getByIds),
      }
    };
  }
});

Demo

price, products, cartInfo, cart dataloaders

Interfaces/unions

Interfaces

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  interface Product {
    id: ID!
    title: String!
    price: Float!
  }

  type ProductInStock implements Product {
    id: ID!
    title: String!
    price: Float!
    
    quantity: Quantity!
  }

  type Quantity {
    min: Int!
    step: Int!
    max: Int!
  }

  type ProductOutOfStock implements Product {
    id: ID!
    title: String!
    price: Float! 
  }

  type ProductReplaced implements Product {
    id: ID!
    title: String!
    price: Float!

    replacement: ProductInStock!
  }

  type Query {
    products(pagination: PaginationParams!): [Product!]!
  }
`;

const resolvers: Resolvers = {
  ...
};

...

Interfaces

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  Product: {
    __resolveType: (product: Product) => {
      if (!!(product as ProductInStock).quantity) {
        return "ProductInStock";
      }
      if (!!(product as ProductReplaced).replacement) {
        return "ProductReplaced";
      }
      return "ProductOutOfStock";
    },
  },
};

...

Interfaces

import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "./generated/graphql";

const typeDefs = gql`
  ...
`;

const resolvers: Resolvers = {
  ProductInStock: {
    __isTypeOf: (product) => !!product.quantity,
  },
  ProductReplaced: {
    __isTypeOf: (product) => !!product.replacement,
  },
  ProductOutOfStock: {
    __isTypeOf: () => true,
  },
};

...

This only works if it is defined after the other types!

Demo

Product interface

Demo

Product interface

Takeaways

Server

  • Identify interfaces/unions (__resolveType vs __isTypeOf)
const resolvers: Resolvers = {
  Product: {
    __resolveType: (product) => {
      if (!!(product as ProductInStock).quantity) {
        return "ProductInStock";
      }
      if (!!(product as ProductReplaced).replacement) {
        return "ProductReplaced";
      }
      return "ProductOutOfStock";
    },
  },
}

// vs

const resolvers: Resolvers = {
  ProductInStock: {
    __isTypeOf: (product) => !!product.quantity,
  },
  ProductReplaced: {
    __isTypeOf: (product) => !!product.replacement,
  },
  ProductOutOfStock: {
    __isTypeOf: (product) => !!product.title,
  },
}

Server

  • Identify interfaces/unions (__resolveType vs __isTypeOf)
  • Use dataloaders (N + 1)
// Do
const resolvers: Resolvers = {
  ProductInStock: {
    price: ({ id }, _args, { dataloaders: { price }})
      => price.load(id) 
  },
}

// Don't
const resolvers: Resolvers = {
  ProductInStock: {
    price: ({ id }, _args, { services: { price }})
      => price.getById(id) 
  },
}

Server

  • Identify interfaces/unions (__resolveType vs __isTypeOf)
  • Use dataloaders (N + 1)
  • Put all deps on the context
const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  cache: "bounded",
  context: (): Context => {
    const priceService = new PriceService();
    const cartService = new CartService();
    const productsService = new ProductsService();
    return {
      services: {
        products: productsService,
        price: priceService,
        cart: cartService,
      },
      dataloaders: {
        products: new DataLoader(...),
        price: new DataLoader(...),
        cart: new DataLoader(...),
        cartInfo: new DataLoader(...),
      },
    };
  },
});

Questions?

Server

Apollo Cache

Apollo Client stores the results of your GraphQL queries in a local, normalized, in-memory cache. This enables Apollo Client to respond almost immediately to queries for already-cached data, without even sending a network request.

Possible types

You can pass a possibleTypes option to the InMemoryCache constructor to specify supertype-subtype relationships in your schema. This object maps the name of an interface or union type (the supertype) to the types that implement or belong to it (the subtypes).

codegen.yml

overwrite: true
schema: "http://localhost:4000"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-apollo-angular"
  ./schema.graphql:
    - "schema-ast"
  ./fragmentTypes.json:
    plugins:
      - fragment-matcher
config:
  addExplicitOverride: true
  nonOptionalTypename: true
import introspectionQueryResultData from '../../fragmentTypes.json';

export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> {
  return {
    link: httpLink.create({ uri }),
    cache: new InMemoryCache({
      possibleTypes: introspectionQueryResultData.possibleTypes,
    }),
  };
}
{
  "possibleTypes": {
    "Product": [
      "ProductInStock",
      "ProductOutOfStock",
      "ProductReplaced"
    ],
    "ProductResult": [
      "NotFound",
      "ProductInStock",
      "ProductOutOfStock",
      "ProductReplaced"
    ]
  }
}

Fetch policies

Demo

Performance issue

Mutations

In addition to fetching data using queries, Apollo also handles GraphQL mutations. Mutations are identical to queries in syntax, the only difference being that you use the keyword mutation instead of query to indicate that the operation is used to change the dataset behind the schema.

Demo

Add to cart / remove from cart

Stale data fix

  • Refetch queries
  • Update local cache

Cache interaction

Demo

Fix stale data

Optimistic response

It's often possible to predict the most likely result of a mutation before your GraphQL server returns it. Apollo Client can use this "most likely result" to update your UI optimistically, making your app feel more responsive to the user.

Demo

Optimistic response

Error policies

Batching

Demo

Enable batching + optimize

Takeaways

Client

  • Split queries for user experience
export class SmartComponent implements OnInit {
  data$ = this.completeDataSetGQL
    .watch({ ... }, {
      fetchPolicy: 'cache-only',
    })
    .valueChanges.pipe(
      map(...)
    );
      
  ngOnInit(): void {
    this.fastFieldsGQL
      .fetch({ ... })
      .subscribe(() => console.log('Fast is loaded'));

    this.slowFieldsGQL
      .fetch({ ... })
      .subscribe(() => console.log('Slow is loaded'));
  }
}

Client

  • Split queries for user experience
  • Batch queries so the server can optimise
const uri = 'http://localhost:4000';
export function createApollo(httpLink: HttpLink)
  : ApolloClientOptions<any> {
  return {
    link: split(
      ({ getContext }) => getContext()['noBatch'],
      httpLink.create({ uri }),
      new BatchHttpLink({ uri })
    ),
    ...
  };
}
  
// ...
this.productsListPricesGql
  .fetch(PAGINATATION, {
    context: {
      noBatch: true,
    },
  })
  .subscribe(() => console.log('Data is loaded'));
 

Client

  • Split queries for user experience
  • Batch queries so the server can optimise
  • Use optimistic responses and manipulate the cache
export class SmartComponent implements OnInit {
  addProductToCart({ product: { id, price }, quantity }: AddToCartEvent) {
    this.addToCartGQL
      .mutate(
        { id, quantity },
        {
          optimisticResponse: {
            __typename: 'Mutation',
            addToCart: true,
          },
          update: (proxy, _, { variables }) => {
            const { id, quantity } = variables!;
            proxy.writeFragment({
              id: `ProductInStock:${id}`,
              fragment: CartInfoFragmentDoc,
              data: {
                __typename: 'ProductInStock',
                cartInfo: {
                  id,
                  quantity,
                  total: price! * quantity,
                },
              },
            });
          },
        }
      )
      .subscribe(() => console.log('Product is added to cart'));
  }
}

Client

  • Split queries for user experience
  • Batch queries so the server can optimise
  • Use optimistic responses and manipulate the cache
  • Set the proper fetch-policies per query
export class SmartComponent implements OnInit {
  data$ = this.completeDataSetGQL
    .watch({ ... }, {
      fetchPolicy: 'cache-only',
    })
    .valueChanges.pipe(
      map(...)
    );
      
  ngOnInit(): void {
    this.fastFieldsGQL
      .fetch({ ... })
      .subscribe(() => console.log('Fast is loaded'));

    this.slowFieldsGQL
      .fetch({ 
        fetchPolicy: 'cache-and-network',
      })
      .subscribe(() => console.log('Slow is loaded'));
  }
}

Questions?

Client

codegen.yml

overwrite: true
schema: "http://localhost:4000"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-apollo-angular"
  ./schema.graphql:
    - "schema-ast"
config:
  addExplicitOverride: true
  nonOptionalTypename: true
query ProductsList($page: Int!, $size: Int!) {
  products(pagination: { page: $page, size: $size }) {
    results {
      id
      title
      description
      ... on ProductInStock {
        quantity {
          min
          step
          max
        }
      }
      ... on ProductReplaced {
        replacement {
          id
          title
        }
      }
    }
  }
}

codegen.yml

overwrite: true
schema: "http://localhost:4000"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-apollo-angular"
  ./schema.graphql:
    - "schema-ast"
config:
  addExplicitOverride: true
  nonOptionalTypename: true
export type ProductsListQuery = {
  __typename: 'Query', 
  products: { 
    __typename: 'Products', 
    results: Array<{
      __typename: 'ProductInStock', 
      id: string, 
      title: string, 
      description: string, 
      quantity: { 
        __typename: 'Quantity',
        min: number, 
        step: number,
        max: number 
      }
    } | { 
      __typename: 'ProductOutOfStock', 
      price: number, 
      id: string, 
      ...
    }
}

@Injectable({
  providedIn: 'root'
})
export class ProductsListGQL extends Apollo.Query<ProductsListQuery, ProductsListQueryVariables> {
  override document = ProductsListDocument;

  constructor(apollo: Apollo.Apollo) {
    super(apollo);
  }
}
export function useProductsListQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<ProductsListQuery, ProductsListQueryVariables>) {
  return ApolloReactHooks.useQuery<ProductsListQuer, ProductsListQueryVariables>(ProductsListDocument, baseOptions);
}

GraphQL

By rachnerd

GraphQL

  • 171