GraphQL & Apollo

https://goo.gl/qYrg4q

Web Service

A l'ancienne

RESTful

{
  "movies": [
    "title": "Star Wars",
    "year": 1977,
    "directorId": 0,
  ]
}

GET  http://api.example.com/v1/movies

RESTful

{
  "firstname": "George",
  "lastname": "Lucas",
}

GET  http://api.example.com/v1/directors/0

Waterfall

SOAP

HTTP Method URL
POST http://api.example.com/v1/movies
<?xml version="1.0"?>
<soap:Envelope
    xmlns:soap="..."
    xmlns:m="...">
  <soap:Header>
  </soap:Header>
  <soap:Body>
    <m:GetAllMovies>
      <m:Genre>sf</m:Genre>
    </m:GetAllMovies>
  </soap:Body>
</soap:Envelope>
<?xml version="1.0"?>
<soap:Envelope>
  <soap:Header>
  </soap:Header>
  <soap:Body>
    <m:GetAllMoviesResponse>
      <m:Movie>
        <m:Title>Star Wars</m:Title>
        <m:Year>1977</m:Year>
        <m:DirectorId>0</m:DirectorId>
      </m:Movie>
    </m:GetAllMoviesResponse>
  </soap:Body>
</soap:Envelope>

GraphQL

Pourquoi

GraphQL ?

Fun Fact

Depuis plusieurs années

Vous utilisez déjà GraphQL

Origines

2012 - 2015

Pourquoi GraphQL ?

Le grand problème

GraphQL résout des problèmes bien réels

  • Client mobile (latence)
  • Plusieurs clients (ex: Web + iOS)
  • Micro-services
  • API REST devenue trop complexe
  • Découplage Frontend ↔ Backend

REST n'est pas la réponse

REST n'est pas la réponse

  • Ad-hoc
  • Waterfall
  • Taille des données
  • Versions
  • Grosses documentations

GraphQL améliore votre expérience de développeur

  • Relation claire entre backends & frontends
  • Moins de réunions et de problèmes de com
  • Plus besoin d'écrire de doc
  • Plus besoin de comprendre votre API
  • Outils de développements avancés

GraphQL

Mais qu'est-ce donc exactement ?

Design

  • Hiérarchie
  • Orienté Produit
  • Typage Fort
  • Requêtes précises du client
  • Introspection

Le client reçoit exactement ce qu'il a demandé

{
  movie {
    title
  }
}
{
  "movie": {
    "title": "Star Wars"
  }
}

Plusieurs ressources en une seule requête

{
  movie {
    title
    director {
      firstname
      lastname
    }
  }
}
MOVIES
DIRECTORS

Structuration des données avec des Types

{
  movie {
    title
    director {
      firstname
      lastname
    }
  }
}
type Movie {
  title: String
  director: Director
}

type Director {
  firstname: String
  lastname: String
}

Évolutions sans version

type Movie {
  title: String @deprecated
  originalTitle: String
  director: Director
  releaseYear: Int
}
type Movie {
  title: String
  director: Director
}

Expérience développeur

GraphQL

Côté Serveur

Schema

  • Description de l'API
  • Typage Fort
  • Utilisation explicite
  • Contient des Types, Enums, ...
  • Et des descriptions/commentaires !

Scalaires

  • Int
  • Float
  • String
  • Boolean
  • ID
  • <le vôtre ici>

Objets

type Movie {
  title: String!
  releaseYear: Int
  director: Person
}

type Person {
  name: String!
  age: Int!
}

Interfaces

interface NamedEntity {
  name: String
}

type Person implements NamedEntity {
  name: String
  age: Int
}

Unions

type Movie {
  title: String
  releaseYear: Int
  director: Person
}

type Person {
  name: String
  age: Int
}

union SearchResult = Movie | Person

Enums

enum Status {
  RUMORED
  IN_PRODUCTION
  RELEASED
}


type Movie {
  status: Status
}

Opérations

schema {
  # Récupération
  query: Query
  # Modification
  mutation: Mutation
  # Temps-réel
  subscription: Subscription
}

Arguments

type Query {
  movie(id: ID): Movie
}

type Movie {
  posterUrl(size: Int): String
}

Lists

type Query {
  movies(genre: String): [Movie]
}

type Movie {
  actors: [Actor]
}

Inputs

input ReviewInput {
  stars: Int!
  comment: String
}

type Review {
  id: ID!
  stars: Int!
  comment: String
}

type Mutation {
  addReview(movie: ID!,
            review: ReviewInput): Review
}

Directives

type Movie {
  title: String @deprecated
  originalTitle: String
  director: Director
  releaseYear: Int
}

GraphQL

Côté Client

Requête

query {
  movies(genre: "sf") {
    title
    director {
      name
      age
    }
  }
}
{
  "data": {
    "movies": [
      {
        "title": "Star Wars",
        "director": {
          "name": "George Lucas",
          "age": 72
        }
      }
    ]
  }
}

Document

Résultat

Plusieurs Requêtes ?

{
  movies(genre: "sf") {
    title
  }
  tvShows(genre: "sf") {
    title
  }
}
{
  "movies": [
    {
      "title": "Star Wars"
    }
  ],
  "tvShows": [
    {
      "title": "The Expanse"
    }
  ]
}

Variables

query videos ($genre: String = "all") {
  movies(genre: $genre) {
    title
  }
  tvShows(genre: $genre) {
    title
  }
}


{
  genre: "sf"
}

Directives

query videos ($onlyMovies: Boolean = false) {
  movies @include(if: true) {
    title
  }
  tvShows @skip(if: $onlyMovies) {
    title
  }
}

Fragments

query {
  movies(genre: "sf") {
    ...movieFields
  }
  movies(genre: "action") {
    ...movieFields
  }
}

