lessons learned implementing a graphql api
TRANSCRIPT
LESSONS LEARNED FROM IMPLEMENTING A GRAPHQL API
Dirk-Jan @excite-engineer
Dirk is a Software Engineer with afocus on Javascript who gets (way
too) excited about writing stable andwell tested code. GraphQL fan.
Florentijn @Mr_Blue_Sql
GraphQL enthusiast. Focusing onJavascript and realizing highly
available, easy to maintain solutionson AWS. I like giving high �ves.
HI THERE
hi-there.community
Agenda
What is GraphQL?Lessons Learned from
implementing a GraphQL API
What is GraphQL?A query language for your API
Properties
Client controls data, not the serverMultiple resources in a single
requestDocumentation is awesomeType systemDeveloper tools: GraphiQL
GraphQL Demo
LESSON 1
Separation of concerns
Query Joke{ joke(id: "1") { id text funnyLevel } }
Implement the queryconst query = { joke: { type: GraphQLJoke, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: (root, args) { return db.joke.findById(args.id); } } }
Authorization: Joke can only be retrievedby creator
const query = { joke: { type: GraphQLJoke, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (root, args, context) { const data = await db.joke.findById(args.id); if (data == null) return null; /* Authorization */ const canSee = data.creator === context.viewer.id; return canSee ? data : null; } } }
Implement the Joke object type.const GraphQLJoke = new GraphQLObjectType({
name: 'Joke',
fields: {
id: {
type: new GraphQLNonNull(GraphQLID),
resolve: (data) => data.id
},
text: {
type: new GraphQLNonNull(GraphQLString),
resolve: (data) => data.text
},
funnyLevel: {
type: new GraphQLNonNull(GraphQLInt),
resolve: (data) => data.funnyLevel
}
}
});
Result{ joke(id: "1") { id text funnyLevel } }
{ "joke": { "id": "1", "text": "What is the key difference between snowmen and snowwomen? Snowballs.", "funnyLevel": 0 } }
Update joke mutationmutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }
Implementation in GraphQLconst mutation = { type: new GraphQLNonNull(GraphQLJoke), description: "Update a joke.", args: { jokeId: { type: new GraphQLNonNull(GraphQLID) }, funnyLevel: { type: new GraphQLNonNull(GraphQLInt) } }, resolve: (root, args, context) => { //... } }
Duplicate authorization logicresolve: async (root, args, context) => { /* validation */ if (args.funnyLevel < 0 || args.funnyLevel > 5) throw new Error('Invalid funny level.'); const data = await db.joke.findById(args.jokeId); if (data == null) throw new Error('No joke exists for the id'); /* authorization */ if (data.creator !== context.viewer.id) throw new Error('No joke exists for the id') /* Perform update */ data.funnyLevel = funnyLevel; async data.save(); return data; }
Resultmutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }
{ "errors": [ { "message": "Invalid funny level", } ] }
Building out the schema
Retrieve a list of jokesDelete a jokeCreate a joke...
Authorization/Database logic all over theplace!
Direct effects
Logic spread around independent
GraphQL resolvers: Hard to keep in sync.
Testing dif�cult.
Hard to maintain.
Long term issues: In�exibility
Hard to switch from GraphQL to
other API protocol.
Hard to switch to other DB type.
The solution! Separation
- graphql.org
Business Logic
Single source of truth for enforcingbusiness rules.
Determines how data is retrieved,created and updated from DB.
Performs authorization for dataPerforms validation
Connecting GraphQL to the businesslogic:
Resolver functions maps directly to thebusiness logic.
Example 1: Query Joke{ joke(id: "1") { id text funnyLevel } }
Before the splitconst query = { joke: { type: GraphQLJoke, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: async (root, args, context) { const data = await db.joke.findById(args.id); if (data == null) return null; /* Authorization */ const canSee = data.creator === context.viewer.id; return canSee ? data : null; } } }
After the splitimport Joke from "../logic"; const query = { joke: { type: GraphQLJoke, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: (root, args, context) => Joke.gen(context.viewer, args.id) } }
Business logic Layerconst areYouOwner = (viewer: User, data) => {
return viewer.id === data.creator;
}
class Joke {
static async gen(viewer: User, id: string): Promise<?Joke> {
const data = await db.joke.findById(id);
if (data == null) return null;
const canSee = areYouOwner(viewer, data);
return canSee ? new Joke(data) : null;
}
constructor(data: Object) {
this.id = String(data.id);
this.text = data.text;
this.funnyLevel = data.funnyLevel;
}
}
Example 2: Mutation to update a jokemutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }
GraphQLconst mutation = { type: new GraphQLNonNull(GraphQLJoke), description: "Update a joke.", args: { jokeId: { type: new GraphQLNonNull(GraphQLID) }, funnyLevel: { type: new GraphQLNonNull(GraphQLInt) } }, resolve: (root, args, context) => { //... } }
Single source of truth for authorizationimport Joke from "../logic"; resolve: (root, args, context) => { /* Authorization */ const joke = Joke.gen(context.viewer, args.jokeId); if (!joke) throw new Error('No joke found for the id'); /* Performs validation and updates DB */ joke.setFunnyLevel(args.funnyLevel); return joke; }
- graphql.org
Bene�ts
Single source of truth for enforcingbusiness rules.
TestabilityMaintainable
LESSON 2
Relay Compliant Schema
What is it?A GraphQL schema speci�cation that
makes strong assumptions aboutrefetching, pagination, and realizing
mutation predictability.
Client Side Caching
Joke Query
//GraphQL query: query { viewer { id jokes { id } } } //Result: { "viewer": { "id": "1", "jokes": [{ "id": "1" }] } }
SolutionCreate globally unique opaque ids
Joke Query Unique Ids
//GraphQL query: query { viewer { id jokes { id } } } //Result: { "viewer": { "id": "Sh3Ee!p=", "jokes": [{ "id": "m0=nK3Y!" }] } }
The ResultCaching becomes simpleDatabase assumptions opaque to
client
... and every object can easily berefetched
Refetching
Retrieve resource using single query
query { node(id: "E4sT3r!39g=") { id ... on Joke { title funnyness } } }
Pagination
List Example
"data" {
"viewer" {
"jokes": [
{
"id": "1",
"text": "How do you make a tissue"
},
...
{
"id": "500",
"text": "Pfooo, only half way"
},
...
{
"id": "1000",
"text": "Too many jokes! This is not funny anymore!"
Why Pagination?More �ne-grained controlPrevents app from being slowImproves back-end performance
Pagination done right using connectionapproach
query { viewer { jokes(first: 10 /*The offset*/, after: "TUr71e=!" /*Cursor*/) { edges { cursor //Cursor node { text funnyLevel } } pageInfo { //pageInfo contains info about whether there exist more edges hasNextPage } } } }
Opportunity to change to Relay if you
wish
Advantages Relay Compliant SchemaEnforce globally unique id that is
opaque
Any resource that belongs to you
can be retrieved using a single query
Pagination for lists is built in
Opportunity to change to Relay if
you wish
To Sum UpLesson 1: API, Business Logic,
Persistence LayerLesson 2: Relay compliant schema
More Lessons
Authentication
Caching & Batching
Error Handling
Reach us on twitter!@excite-engineer@Mr_Blue_Sql
Thanks!