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
- View renders and asks local service layer for data
- Request to API is made if client cache is stale/empty
- Response is normalized
- Normalized data is distributed to stores
- Stores inject metadata such as receipt time and errors
- Cache update triggers application re-render
- Data is denormalized and injected into view
- 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
deck
By David Zukowski
deck
- 716