Don't

REST on
GraphQL

Overview

  • Query Language
  • React, Apollo & The Graph
  • Building Your API
  • Usage Patterns
  • Development Experience
  • Imagining What's Possible
  • Final Thoughts

The

Query
Language

{
  channels {
    id
    url
    title
    moments {
      id
      by
      kind
      content
    }
  }
}

...But Where Are The Colons & Commas?

query Moi {
  me {
    id
    email
    handle
    avatar
    moments {
      id
      kind
      content
      channel
    }
  }
}
{
  "data": {
    "me": {
      "id": "ohad79sdhnsibufi",
      "email": "myemail@yoooo.com",
      "handle": "surfer_bob",
      "avatar": "http//:... || TFuI=",
      "moments": [
        {
          "id": "qwertyyyy",
          "kind": "text",
          "content": "Wadup homies?!",
          "channel": "reactnyc"
        },
        ...
      ]
    }
  }
}
response:
{
  "query": "query Moi {\n  me { ... } \n}",
  "variables": {},
  "operationName": "Moi"
}

Surprise! It's Just JSON

mutation PublishChannel ($url: String!, $title: String) {
  publishChannel (url: $url, title: $title) {
    id
    url
    title
  }
}

if (typeof mutation === "POST")

{
  "data": {
    "publishChannel": {
      "id": "aiu867aSGiunkaIBYSD88K",
      "url": "katzzz",
      "title": "All About KATZ"
    }
  }
}
response:
mutation getMe {
  getMe {
    id
    handle
    ...
  }
  
  getMyChannels {
    id
    url
    ...
  }
}

Beyond Semantic Differences

query Moi {
  me {
    id
    handle
    ...
  }

  channels {
    id
    url
    ...
  }
}

Directives: Guiding Your Query

query WhatIfs ($what: Boolean, $who: Boolean) {
  channels {
    id
    ...
    moments @skip(if: $what) {
      id
      ...
      authors @include(if: $who) {
        id
        ....
      }
    }
  }
}

Fragments, Inline & Out 

query Moments {
  moments {
    id
    kind
    content
    by {
      id
      ... on Author {
        email
      }
    }
    channel {
      ...channelByMoment
    }
  }
}

fragment channelByMoment on Channel {
  id
  url
  title
}

Where The Graph Really Shines

query {
  channels {
    id
    ...
    moments {
      id
      ...
      authors {
        id
        ....
        moments {
          id
          ...on and on and on
        }
      }
    }
  }
}

Real-Time Data Updates For Your props.isTyping Bubble

subscription ChannelActivity (
  $channel: ID!,
  $options: LiveChannelOptionInput
) {
  channelActivity (channel: $channel, options: $options) {
    event
    payload
  }
}

React,

Apollo &

The Graph

GraphQL Clients Out In The Wild

Lokka - Github
Adrenaline - Github
Apollo - Official - Github
Relay - Official - Github
DIY:
function query(query, variables, operationName) {
  return axios.post('/graphql', {
    query, variables, operationName,
  });
}

Why Apollo?

  • how I first learned GraphQL
  • open-source
  • rich & silky smooth documentation
  • Conceived by MDG - the dude(ette)s behind the Meteor framework

Point Of Departure

import React from 'react';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
  
const App = ({ error, loading, me }) => ...;
 
export default createContainer((props) => {
  let error = null;
  const Moi = Meteor.subscribe('Moi', {
    onReady: () => error && error = null,
    onStop: (e) => e && error = e,
  });

  return {
    error,
    loading: Moi.ready(),
    me: Meteor.user(),
  };
}, App);
imports/ui/app.js

Successful Inspiration

import React from 'react';
import { graphql } from 'react-apollo';
 
import query from '../queries/moi';
 
const App = ({ data: { errors, loading, me } }) => ...;
 
export default graphql(query)(App);
src/components/app.js

Redux Made A New Friend

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { ApolloProvider } from 'react-apollo';

import configClient from './apollo';
import configStore from './store';
import Application from './components/app';
 
const client = configClient({
  uri: `${protocol}//${host}/graphql`,
});

const store = configStore(window.__$__, {
  apollo: client.reducer(),
}, [client.middleware()]);

const appElement = document.getElementById('app');

render(<ApolloProvider client={client} store={store}>
    <BrowserRouter>
      <Application />
    </BrowserRouter>
  </ApolloProvider>, appElement);
