Data Architecture

A tale of two API's

Dickens!

REST

  • Hierarchical (nested) response structure
  • API queried via HTTP verb + URI
  • Endpoints are based on resource types

GraphQL

  • Flattened response structure
  • API queried via request body
  • Queries define a response shape
  • Only serves requested fields
  • Uses mutations to operate on data

REST in Action

[ ] 1 or more requests

Not REST(ing) easy

  • Business requirements don't map to REST endpoints
  • Specialized services must be built to fulfill needs
  • One-off services create an obtuse, esoteric API
  • Difficult to update shared entities without full refresh
  • Reliance on new services slows development
  • Components are tightly coupled to fulfill data needs
  • Resources must be entirely re-fetched
  • Multiple costly network round-trips
  • Greater memory overhead on client
  • Data cannot be reliably cached on the client
  • Data is likely to be over-fetched

Consequences

Front End Solutions

  1. View renders and asks local service layer for data
  2. Request to API is made if client cache is stale/empty
  3. Response is normalized
  4. Normalized data is distributed to stores
  5. Stores inject metadata such as receipt time and errors
  6. Cache update triggers application re-render
  7. Data is denormalized and injected into view
  8. Actions performed on resources trigger a similar process

Our Approach

Yes, we do all this

Current FE Architecture

Normalization

{
  "id" : "1",
  "name" : "Walmart",
  "contacts" : [
    {
      "id"     : "1",
      "name"   : "Michael Scott",
      "phones" : []
    },
    {
      "id"     : "2",
      "name"   : "Pam Beasley",
      "phones" : [{
        "id"     : "1",
        "number" : "1112223344",
        "type"   : {
          "id" : "1",
          "description" : "work"
        }
      }] 
     }
  ]
}
{
  "retailers" : {
    "1" : {
      "id"       : "1",
      "name"     : "Walmart",
      "contacts" : ["1", "2"]
    }
  },
  "contacts" : {
    "1" : {
      "id"     : "1",
      "name"   : "Michael Scott",
      "phones" : []
    },
    "2" : {
      "id"     : "2",
      "name"   : "Pam Beasley",
      "phones" : ["1"]
    }
  },
  "phones" : {
    "1" : {
      "id"     : "1",
      "number" : "1112223344",
      "type"   : "1"
    }
  },
  "phoneTypes" : {
    "1" : {
      "id"          : "1",
      "description" : "work"
    }
  }
}
  • Schema must be completely duplicated on client side
  • Errors only appear at runtime since schema is not shared
  • Increased processing on client for normalization
  • Architectural pieces constantly need to be built/updated
  • Action responses often return too much data
  • REST architecture means data must be re-denormalized
  • Parent/child data needs are still highly coupled

The Struggle Bus

You Know What?

We've done it

 

But it could be so much easier

Paradigm Shift

A Product-Centric API

Relay + GraphQL

  • Queries are defined by React application
  • Client queries a schema, not distinct endpoints
  • Schema is shareable between client and server
  • Development-time schema validation
  • Queries can be composed
  • All data needs are specified upfront
  • Child data requirements are decoupled from parent
  • Application-level query type checking

High-Level Benefits

  • Enables rapid iteration even with volatile requirements
  • Increases cohesion between Back- and Front-end devs
  • GraphQL is an agnostic parser and can be fronted by any app
  • Declarative data requirements lower cognitive overhead
  • Development-time schema validation helps fail faster
  • Relay is tailored specifically for React development
  • Data-reliant rendering logic awaits request completion
  • Low-priority fields can easily be deferred and loaded later
  • Has a growing, active ecosystem backed by Facebook

The Front End

  • Aligns with functionality that has already been built
  • Relay single-handedly eliminates >50% of our process
  • Data requirements no longer require:
    • Customized actions to update stores
    • Resources to communicate with API
    • Services to handle cache checks and resource calls
    • Manual schema updates
    • Manual normalization and denormalization
    • Manual detection of loading/ready states
    • Significant client processing

The New Front End

export default Relay.createContainer(PhoneCard, {
  fragments : {
    phone : () => Relay.QL`
      fragment on Phone {
        id,
        number,
        type {
          id,
          description
        }
      }
    `
  }
});

The New Back End

import express     from 'express';
import graphQLHTTP from 'express-graphql';
import { Schema }  from '../data/schema';

const GRAPHQL_PORT  = 8080;
const graphQLServer = express();

graphQLServer.use('/', graphQLHTTP({
  schema : Schema,
  pretty : true
}));
graphQLServer.listen(GRAPHQL_PORT, () => console.log(
  `GraphQL Server is now running on http://localhost:${GRAPHQL_PORT}`
));

The Schema

const GraphQLContact = new GraphQLObjectType({
  name   : 'Contact',
  fields : {
    id   : globalIdField('Contact'),
    name : {
      type    : GraphQLString,
      resolve : (contact) => contact.name
    },
    email : {
      type    : GraphQLString,
      resolve : (contact) => contact.email
    },
    phone : {
      type    : GraphQLPhone,
      resolve : (contact) => getPhone(contact.phone)
    }
  },
  interfaces : [nodeInterface]
});

Mutations

const GraphQLChangeRetailerPrimaryContactMutation = mutationWithClientMutationId({
  name : 'ChangeRetailerPrimaryContact',
  inputFields : {
    retailer : { type : new GraphQLNonNull(GraphQLID) },
    contact  : { type : new GraphQLNonNull(GraphQLID) }
  },
  outputFields : {
    retailer : {
      type    : GraphQLRetailer,
      resolve : ({ localRetailerId }) => getRetailer(localRetailerId)
    }
  },
  mutateAndGetPayload : ({ retailer, contact }) => {
    const retailerId = fromGlobalId(retailer).id;
    const contactId  = fromGlobalId(contact).id;

    setRetailerPrimaryContact(retailerId, contactId);

    return { localRetailerId : retailerId };
  }
});

Say Yes to NodeJS

  • Reduce code duplication between FE and BE
  • FE and BE can easily jump between code bases
  • GraphQL middleware for Express/Koa/Hapi
  • Node apps can harness variety of existing middleware for common functionality (i.e. authentication w/ PassportJS)
  • Leverage existing internal NPM repository