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 theInMemoryCache
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 ofquery
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