Lessons Learned Implementing a GraphQL API

Post on 19-Mar-2017

113 views 2 download

Transcript of Lessons Learned Implementing a GraphQL API

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!