src/index.js

Let's Log In

import React from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
 
const mutation = gql`
mutation Login ($handle: String!, $password: String!) {
  login (handle: $handle, password: $password) {
    id
  }
}
`;
 
class LoginForm extends React.Component {
  constructor(props) { ... }
  onSubmit(event) {
    event.preventDefault();
    this.props.mutate(this.state.handle, this.state.password)
      .then((response) => console.log(response.data.login.id));
  }
  render() {
    return <form onSubmit={this.onSubmit.bind(this)}>
      <input id="handle" type="text" ... />
      <input id="password" type="password" ... />
      <button type="submit">Log In</button>
    </form>;
  }
}
 
export default graphql(mutation)(LoginForm);
src/components/LoginForm.jsx

Optimism Is Contagious

import React from 'react';
import { graphql } from 'react-apollo';
 
import mutation from '../../schema/mutations/postMessage.gql';
 
class PostComposer extends React.Component {
  constructor(props) { ... }
  onSubmit(event) {
    ...
    this.props.mutate({
      variables: { channel, content, kind },
      optimisticResponse: {
        __type: 'Mutation', 
        postMessage: {
          id: -1,
          channel,
          content,
          kind,
          __typename: 'Post',
        },
      },
    })
  }
  render() {
    return <form onSubmit={this.onSubmit.bind(this)}>...</form>;
  }
}
 
export default graphql(mutation)(PostComposer);
src/components/PostComposer.jsx

Look Ma, No JS!

import React from 'react';
import { graphql } from 'react-apollo';
 
import query from '../../schema/queries/channels.gql';
 
const ChannelList = ({ data: { errors, loading, channels } }) => ...;
 
export default graphql(query)(ChannelList);
src/components/ChannelList.js
query Channels ($limit: Int) {
  channels (limit: $limit) {
    id
    url
    title
  }
}
schema/queries/channels.gql

How Real is Real-Time Tho?

import React from 'react';
import { graphql } from 'react-apollo';
 
import query from '../../schema/queries/moments.gql';
import subscription from '../../schema/subscriptions/momentsByChannel.gql';
 
class ChannelView extends React.PureComponent {
  ...
  render() { ... }
}
 
export default graphql(query)(ChannelView);
src/components/ChannelView.js
schema/subscriptions/momentsByChannel.gql
subscription MomentsByChannel ($channel: ID!) {
  momentsByChannel (channel: $channel) {
    id
    by
    kind
    content
  }
}

How Real is Real-Time Tho? II

class ChannelView extends React.PureComponent {
  componentWillReceiveProps() {
    const { loading, error, channel } = Props.data;
    if (loading || error) return;
    if (channel.id !== this.props.data.channel.id) {
      if (this.subscription) this.unsubscribe();
      this.subscription = this.subscribe();
    }
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  subscribe() { ... }

  unsubscribe() {
    this.subscription && this.subscription.unsubscribe();
    this.subscription = null;
  }

  render() { ... }
}
src/components/ChannelView.js

How Real is Real-Time Tho? III

class ChannelView extends React.PureComponent {
  componentWillReceiveProps() { ... }
  componentWillUnmount() { ... }
  subscribe() {
    const { data: { subscribeToMore, variables } } = this.props;

    return subscribeToMore({
      document: subscription,
      onError: error => ...,
      updateQuery: (prev, { subscriptionData }) => {
        const { momentsByChannel } = subscriptionData.data;

        return { ...prev, moments: [...prev.moments, momentsByChannel] }
      },
    });
  }
  unsubscribe() { ... }
  render() { ... }
}
src/components/ChannelView.js

Building

Your

API

There Are Many Avenues

Sandbox

Backend As A Service

Node.js

type Author {
  id: ID!
  handle: String
  avatar: URL
  channels: [Channel]
  moments: [Moment]
  email: String
}

Schema Language

type Channel { ... }

type Query {
  tasks: [String]
  me: Author
  author(
    id: ID
    ): Author
  ...
}

type Mutation {
  ....
  publishChannel(
    url: String!
    title: String
    ): Channel
  ...
}

type Subscription { ... }

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}
type Query {
  me: Author
  ...
}

type Mutation {
  login(
    user: String!
    pass: String!
    ): Author
  ...
}

