Lessons Learned Implementing a GraphQL API

53
LESSONS LEARNED FROM IMPLEMENTING A GRAPHQL API

Transcript of Lessons Learned Implementing a GraphQL API

Page 1: Lessons Learned Implementing a GraphQL API

LESSONS LEARNED FROM IMPLEMENTING A GRAPHQL API

Page 2: Lessons Learned 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.

Page 3: Lessons Learned Implementing a GraphQL API

HI THERE

hi-there.community

Page 4: Lessons Learned Implementing a GraphQL API

Agenda

What is GraphQL?Lessons Learned from

implementing a GraphQL API

Page 5: Lessons Learned Implementing a GraphQL API

What is GraphQL?A query language for your API

Page 6: Lessons Learned Implementing a GraphQL API

Properties

Client controls data, not the serverMultiple resources in a single

requestDocumentation is awesomeType systemDeveloper tools: GraphiQL

Page 7: Lessons Learned Implementing a GraphQL API

GraphQL Demo

Page 8: Lessons Learned Implementing a GraphQL API

LESSON 1

Separation of concerns

Page 9: Lessons Learned Implementing a GraphQL API

Query Joke{ joke(id: "1") { id text funnyLevel } }

Page 10: Lessons Learned Implementing a GraphQL API

Implement the queryconst query = { joke: { type: GraphQLJoke, args: { id: { type: new GraphQLNonNull(GraphQLID) } }, resolve: (root, args) { return db.joke.findById(args.id); } } }

Page 11: Lessons Learned Implementing a GraphQL API

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; } } }

Page 12: Lessons Learned Implementing a GraphQL API

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

}

}

});

Page 13: Lessons Learned Implementing a GraphQL API

Result{ joke(id: "1") { id text funnyLevel } }

{ "joke": { "id": "1", "text": "What is the key difference between snowmen and snowwomen? Snowballs.", "funnyLevel": 0 } }

Page 14: Lessons Learned Implementing a GraphQL API

Update joke mutationmutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }

Page 15: Lessons Learned Implementing a GraphQL API

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) => { //... } }

Page 16: Lessons Learned Implementing a GraphQL API

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; }

Page 17: Lessons Learned Implementing a GraphQL API

Resultmutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }

{ "errors": [ { "message": "Invalid funny level", } ] }

Page 18: Lessons Learned Implementing a GraphQL API

Building out the schema

Retrieve a list of jokesDelete a jokeCreate a joke...

Page 19: Lessons Learned Implementing a GraphQL API

Authorization/Database logic all over theplace!

Page 20: Lessons Learned Implementing a GraphQL API

Direct effects

Logic spread around independent

GraphQL resolvers: Hard to keep in sync.

Testing dif�cult.

Hard to maintain.

Page 21: Lessons Learned Implementing a GraphQL API

Long term issues: In�exibility

Hard to switch from GraphQL to

other API protocol.

Hard to switch to other DB type.

Page 22: Lessons Learned Implementing a GraphQL API

The solution! Separation

- graphql.org

Page 23: Lessons Learned Implementing a GraphQL API

Business Logic

Single source of truth for enforcingbusiness rules.

Determines how data is retrieved,created and updated from DB.

Performs authorization for dataPerforms validation

Page 24: Lessons Learned Implementing a GraphQL API

Connecting GraphQL to the businesslogic:

Resolver functions maps directly to thebusiness logic.

Page 25: Lessons Learned Implementing a GraphQL API

Example 1: Query Joke{ joke(id: "1") { id text funnyLevel } }

Page 26: Lessons Learned Implementing a GraphQL API

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; } } }

Page 27: Lessons Learned Implementing a GraphQL API

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) } }

Page 28: Lessons Learned Implementing a GraphQL API

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;

}

}

Page 29: Lessons Learned Implementing a GraphQL API

Example 2: Mutation to update a jokemutation { updateJoke(jokeId: "1", funnyLevel: 10) { id text funnyLevel } }

Page 30: Lessons Learned Implementing a GraphQL API

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) => { //... } }

Page 31: Lessons Learned Implementing a GraphQL API

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; }

Page 32: Lessons Learned Implementing a GraphQL API

- graphql.org

Page 33: Lessons Learned Implementing a GraphQL API

Bene�ts

Single source of truth for enforcingbusiness rules.

TestabilityMaintainable

Page 34: Lessons Learned Implementing a GraphQL API

LESSON 2

Relay Compliant Schema

Page 35: Lessons Learned Implementing a GraphQL API

What is it?A GraphQL schema speci�cation that

makes strong assumptions aboutrefetching, pagination, and realizing

mutation predictability.

Page 36: Lessons Learned Implementing a GraphQL API

Client Side Caching

Page 37: Lessons Learned Implementing a GraphQL API

Joke Query

//GraphQL query: query { viewer { id jokes { id } } } //Result: { "viewer": { "id": "1", "jokes": [{ "id": "1" }] } }

Page 38: Lessons Learned Implementing a GraphQL API

SolutionCreate globally unique opaque ids

Page 39: Lessons Learned Implementing a GraphQL API

Joke Query Unique Ids

//GraphQL query: query { viewer { id jokes { id } } } //Result: { "viewer": { "id": "Sh3Ee!p=", "jokes": [{ "id": "m0=nK3Y!" }] } }

Page 40: Lessons Learned Implementing a GraphQL API

The ResultCaching becomes simpleDatabase assumptions opaque to

client

Page 41: Lessons Learned Implementing a GraphQL API

... and every object can easily berefetched

Page 42: Lessons Learned Implementing a GraphQL API

Refetching

Page 43: Lessons Learned Implementing a GraphQL API

Retrieve resource using single query

query { node(id: "E4sT3r!39g=") { id ... on Joke { title funnyness } } }

Page 44: Lessons Learned Implementing a GraphQL API

Pagination

Page 45: Lessons Learned Implementing a GraphQL API

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!"

Page 46: Lessons Learned Implementing a GraphQL API

Why Pagination?More �ne-grained controlPrevents app from being slowImproves back-end performance

Page 47: Lessons Learned Implementing a GraphQL API

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 } } } }

Page 48: Lessons Learned Implementing a GraphQL API

Opportunity to change to Relay if you

wish

Page 49: Lessons Learned Implementing a GraphQL API

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

Page 50: Lessons Learned Implementing a GraphQL API

To Sum UpLesson 1: API, Business Logic,

Persistence LayerLesson 2: Relay compliant schema

Page 51: Lessons Learned Implementing a GraphQL API

More Lessons

Authentication

Caching & Batching

Error Handling

Page 52: Lessons Learned Implementing a GraphQL API

Reach us on twitter!@excite-engineer@Mr_Blue_Sql

Page 53: Lessons Learned Implementing a GraphQL API

Thanks!