Lessons Learned Implementing a GraphQL API

  • View
    99

  • Download
    2

Embed Size (px)

Text 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 difcult.

    Hard to maintain.

  • Long term issues: Inexibility

    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

  • 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

  • Benets

    Single source of truth for enforcingbusiness rules.

    TestabilityMaintainable

  • LESSON 2

    Relay Compliant Schema

  • What is it?A GraphQL schema specication 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!