GraphQL the holy contract between client and server @nodkz Pavel Chertorogov with GraphQL since 2015 3
Client-server intro 4 Intro Client-server apps
Redis HTML XML PostgreSQL REST HTTP ES URL JSON MongoDB form-data WS etc… MySQL url-encoded etc… etc… SERVER CLIENTS
Java Python iOS .NET NodeJS Go Browsers Android etc… C# Ruby etc… 5 Intro Backend capabilities
Invoice Customer
id id 1 customer_id * name amount date
Simple DB Schema Diagram 6 Intro
Wordpress 4.4.2 Schema (12 tables)
Drupal8 Schema (60 tables) SalesLogix Schema (168 tables) 7 Intro
… and even more Monster with 333 tables Order table (138 fields)
Hi-Res: http://anna.voelkl.at/wp-content/uploads/2016/12/ce2.1.3.png Magento: Commerce Solutions for Selling Online (333 tables) 8 Intro
How to create API for this db-schema HELL? 9 Intro
Wait! I have a better plan! 10 GraphQL Basics This is GraphQL Schema
Wikipedia MBT-70 schema 11
GraphQL basics 12 GraphQL Basics GraphQL – is a …
query query language executor + C# for APIs on Schema .NET Python NodeJS for Frontenders for Backenders Ruby Go etc… 13 GraphQL Basics GraphQL Query Language
GraphQL query Response in JSON 14 GraphQL Basics GraphQL Query Language
GraphQL query Response in JSON 15 GraphQL Basics GraphQL Schema
GraphQL Schema Single entrypoint
Query Mutation Subscription Type Type Type
READ (stateless) WRITE (stateless) EVENTS (stateful) 16 GraphQL Basics ObjectType
Type Type Invoice Name
Name id Field
Field 1 customerId Field amount Field Field N date Field 17 GraphQL Basics Field Config
Type
Field 1
Field N
Type Scalar or Object Type which returns resolve
Resolve Function with fetch logic from any data source
Args Set of input args for resolve function
Description Documentation
DeprecationReason Field hiding 18 GraphQL Basics Relations between types
Type Type Invoice Customer
Field id Int * 1 id id Field Int customerId Field Field String Int name Field amount Decimal date Field Date 19 GraphQL Basics
One does not simply draw a relation line in GraphQL!
Invoice * 1 Customer 20 GraphQL Basics Relation is a new field in your Type
Type Type Invoice Customer
id Field Field select * from invoice where id customerId Field customer_id = {customer.id} name Field Field amount Resolve invoices Field Array
customer Field Resolve Customer select * from customer where id = {invoice.customerId} 21 GraphQL Basics — Limit? Offset? Sort? Additional filtering? — No problem, add them to args and process in resolve function!
Type Type Invoice Customer
select * from invoice where id Field Field customer_id = {customer.id} id customerId Field limit {args.limit} name Field Field amount Resolve invoices Field Args Array
function (source, args, context, info ) { // access logic (check permissions) // fetch data logic (from any mix of DBs) // processing logic (operations, calcs) return data; } ANY PRIVATE BUSINESS LOGIC… 23 GraphQL Basics Schema Introspection
printSchema(schema); // txt output (SDL format)
graphql(schema, introspectionQuery); // json output (AST)
Just remove Resolve functions (private business logic) and you get PUBLIC schema 24 GraphQL Basics Schema Introspection example SDL format types type Customer { • id: Int (txt) • fields name: String args # List of Invoices for current Customer • invoices(limit: Int): [Invoice] • docs } resolve • # Show me the money • directives type Invoice { input types id: Int • customerId: Int • enums amount: Decimal # Customer data for current Invoice • interfaces customer: Customer • unions oldField: Int @deprecated(reason: "will be removed") } 25 GraphQL Basics Schema Introspection provides an ability for awesome tooling: • Autocompletion • Query validation • Documentation • Visualization • TypeDefs generation for static analysis (Flow, TypeScript)
GraphiQL — graphical interactive in-browser GraphQL IDE Eslint-plugin-graphql — check queries in your editor, CI Relay-compiler — generates type definitions from queries 26 GraphQL Basics Type definition example
const QueryType = new GraphQLObjectType({ name: 'Query', fields: () => ({ films: { type: new GraphQLList(FilmType), args: { limit: { type: GraphQLInt, defaultValue: 5 }, }, resolve: async (source, args) => { const data = await loadData(`https://swapi.co/api/films/`); return data.slice(0, args.limit); }, }, ...otherFields, }), }); 27 GraphQL Basics Type definition example
const QueryTypeQuery = new GraphQLObjectType({ name: 'Query',Type Type fields: () => ({ Field 1 filmsfield : { typetype : new GraphQLList(FilmType), Field N argsargs : { limit: { type: GraphQLInt, defaultValue: 5 }, Type }, Args resolveresolve : async (source, args) => { const data = await loadData(`...`); Resolve return data.slice(0, args.limit); }, Description }, FieldConfig ...otherFields, DeprecationReason }), }); 28 GraphQL Basics Don’t forget to read about • input types • directives • enums • interfaces • unions • fragments http://graphql.org/learn/ 29 Backend capabilities Client requirements https://graphql-compose.herokuapp.com/ GraphQL Demo 30 Fetch Network sub-fetch 1 Network RESULT Delay Delay sub-fetch 2 REST API sub-fetch N - Sub-fetch logic on client side (increase bundle size) - Over-fetching (redundant data transfer/parsing)
Query Network RESULT Delay
GraphQL + No additional network round-trip (speed) + Exactly requested fields (speed) + Sub-fetch logic implemented on server side 31 Intro A copy from one of the previous slides… Client-server apps
Redis HTML XML PostgreSQL REST HTTP ES URL JSON MongoDB form-data WS etc… MySQL url-encoded etc… etc… SERVER CLIENTS
Java Python iOS .NET NodeJS Go Browsers Android etc… C# Ruby etc… 32 GraphQL – is a query language for APIs
Specification for describing the capabilities and requirements of data models Redis PostgreSQL for client‐server apps ES HTTP MongoDB WS MySQL SCHEMA Pretty QUERY INTROSPECTION etc… etc… Language
SERVER CLIENTS NodeJS Go .NET Browsers iOS Java Ruby C# Python etc… etc… Android 33 For frontend developers
Static Analysis 34 Static Analysis Static type checks � PRODUCTIVITY • Types checks • Functions call checks • Auto-suggestion • Holy refactoring
Static program analysis is the analysis of computer software that is performed without actually executing programs 35 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing
Let’s turbo-charge our client apps static analysis with GraphQL queries 36 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing
1
Schema Introspection 37 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing
1
Schema 2 Introspection Relay-compiler Watcher Apollo-codegen 38 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing
1 File with Response Type Defs Schema 2 3 Introspection Watcher
Hey, you have a wrong Query 39 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing 4 1 File with Response Type Defs Schema 2 3 Introspection Watcher 40 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing 4 1 File with Response Type Defs Schema 2 3 5 Introspection Watcher Flowtype or TypeScript 41 Static Analysis SERVER JS CLIENT
GraphQL File with GraphQL Query Schema and Response processing 4 1 File with Response Type Defs Schema 2 3 5 Introspection Watcher Flowtype
Houston, we have a Type Check problem 6 at line 19287 col 5: possible undefined value 42 Static Analysis DEMO GraphQL Query Generated Response Type Def Crappy Code Flow typed Code Flow error 43 Balance.js Static Analysis
GraphQL Query import { graphql } from 'react-relay'; const query = graphql` query BalanceQuery { viewer { JS CLIENT
cabinet { File with GraphQL Query accountBalance and Response processing 4 } File with Response Type Defs } 3 5 }`; Watcher Flowtype 44 __generated__/BalanceQuery.graphql.js Static Analysis
Generated Response Type Def JS CLIENT File with GraphQL Query and Response processing /* @flow */ 4
File with /*:: Response Type Defs export type BalanceQueryResponse = {| 3 5 +viewer: ?{| Watcher Flowtype +cabinet: ?{| +accountBalance: ?number; |}; Writer time: 0.53s [0.37s compiling, …] |}; Created: - BalanceQuery.graphql.js |}; Unchanged: 291 files */ Written default in 0.61s 45 Balance.js Static Analysis import { graphql } from 'react-relay'; Crappy Code import * as React from 'react';
export default class Balance extends React.Component { render() { const { viewer } = this.props; return
File with
Flow typed Code import { graphql } from 'react-relay'; import * as React from 'react'; import type { BalanceQueryResponse } from './__generated__/BalanceQuery.graphql';
type Props = BalanceQueryResponse;
export default class Balance extends React.Component
} File with const query = graphql`query BalanceQuery { Response Type Defs viewer { cabinet { accountBalance } } 3 5 }`; Watcher Flowtype
47 Balance.js Static Analysis
/* @flow */ Flow typed Code import { graphql } from 'react-relay'; import * as React from 'react'; import type { BalanceQueryResponse } from './__generated__/BalanceQuery.graphql';
type Props = BalanceQueryResponse;
export default class Balance extends React.Component
} File with const query = graphql`query BalanceQuery { Response Type Defs viewer { cabinet { accountBalance } } 3 5 }`; Watcher Flowtype
48 Static Analysis Flow errors
Error: src/_demo/Balance.js:11
11: return
Error: src/_demo/Balance.js:11 11: return
type Props = BalanceQueryResponse; class Balance extends React.Component
Error: src/_demo/Balance.js:11 11: return
Denial of Service attacks aka Resource exhaustion attaks Solutions: query HugeResponse { user { • avoid nesting relations friends(limit: 1000) { friends(limit: 1000) { cost analysis on the query friends(limit: 1000) { • ... } pre-approve queries that } • } the server can execute } (persisted queries by unique ID) } using by Facebook 53 GraphQL Query Problems
N+1 query problem Solution: DataLoader query NestedQueryN1 { { const CatLoader = new DataLoader( productList { ids => Category.findByIds(ids) id ); categoryId 1 query for category { ProductList CatLoader.load(1); id CatLoader.load(2); name CatLoader.load(1); } N queries CatLoader.load(4); } for fetching every } will do just one BATCH request } Category by id on next tick to the Database 54 For backend developers Schema construction problems 55 Query Type example Problem #1: too much copy/paste const QueryType = new GraphQLObjectType({ name: 'Query', fields: () => ({ films: ..., 6 fields and persons: ..., every FieldConfig planets: ..., species: ..., consists from starships: ..., almost identical vehicles: ..., 12 ctrl+c/ctrl+v lines }), The Star Wars API }); https://swapi.co 56 Problem #1: too much copy/paste FieldConfig example for films field films: { type: new GraphQLList(FilmType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: async (source, args) => { const data = await loadData(`https://swapi.co/api/films/`); if (args && args.limit > 0) { return data.slice(0, args.limit); } return data; }, 57 Comparison of two FieldConfigs Problem #1: too much copy/paste
{ films: { type: new GraphQLList(FilmType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: async (source, args) => { const data = await loadData(`https://swapi.co/api/films/`); if (args && args.limit > 0) { return data.slice(0, args.limit); } return data; }, }, differs planets: { type: new GraphQLList(PlanetType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, only by url resolve: async (source, args) => { const data = await loadData(`https://swapi.co/api/planets/`); if (args && args.limit > 0) { return data.slice(0, args.limit); } return data; }, }, } 58 Problem #1: too much copy/paste Solution 1: you may generate your resolve functions
function createListResolve(url) { return async (source, args) => { const data = await loadData(url); if (args && args.limit > 0) { return data.slice(0, args.limit); } return data; }; create a function } which returns a resolve function 59 Problem #1: too much copy/paste Solution 1: you may generate your resolve functions
{ films: { type: new GraphQLList(FilmType), films: { args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: async (source, args) => { type: new GraphQLList(FilmType), const data = await loadData(`https://swapi.co/api/films/`); args: { limit: { type: GraphQLInt, defaultValue: 5 } }, if (args && args.limit > 0) { return data.slice(0, args.limit); resolve: createListResolve(`https://swapi.co/api/films/`), } }, return data; }, }, reduce N times 7 LoC to 1 LoC
planets: { type: new GraphQLList(PlanetType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, planets: { resolve: async (source, args) => { type: new GraphQLList(PlanetType), const data = await loadData(`https://swapi.co/api/planets/`); if (args && args.limit > 0) { args: { limit: { type: GraphQLInt, defaultValue: 5 } }, return data.slice(0, args.limit); resolve: createListResolve(`https://swapi.co/api/planets/`), } return data; }, }, }, } 60 Problem #1: too much copy/paste Solution 2: you may generate your FieldConfigs
films: { type: new GraphQLList(FilmType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: createListResolve(`https://swapi.co/api/films/`), }, differs only by `Type` and `url`
planets: { type: new GraphQLList(PlanetType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: createListResolve(`https://swapi.co/api/planets/`), }, 61 Problem #1: too much copy/paste Solution 2: you may generate your FieldConfigs
function createFieldConfigForList(type, url) { return { type: new GraphQLList(type), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: createListResolve(url), }; } create a function which returns a FieldConfig 62 Problem #1: too much copy/paste Solution 2: you may generate your FieldConfigs
films: { type: new GraphQLList(PlanetType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: createListResolve(`https://swapi.co/api/films/`), }, planets: { type: new GraphQLList(FilmType), args: { limit: { type: GraphQLInt, defaultValue: 5 } }, resolve: createListResolve(`https://swapi.co/api/planets/`), }, 10 LoC reduced to 2 LoC
{ films: createFieldConfigForList(FilmType, `https://swapi.co/api/films/`), planets: createFieldConfigForList(PlanetType, `https://swapi.co/api/planets/`), } 63 Problem #1: too much copy/paste Solution 1: you may generate your resolve functions Solution 2: you may generate your FieldConfigs
was reduced in 3 times 30 LoC
DRY principle 90 LoC (don't repeat yourself) 64 Problem #2: keep GraphQL types in sync with ORM/DB How to keep to Schemas in SYNC?
Model: User Type: User
SYNC
• add new fields With time you may: • change field types • remove fields • rename fields 65 Problem #2: keep GraphQL types in sync with ORM/DB Solution: generate GraphQL types from ORM models
Model: User Type: User
GENERATE
• via some cli/script • on server boot load (better) SSOT principle (single source of truth) 66 Problem #3: mess in types and resolve functions
InvoiceType.js CustomerType.js
id id customerId name amount Resolve invoices date
customer Resolve
select * from CUSTOMER select * from INVOICE where … where … InvoiceType.js contains CUSTOMER query CustomerType.js contains INVOICE query 67 Problem #3: mess in types and resolve functions
CustomerType.js id select * from INVOICES where … name select * from TRANSACTIONS where … invoices transactions select * from TICKETS where … tickets select * from EVENTS where … events select * from LIKES where … likes select * from MESSAGES where … messages select * from … …
CustomerType.js knows too much about queries of others types 68 Problem #3: mess in types and resolve functions
Customer AdminNoteType.js Resolve
id Resolve Product msg
Resolve Invoice
Resolve …
What if you need to restrict access for some group of users? Modify resolvers in all places? 69 Problem #3: mess in types and resolve functions Solution: GraphQL Models*
* “GraphQL Model” is not a part of GraphQL specification.
It’s suggested additional layer of abstraction for more comfortable way to construct and maintain your schema and relations into it. 70 Problem #3: mess in types and resolve functions Solution: GraphQL Models Contains:
class CustomerGQLModel { 1. type definition type: CustomerGraphQLType; 1 Type resolvers: { 2. all possible findById: { type: CustomerGraphQLType, 2 MAP
71 Problem #4: import { makeExecutableSchema } from ‘graphql-tools'; Writing Types via SDL and providing resolvers separately.
const typeDefs = ` type args const resolvers = { resolve type Query { Query: { customer(id: Int!): Customer customer: (_, { id }) => invoices(limit: Int): [Invoice] Customer.find({ id: id }), } invoices: (_, { limit }) => Invoice.findMany({ limit }),
}, type Customer { Customer: { id: Int! invoices: (source) => firstName: String Invoice.find({ customerId: source.id }), invoices: [Invoice] }, }`; }; const schema = makeExecutableSchema({ typeDefs, resolvers }); It’s nice developer experience for small to medium sized schema BUT… 72 Problem #4: import { makeExecutableSchema } from ‘graphql-tools'; Hard to work with complex input args type Query { All highlighted parts with red lines should be in sync invoices(filter: FilterInput): [Invoice] } invoices: (_, { filter }) => { const { num, dateRange, status } = filter; input FilterInput { num: Int const q = {}; dateRange: DateRangeInput if (num) q.num = num; status: InvoiceStatusEnum } if (dateRange) q['date.$inRange'] = dateRange; input DateRangeInput { if (status) q.status = status; min: Date max: Date return Post.findMany(q); } }, enum InvoiceStatusEnum { • If one InputType used in several resolvers, then the unpaid paid declined complexity of refactoring increases dramatically. } • If one InputType per resolver, then too much * This example contains an error in the code, try to find it ;) copy/paste almost similar types. 73 Problem #4: import { makeExecutableSchema } from ‘graphql-tools'; Solution: build the schema programmatically Generate FieldConfigs via your custom functions (Resolvers) …
class InvoiceGQLModel { findManyResolver(configOptions) { return { type: InvoiceType, type args: { args filter: { type: new GraphQLInputObjectType({ … })}, resolve }, resolve: (_, args) => Invoice.findMany(args), } } findByIdResolver() { … } … … and then … 74 Problem #4: import { makeExecutableSchema } from ‘graphql-tools'; Solution: build the schema programmatically …and then build your Schema from fields and your Resolvers
import { GraphQLSchema, GraphQLObjectType } from 'graphql'; import InvoiceResolvers from './InvoiceResolvers';
const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { invoices: InvoiceResolvers.findManyResolver(), ... }, }), }); http://graphql.org/graphql-js/constructing-types/ 75 Problem #4: import { makeExecutableSchema } from ‘graphql-tools';
type args type Query { resolve invoices(filter: FilterInput): [Invoice] invoices: (_, { filter }) => { … } }
combine code from different places back to FieldConfig { type type: new GraphQLList(Invoice), args: { args filter: { type: new GraphQLInputObjectType({ … })}, resolve }, resolve: (_, { filter }) => { … }, } 76 Problem #4: import { makeExecutableSchema } from ‘graphql-tools';
type Query { invoices(filter: FilterInput): [Invoice] invoices: (_, { filter }) => { … } }
combine code from different places back to FieldConfig { type: [Invoice], args: { filter: `input FilterInput { … }`, my code with }, graphql-compose resolve: (_, { filter }) => { … }, } 77 For backend developers
Graphql-compose packages 78 graphql-compose packages Graphql-compose-* — OSS packages family for generating GraphQL Types The main idea is to generate GraphQL Schema from your ORM/Mappings at the server startup with a small lines of code as possible.
MIT License Help wanted Exposes Flowtype/Typescript declarations With awful docs all packages have more than 460 starts on GitHub 79 graphql-compose packages
Graphql-compose works almost like a webpack
It bundles your Schema from different type sources 80 graphql-compose packages
ORM Schema, Schema creation workflow 1 Mapping Generate editable GraphQL Types with a set of CRUD Resolvers (FieldConfigs w/ args, type, resolve)
TypeComposer Manually created TypeComposers 2 with Resolvers or Vanilla GraphQL types Remove/add fields Wrap default Resolvers with custom business logic Create own Resolvers (FieldConfigs) Build relations between types 3 GraphQL Schema 81 graphql-compose packages Graphql-compose provides handy syntax for manual type creation
const InvoiceItemTC = TypeComposer.create(` type InvoiceItem { description: String qty: Int price: Float } `); SDL syntax for simple types (schema definition language) 82 graphql-compose packages Graphql-compose provides handy syntax for manual type creation
const InvoiceTC = TypeComposer.create({ name: 'Invoice', fields: { id: 'Int!', SDL syntax inside now: { type: 'Date', resolve: () => Date.now() Type as function, [ ] as List }, items: () => [InvoiceItemTC], }, Config Object Syntax }); for complex types 83 graphql-compose packages Graphql-compose provides methods for modifying Types
TC.addFields({ field1: …, field2: … }); TC.removeField(['field2', ‘field3']); TC.extendField('lat', { description: 'Latitude', resolve: () => {} });
TC.hasField('lon'); // boolean TC.getFieldNames(); // ['lon', 'lat'] TC.getField('lon'); // FieldConfig TC.getField('lon'); // return FieldConfig TC.getFields(); // { lon: FieldConfig, lat: FieldConfig } TC.setFields({ ... }); // completely replace all fields TOP 3 commonly TC.setField('lon', { ... }); // replace `lon` field with new FieldConfig TC.removeField('lon'); TC.removeOtherFields(['lon', 'lat']); // will remove all other fields used methods TC.reorderFields(['lat', 'lon']); // reorder fields, lat becomes first TC.deprecateFields({ 'lat': 'deprecation reason' }); // mark field as deprecated TC.getFieldType('lat'); // GraphQLFloat TC.getFieldTC('complexField'); // TypeComposer TC.getFieldArgs('lat'); // returns map of args config or empty {} if no args Bunch of other TC.hasFieldArg('lat', 'arg1'); // false TC.getFieldArg('lat', 'arg1'); // returns arg config useful methods 84 graphql-compose packages Graphql-compose create relations between Types via FieldConfig
Type as function InvoiceTC.addField('items', { solves hoisting problems type: () => ItemsTC, resolve: (source) => { return Items.find({ invoiceId: source.id }) }, }); 85 graphql-compose packages Graphql-compose create relations between Types via Resolvers
InvoiceTC.addRelation('items', { resolver: () => ItemsTC.getResolver('findMany'), prepareArgs: { filter: source => ({ invoiceId: source.id }), }, }); Prepare args for Resolver 86 graphql-compose packages Graphql-compose is a great tool for writing your own type generators/plugins
graphql-compose-json type generator graphql-compose-mongoose type generator resolver generator
graphql-compose-pagination resolver generator
graphql-compose-connection resolver generator
graphql-compose-relay type/resolver modifier
graphql-compose-elasticsearch type generator resolver generator http API wrapper � graphql-compose-aws SDK API wrapper 87 graphql-compose packages Huge GraphQL Schema example graphql-compose-aws ~700 lines of code, 2 days of work generates more than 10 000 GraphQL Types schema size ~2 Mb in SDL, ~9 Mb in json JUST IN 2 DAYS 88 graphql-compose packages AWS Cloud API in GraphQL Schema generation takes about ~1-2 seconds 125 Services 3857 Operations 6711 Input/Output params
https://graphqlbin.com/plqhO 89 graphql-compose packages
Graphql-compose schema demos
Mongoose, Elastic, Northwind https://github.com/nodkz/graphql-compose-examples https://graphql-compose.herokuapp.com
Wrapping REST API https://github.com/lyskos97/graphql-compose-swapi https://graphql-compose-swapi.herokuapp.com 90
Last words… 91 GraphQL is awesome!
less stress more success less time on coding less network traffic � less errors
PS. SOMETIMES A LOT LESS 92 Tutorials
Read medium graphql
Watch youtube graphql
Glue howtographql.com 93 Take away! GraphQL is powerful query language with great tools
GraphQL is typed so it helps with static analysis on clients
Generate GraphQL Schemas on server 94
THANKS! Pavel Chertorogov nodkz 95 GraphQL is a
for your server and client apps