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

Intro to GraphQL

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Norman 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

Avatar for Norman

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