fragment movieFields on Movie {
  title
  releaseYear
}

Inline Fragments

query {
  search {
    __typename
    title
    ... on Movies {
      releaseYear
    }
    ... on TvShow {
      currentSeason
    }
  }
}
{
  "search": [
    {
      "__typename": "Movie",
      "title": "Star Wars",
      "releaseYear": 1997
    },
    {
      "__typename": "TvShow",
      "title": "The Expanse",
      "currentSeason": 2
    }
  ]
}

Alias

query {
  searchResults: search {
    label: title
    ... on Movies {
      year: releaseYear
    }
    ... on TvShow {
      season: currentSeason
    }
  }
}
{
  "searchResults": [
    {
      "label": "Star Wars",
      "year": 1997
    },
    {
      "label": "The Expanse",
      "season": 2
    }
  ]
}

Mutations

mutation addReview($movie: ID!, $review: ReviewInput) {
  newReview: addReview (movie: $movie, review: $review) {
    id
    stars
    comment
  }
}
{
  "newReview": {
    "id": "f6gt7DA42-1F",
    "stars": 5,
    "comment": "This movie is awesome!"
  }
}

Introspection

query {
  __schema {
    types {
      name
    }
  }
}
{
  "__schema": {
    "types": [
      {
        "name": "Movie"
      },
      {
        "name": "TvShow"
      }
    ]
  }
}

Meteor Development Group

meteor.io

github.com/apollographql

  • Client GraphQL
    • JavaScript
    • Android
    • iOS
  • Outils serveur
    • express
    • koa
    • hapi
    • restify
    • AWS lambda

Apollo Optics

apollodata.com/optics

Apollo

Côté Serveur

Design

Serveur HTTP

Schema

Resolvers

Connectors

type

{...}

Schema

const typeDefs = `
# General entity
interface Entity {
  id: ID!
  name: String!
  image: String
}

# Movie
type Movie implements Entity {
  id: ID!
  name: String!
  image: String
  genre: String!
  releaseYear: Int
  director: Director
}
`

Connectors

const collection = new DataBaseCollection()

export function getAll () {
  return collection
}

export function getByGenre (genre) {
  return collection.filter(i => i.genre === genre)
}

export function getById (id) {
  return collection.find(i => i.id === id)
}

Resolvers

const resolvers = {
  Movie: {
    director: (movie) => Directors.getById(movie.directorId),
  },
  Query: {
    movies: (root, { genre }) => Movies.getByGenre(genre),
    movie: (root, { id }) => Movies.getById(id),
    tvShows: (root, { genre }) => TvShows.getByGenre(genre),
    tvShow: (root, { id }) => TvShows.getById(id),
    search: (root, { text }) => Search.search(text),
  },
}

Executable schema

import typeDefs from './type-defs'
import resolvers from './resolvers'

const jsSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
})

export default jsSchema

Express Server

import { graphqlExpress, graphiqlExpress }
                               from 'graphql-server-express'

import schema from './schema'

const PORT = process.env.PORT || 3000

const app = express()

// GraphQL server
app.use('/graphql', graphqlExpress({ schema }))

// GraphiQL devtool
app.use('/graphiql', graphiqlExpress({
  endpointURL: '/graphql',
}))

app.listen(PORT, () => console.log(`Listening on port ${PORT}`))

Done!

Apollo

Côté Client

Design

Query

Mutation

Subscription (Web socket)

.gql

Observable

query

Cache Normalisé

Apollo Client

import { ApolloClient, createNetworkInterface } from 'apollo-client'

// Create the apollo client
const apolloClient = new ApolloClient({
  networkInterface: createNetworkInterface({
    uri: 'http://localhost:3000/graphql',
    transportBatching: true,
  }),
  connectToDevTools: true,
})

Query

import gql from 'graphql-tag'

const MOVIES_QUERY = gql`
query Movies($genre: String) {
  movies (genre: $genre) {
    id
    name
    image
    genre
    releaseYear
  }
}`

apolloClient.query({
  query: MOVIES_QUERY,
  variables: { genre: "sf" },
}).then(result => {
  console.log(result)
})

WatchQuery

const q = apolloClient.watchQuery({
  query: MOVIES_QUERY,
})

const sub = q.subscribe({
  next: (result) => console.log(result),
  error: (error) => console.error(error),
})

// Later...

sub.unsubscribe()

Refetch

q.refetch()

q.setVariables({
  genre: 'comics',
})

q.setOptions({
  fetchPolicy: 'cache-only',
})

Pagination: FetchMore

q.fetchMore({
  variables: {
    page: this.page,
    pageSize,
  },
  updateQuery: (previousResult, { fetchMoreResult }) => {
    const newMovies = fetchMoreResult.paginatedMovies.movies
    const hasMore = fetchMoreResult.paginatedMovies.hasMore

    return {
      paginatedMovies: {
        // Merging the movie list
        movies: [...previousResult.paginatedMovies.tags, ...newMovies],
        hasMore,
      },
    }
  },
})

Mutation

apolloClient.mutate({
  mutation: gql`mutation ($label: String!) {
    addTag(label: $label) {
      id
      label
    }
  }`,
  variables: { label: newTag },
  updateQueries: {
    tagList: (previousResult, { mutationResult }) => ({
      tags: [...previousResult.tags, mutationResult.data.addTag],
    }),
  },
  optimisticResponse: {
    __typename: 'Mutation',
    addTag: {
      __typename: 'Tag',
      id: -1,
      label: newTag,
    },
  },
}).then((data) => {
  console.log(data)
}).catch((error) => {
  console.error(error)
  this.newTag = newTag
})

Apollo + Vue

github.com/Akryum/graphql-demo

conf.vuejs.org

Merci !

Made with Slides.com