Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Intro to GraphQL

Norman
September 11, 2018

Intro to GraphQL

Presentation for introducing GraphQL to our devs.
- General info about REST and GraphQL
- Glimpse at implementing backend in Python
- Glimpse at frontend using Javascript/React + Apollo

Norman

September 11, 2018
Tweet

More Decks by Norman

Other Decks in Programming

Transcript

  1. G R A P H Q L I N T

    R O T O Norman Thomas 2018-09-11
  2. T O C REST vs RESTish + RPC documentation validation

    usage HATEOAS GraphQL schema validation tools usage
  3. !3 H O W T O G E T S

    O M E R E S T ?
  4. T H E B A S I C S •

    Invented by Roy Fielding in 2000 • Resource identification (URI) • Self-descriptive messages • Stateless • Cacheable • Hypermedia as the engine of application state (HATEOAS) !4
  5. R I C H A R D S O N

    M AT U R I T Y M O D E L • Level 3: The Holy Grail • Level 2: Proper use of HTTP verbs ⚡ • Level 1: Resource URIs • Level 0: Many points of access !5
  6. T H E O R I G I N O

    F H AT E O A S
  7. R E S T I N P E A C

    E • Almost Level 2 • HTTP Verbs • PUT vs PATCH • HATEOAS, hate who? • Custom adaptions to request only needed fields or perform searches • Query parameters reduce cacheability !7
  8. H AT E O A S • Follows three purposes

    • document API for developers • URL construction is server-side • action availability determined by server !8
  9. H AT E O A S E X A M

    P L E S • Several competing specifications • Collection+JSON • JSON-LD + Hydra • HAL • Siren • … !9
  10. S O W H AT D O E S H

    AT E O A S L O O K L I K E ? { "class": "player", "links": [ {"rel": [ "self" ], "href": "https://api.example.com/player/1234567890/friends"}, {"rel": [ "next" ], "href": "https://api.example.com/player/1234567890/friends?page=2"} ], "actions": [{ "class": "add-friend", "href": "https://api.example.com/player/1234567890/friends", "method": "POST", "fields": [ {"name": "name", "type": "string"}, {"name": "alternateName", "type": "string"}, {"name": "image", "type": "href"} ] }], "properties": { "size": "2" }, "entities": [ { "links": [ {"rel": [ "self" ], "href": "https://api.example.com/player/1895638109"}, {"rel": [ "friends" ], "href": "https://api.example.com/player/1895638109/friends"} ], "properties": { "playerId": "1895638109", "name": "Sheldon Dong", "alternateName": "sdong", "image": "https://api.example.com/player/1895638109/avatar.png" } }, { "links": [ {"rel": [ "self" ], "href": "https://api.example.com/player/8371023509"}, {"rel": [ "friends" ], "href": "https://api.example.com/player/8371023509/friends" } ], "properties": { "playerId": "8371023509", "name": "Martin Liu", "alternateName": "mliu", "image": "https://api.example.com/player/8371023509/avatar.png" } } ] } GET https://api.example.com/player/1234567890/friends https://sookocheff.com/post/api/on-choosing-a-hypermedia-format/
  11. //In [42]: mc.campaigns.get("a43c476c42") {"id": "a43c476c42", "web_id": 639485, "type": "variate", "create_time":

    "2018-07-12T15:04:10+00:00", "archive_url": "http://eepurl.com/dAYUOX", "status": "sent", "emails_sent": 8325, "send_time": "2018-07-12T15:16:09+00:00", ...... "_links": [{"rel": "parent", "href": "https://us6.api.mailchimp.com/3.0/campaigns", "method": "GET", "targetSchema": "https://us6.api.mailchimp.com/schema/3.0/Definitions/Campaigns/CollectionResponse.json", "schema": "https://us6.api.mailchimp.com/schema/3.0/CollectionLinks/Campaigns.json"}, {"rel": "self", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42", "method": "GET", "targetSchema": "https://us6.api.mailchimp.com/schema/3.0/Definitions/Campaigns/Response.json"}, {"rel": "delete", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42", "method": "DELETE"}, {"rel": "send", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/actions/send", "method": "POST"}, {"rel": "cancel_send", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/actions/cancel-send", "method": "POST"}, {"rel": "feedback", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/feedback", "method": "GET", "targetSchema": "https://us6.api.mailchimp.com/schema/3.0/Definitions/Campaigns/Feedback/CollectionResponse.json"}, {"rel": "content", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/content", "method": "GET", "targetSchema": "https://us6.api.mailchimp.com/schema/3.0/Definitions/Campaigns/Content/Response.json"}, {"rel": "send_checklist", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/send-checklist", "method": "GET", "targetSchema": "https://us6.api.mailchimp.com/schema/3.0/Definitions/Campaigns/Checklist/Response.json"}, {"rel": "pause", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/actions/pause", "method": "POST"}, {"rel": "resume", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/actions/resume", "method": "POST"}, {"rel": "replicate", "href": "https://us6.api.mailchimp.com/3.0/campaigns/a43c476c42/actions/replicate", "method": "POST"}]} M A I L C H I M P
  12. D O C U M E N TAT I O

    N & VA L I D AT I O N • No documentation whatsoever out-of-the-box • documentation inherently out of sync • Validation only as add-on JSON-Schema • tedious to write and maintain • multiple schemas for one resource a pain !12
  13. { "$schema": "http://json-schema.org/schema#", "id": "app_inbook_marketing.json", "type": "object", "properties": { "id":

    { "type": "string" }, "realm_id": { "$ref": "GjpDefinitionsSchema.json#/definitions/id" }, "name": { "type": "string" }, "status": { "type": "string", "enum": ["new", "active", "archived"] }, "period": { "type": "object", "properties": { "from_date": { "oneOf": [ { "type": "null" }, { "$ref": "GjpDefinitionsSchema.json#/definitions/date" } ] }, "to_date": { "oneOf": [ { "type": "null" }, { "$ref": "GjpDefinitionsSchema.json#/definitions/date" } ] } }, "required": [ "from_date", "to_date" ], "additionalProperties": false }, "position": { "type": "string", "enum": ["front", "back"] }, "priority": { "oneOf": [ { "type": "null" }, { "type": "integer" } ] }, "default_tracking_code": { "oneOf": [ { "type": "null" }, { "type": "string" } ] }, "channels": { "oneOf": [ { "type": "null" }, { "type": "array", "items": { "$ref": "#/definitions/channel" } } ] }, "elements": { "type": "array", "items": { "$ref": "#/definitions/element" } }, "filters": { "type": "array", "items": { "type": "string" } }, "titles": { "type": "array", "items": { "$ref": "#/definitions/title" } } }, "required": [ "id", "realm_id", "name", "period", "position", "priority", "default_tracking_code", "channels", "elements", "filters", "titles" ], "additionalProperties": false, "definitions": { "channel": { "type": "object", "properties": { "app_name": {"type": "string"}, "tracking_code": { "oneOf": [ { "type": "null" }, { "type": "string" } ] } }, "required": [ "app_name", "tracking_code" ], "additionalProperties": false }, "element": { "type": "object", "properties": { "type": { "type": "string", "enum": ["TITLE", "COVER", "LINK", "TEXT", "IMAGE", "etc..."]}, "content": { "type": "string" }, "linked": { "type": "boolean" }, "linked_object": { "oneOf": [ { "type": "null" }, { "$ref" : "#/definitions/linked_object" }]} }, "required": [ "type", "linked", "content", "linked_object" ], "additionalProperties": false
  14. G R A P H Q L F O R

    R E S T L E S S D E V S
  15. I N T R O T O G R A

    P H Q L • Developed by Facebook in 2012, released in 2015 • Schema first! • Implementations, tools, frameworks for many languages • JS, Python, Swift, Java, C++, Scala, Perl, Haskell, Rust, Go, … • Go see for yourself: https://github.com/chentsulin/awesome-graphql • Server and client frameworks • Visual clients to interactively send queries (GraphiQL, Playground) !15
  16. • Visualizers • Code autocompletion • Browser Dev Tools •

    Mock server • DB connection tools, e.g. SQLAlchemy • Real-time subscriptions • Strong typing !16
  17. type Document { id: Int! realm_id: Int! publication_types: [PublicationType!] ean:

    EAN! title: String! subtitle: String author: String author_user_id: Int created: Date! on_sale_date: Date price: Int imprint: String vlb_kat: String bisac: Bisac thema: [Thema!] genre: String tags: [String!] language: String } S C H E M A F I R S T enum PublicationType { POD PDF EPUB MOBI IBOOKS AUDIOBOOK SOFTWARE } type Bisac { name: String! code: String! } type Thema { name: String! code: String! } !20
  18. I N L I N E D O C U

    M E N TAT I O N
  19. import re from graphene.types import Scalar from graphql.language import ast

    def _is_ean(ean): regex = r'^978\d{10}$' return re.match(regex, ean) class EAN(Scalar): class Meta: description = 'EAN' @staticmethod def serialize(ean): assert _is_ean(ean), 'Invalid EAN "{}"'.format(ean) return ean @staticmethod def parse_literal(node): if isinstance(node, ast.StringValue) and _is_ean(node.value): return node.value @staticmethod def parse_value(value): return value C U S T O M T Y P E S !23
  20. F I E L D D E P R E

    C AT I O N
  21. P R O P E R Q U E RY

    L A N G U A G E
  22. import graphene from backend.datasource.campaign import CampaignTable from .campaign_types import Campaign

    class Query(graphene.ObjectType): campaign = graphene.Field( Campaign, id=graphene.ID(required=True), description='Load a single campaign by id' ) campaigns = graphene.List( graphene.NonNull(Campaign), description='List of all campaigns' ) search_campaign = graphene.List( graphene.NonNull(Campaign), name_=graphene.String(name='name', required=True), description='Find campaign by name' ) def resolve_campaign(self, info, id): db = CampaignTable() result = db.get_campaign(id) return Campaign(**result) if result is not None else None def resolve_campaigns(self, info): db = CampaignTable() result = db.all_campaigns() return [Campaign(**entry) for entry in result] def resolve_search_campaign(self, _, name_): db = CampaignTable() result = db.find_campaign(name_) return [Campaign(**entry) for entry in result] I M P L E M E N T I N G Q U E R I E S type Query { campaign(id: ID!): Campaign campaigns: [Campaign!] searchCampaign(name: String!): [Campaign!] } from graphene.test import Client from backend.schema import schema client = Client(schema) def test_get_campaign(): result = client.execute(''' query { campaign(id:"16057911-5382-478a-a4b0-9e5082276893") { id name } } ''') assert result == { "data": { "campaign": { "id": "16057911-5382-478a-a4b0-9e5082276893", "name": "Krimi Herbst-Erscheinungen" } } }
  23. I M P L E M E N T I

    N G T H E F R O N T E N D import ApolloClient from "apollo-boost"; import { graphqlApiUrl } from '../constants' export const client = new ApolloClient({ uri: graphqlApiUrl, }) import { gql } from 'apollo-boost' export const GET_ALL_CAMPAIGNS = gql` query Campaigns { campaigns { id realm_id name status created period { from_date to_date } } } ` export const CREATE_CAMPAIGN = gql` mutation CreateCampaign($realm_id: Int!, $name: String!) { createCampaign( realm_id: $realm_id name: $name ) { ok campaign { id realm_id name status created period { from_date to_date } } } } ` import React, { Component } from 'react' import { ApolloProvider } from 'react-apollo' import { client } from ‘./graphql/client' […] Export default class extends Component { render () { return ( <ApolloProvider client={client}> […] </ApolloProvider> ) } }
  24. import { graphql, compose } from 'react-apollo' import { GET_ALL_CAMPAIGNS,

    CREATE_CAMPAIGN, DELETE_CAMPAIGN } from '../../graphql/queries' class OverviewPage extends Component { createCampaign (name) { const variables = { realm_id: 1, name } return this.props.createCampaign({ variables }) .then(() => this.props.data.refetch()) .then(() => this.setState(() => ({ showForm: false }))) .catch(error => console.log(error)) } deleteCampaign (id) { const variables = { id } return this.props.deleteCampaign({ variables, optimisticResponse: { __typename: 'Mutations', deleteCampaign: { __typename: 'DeleteCampaign', ok: true } } }) .then(() => this.props.data.refetch()) .catch(error => console.log(error)) } render () { const { classes, data } = this.props const campaigns = data.loading ? [] : data.campaigns return ( <PageWithoutSidebar classes={classes}> <CampaignList campaigns={campaigns} deleteCampaign={this.deleteCampaign.bind(this)}/> <NewCampaign showForm={this.state.showForm} toggleForm={this.toggleForm.bind(this)}/> {this.state.showForm && <NewCampaignForm createCampaign={this.createCampaign.bind(this)}/>} </PageWithoutSidebar> ) } } export default compose( graphql(CREATE_CAMPAIGN, { name: 'createCampaign' }), graphql(DELETE_CAMPAIGN, { name: 'deleteCampaign' }), graphql(GET_ALL_CAMPAIGNS) )(OverviewPage)
  25. R E S T I N G I S F

    O R T H E E L D E R LY