type Subscription {
  moments(
    channels: [ID]
    ): Moment
  ...
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

Scalar

Under The Hood

Constructing Your Schema

const mongoose = require('mongoose');
const { makeExecutableSchema } = require('graphql-tools');
const { withFilter, PubSub } = require('graphql-subscriptions');

const Users = mongoose.model('User');
const Channels = mongoose.model('Channel');
const Posts = mongoose.model('Post');

const pubsub = new PubSub();

const schemaIndex = require('../schema/index.graphql');

const typeDefs = [schemaIndex];

const resolvers = { ... };

module.exports = makeExecutableSchema({
  typeDefs,
  resolvers,
  logger: { log: e => console.log(e) },
  allowUndefinedInResolve: true,
});
server/schema.js

Resolvers Are Your Workhorse

const resolvers = {
  Query: {
    me: root => root.user,
    author: (root, args) => Users.findOne(args.id),
    channels: async (root, { limit }) => Channels.search(limit),
    ...,
  },
  Mutation: {
    login(root, args, ctx) {
      if (root.user) return root.user;
      return Users.loginUser(args.handle, args.password, ctx);
    },
    publishChannel: (root, { url, title }) =>
      root.user && Channels.publish(url, title, root.user),
    async postMessage(root, { channel, content, kind }) {
      if (root.user) {
        const memory = await Posts.create(root.user.id, channel, content, kind);
        pubsub.publish('memory', { memory });
        return true;
      }
    },
  },
  Subscription: {
    moments: {
      subscribe: withFilter(() => pubsub.asyncIterator('memory'),
        ({ messenger: { payload } }, variables) => payload.channel === variables.channel),
    },
  },
  ...,
};
server/schema.js

Server Setup

const { execute, subscribe } = require('graphql');
const { graphqlKoa, graphiqlKoa } = require('graphql-server-koa');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const { createLocalInterface } = require('apollo-local-query');

const host = ...;
const path = ...;
const schema = require('./schema.js');

const getRootValue = async ctx => ...;

module.exports = { ... };
server/graphql.js

Server Setup II

module.exports = {
  localInterface: ...,

  graphql: graphqlKoa(async ctx => ({
    schema,
    rootValue: await getRootValue(ctx),
    context: ctx,
  })),

  graphiql: graphiqlKoa({
    endpointURL: path,
    subscriptionsEndpoint: `ws://${host}${path}`,
  }),

  createSubscriptionServer(options = {}) {
    const { server, keepAlive = 1000 } = options;

    return SubscriptionServer.create({
      schema,
      execute,
      subscribe,
     keepAlive,
    }, { server, path, });
  },
};
server/graphql.js

 

Usage

Patterns

import ApolloClient, { createNetworkInterface } from 'apollo-client';
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';

export default function createClient({ uri }) {
  const subscriptionInterface = new SubscriptionClient(uri, {
    lazy: true,
    reconnect: true,
    connectionCallback: error => console.log(error),
  });

  const networkInterface = createNetworkInterface({
    uri,
    opts: {
      credentials: 'same-origin',
    },
  });

  networkInterface.use([{
    applyMiddleware(req, next) {
      if (!req.options.headers) req.options.headers = {};
      const token = localStorage.getItem('session.token');
      req.options.headers.Authorization = token ? `JWT ${token}` : null;
      next();
    },
  }]);

  return new ApolloClient({
    dataIdFromObject: o => o.id,
    networkInterface: addGraphQLSubscriptions(networkInterface, subscriptionInterface),
    connectToDevTools: process.env.NODE_ENV !== 'production',
  });
}
src/apollo-client.js

The Client

import React from 'react';
import { graphql } from 'react-apollo';

import mutation from '../mutations/login.gql';

const ActionButton = ({ children, action }) =>
  (<button
    type="button"
    onClick={action}>{
      children
    }</button>);

export default graphql(mutation, {
  props({ ownProps: {
    children,
    onSuccess,
    onFailure,
    variables,
    options = {},
  }, mutate }) {
    return {
      children,
      action(event) {
        return mutate({ variables, ...options })
          .then(onSuccess)
          .catch(onFailure);
      },
    };
  },
})(ActionButton);
src/components/LoginButton.jsx

One Hit Wonder

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { ApolloClient, ApolloProvider, getDataFromTree } from 'react-apollo';

import configStore from '../store';
import Application from '../components/app';

export default async function render(ctx, {
  ...,
  networkInterface,
}) {
  const client = new ApolloClient({
    ..., // dataIdFromObject: o => o.id, etc.
    networkInterface,
    ssrMode: true,
  });

  const store = configStore(undefined, {
    apollo: client.reducer()
  }, [client.middleware()]);

  const app = (<StaticRouter location={ctx.path} context={ctx.state}>
    <ApolloProvider client={client} store={store}>
      <Application isServer />
    </ApolloProvider>
  </StaticRouter>);

  ...continued
}

Server Side Rendering I

src/ssr/render.js
import Html from './Html';

export default async function render(ctx, {
  css = [],
  scripts = [],
  ...,
}) {
  ...from last slide

  await getDataFromTree(app)

  const html = ReactDOMServer.renderToString(app);

  if ([301, 302, 404].includes(ctx.state.status)) {
    if (ctx.state.status === 404) ctx.status = ctx.state.status;
    else {
      ctx.status = ctx.state.status;
      return ctx.redirect(ctx.state.url);
    }
  }

  ctx.type = 'text/html';
  ctx.body = `<!DOCTYPE html>\n${ReactDOMServer.renderToStaticMarkup(
    <Html
      html={html}
      css={css}
      scripts={scripts}
      window={{
        __$__: store.getState(),
      }} />)}`;
}

Server Side Rendering II

src/ssr/render.js
const { execute } = require('graphql');
const { createLocalInterface } = require('apollo-local-query');

const schema = require('...');

async function getRootValue(ctx) {...}

app.context.localInterface = async ctx => createLocalInterface(
  { execute }, schema, { rootValue: await getRootValue(ctx), context: ctx });

app.get('/*', async (ctx, next) => {
  const networkInterface = await ctx.localInterface(ctx);
  const { css, scripts, manifest } = ctx.assets;

  await ctx.render(ctx, {
    css,
    scripts,
    manifest,
    networkInterface,
  });
});

Server Side Rendering II.V

server/app.js

Develop-ment
Experience

Lint like a boss.

.eslintrc.js
module.exports = {
  rules: {
    ...,

    'graphql/template-strings': ['error', {
      env: 'apollo',
      schemaJson: require('./schema/schema.json'),
    }],

  },
};
npm install eslint-plugin-graphql --save-dev

Parse your client-side GraphQL operations.

webpack.config.js
module.exports = {
  ...,

  module: {
    rules: [..., {
      test: /\.(graphql|gql)$/,
      loader: 'graphql-tag/loader',
    }],
  },

};
npm install graphql-tag --save-dev

Testing your schema is fun. I promise.

tests/graphql.test.js
const { graphql } = require('graphql');

describe('GraphQL', () => {
  const schema = require('../schema/schema.js');

  it('should just query', done => {
    const query = `
      query Moi {
        me {
          id
        }
      }
    `;

    const rootValue = {};
    const context = {};
    const variables = {};
    const operationName = 'Moi';

    graphql(schema, query, rootValue, context, variables, operationName)
      .then(result => {
        const me = result.data.me;
        
        return done();
      })
      .catch(done);
  });
});

G​raphiql
is your
friend.

Introspection Is Just A Query

  query IntrospectionQuery {
    __schema {
      queryType { name }
      mutationType { name }
      subscriptionType { name }
      types {
        ...FullType
      }
      directives {
        name
        description
        args {
          ...InputValue
        }
        onOperation
        onFragment
        onField
      }
    }
  }

  fragment FullType on __Type { ... }

  fragment InputValue on __InputValue { ... }

  fragment TypeRef on __Type { ... }

Imagining

What's

Possible

IoT and Web of Things (WoT)

const http = require('http');

const renderApp = require('...');
const graphql = require('...');

const unix_socket = ...;
const port = ...;

http.createServer((request, response) => {
  switch (request.method) {
    default: {
      response.statusCode = 400;
      return response.end('Try GET for UI or POST for API.');
    }
    case 'GET': {
      return renderApp(request, response);
    }
    case 'POST': {
      return graphql(request, response);
    }
  }
}).listen(unix_socket || port);


Final
Thoughts

G​raphQL
Does Not
Solve

Everything

Nor
Does It
Replace

REST/SOAP

G​raphQL
Facilitates Organic

Development

Thanks

Everyone!

Don't REST On GraphQL

By Michael Tobia

Don't REST On GraphQL

ReactNYC meetup #7

  • 684