All Articles

GraphQL Resolvers + Ramda = CRUD

I began learning GraphQL and already love how it compliments Redux by shaping API responses without actions/reducers. Writing resolvers feels a bit like writing reducers, which I already love doing with Ramda.

I’m currently following this amazing GraphQL tutorial, and wish to discuss implementing its exercises with Ramda.

Disclaimer:

These patterns are intentionally overkill, and only meant to have some Ramda fun 🐏. You’ll learn some basic GraphQL if you haven’t already. 😁

Setup

Schema

src/schema.graphql looks like this. You can only get all links for now.

type Query {
  links: [Link!]
}

type Link {
  id: ID!
  description: String!
  url: String!
}

src/links.json is based on the howtographql tutorial, just duplicated a few times for more sample data.

[
  {
    "id": "link-0",
    "url": "[www.howtographql.com](http://www.howtographql.com)",
    "description": "Fullstack tutorial for GraphQL"
  },
  {
    "id": "link-1",
    "url": "[www.howtographql.com](http://www.howtographql.com)",
    "description": "Fullstack tutorial for GraphQL"
  },
  {
    "id": "link-2",
    "url": "[www.howtographql.com](http://www.howtographql.com)",
    "description": "Fullstack tutorial for GraphQL"
  },
  {
    "id": "link-3",
    "url": "[www.howtographql.com](http://www.howtographql.com)",
    "description": "Fullstack tutorial for GraphQL"
  }
]

Here’s src/index.js

const { GraphQLServer } = require('graphql-yoga');
let links = require('./links.json');

const resolvers = {
  Query: {
    links: () => links
  }
};

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers
});

server.start(() => console.log('Running on port 4000'));

Since our links resolver returns the links array, querying for it returns the entire dataset.

Query:

query {
  links {
    id
    url
    description
  }
}

Find by ID

Let’s update src/schema.graphql and allow finding links by ID.

type Query {
  links: [Link!]
  link(id: ID!): Link
}

Now our resolver in src/index.js

const resolvers = {
  Query: {
    links: () => links,
    link: (root, { id }) => links.find((link) => link.id === id)
  }
};

Search links with the given id.

Try this query:

query {
  link(id: "link-2") {
    id
    url
    description
  }
}

Our result:

{
  "data": {
    "link": {
      "id": "link-2",
      "url": "[www.howtographql.com](http://www.howtographql.com)",
      "description": "Fullstack tutorial for GraphQL"
    }
  }
}

Works perfectly! Feel free to try other IDs.

R.find and R.propEq

Much like the native Array.find, R.find returns the first element matching your predicate function.

So we could refactor our link resolver to

const { find } = require('ramda');

const resolvers = {
  Query: {
    links: () => links,
    link: (root, { id }) => find((link) => link.id === id, links)
  }
};

But that’s not exciting enough. We can replace the predicate with R.propEq.

const { find, propEq } = require('ramda');
const idEq = propEq('id');

const resolvers = {
  Query: {
    links: () => links,
    link: (root, { id }) => find(idEq(id), links)
  }
};

R.propEq takes 3 parameters:

  1. Property name
  2. A value
  3. The object to match on

Since it’s curried, we can supply one or two params and get back a function expecting the rest. This makes partial application trivial.

We supplied 'id' as the property name to look for, then id from the link resolver as the value, and find will supply each link object as it loops over the list.

Our query results haven’t changed.

Let’s update src/schema.graphql and support adding new link resources.

type Mutation {
  post(url: String!, description: String!): Link!
}

We require a url and description, and will return the new link upon creating it.

Now we add a post resolver.

const resolvers = {
  Query: {
    links: () => links,
    link: (root, { id }) => find(propEq('id', id), links)
  },
  Mutation: {
    post: (root, { url, description }) => {
      const link = {
        id: `link-${links.length}`,
        url,
        description
      };

      links.push(link);

      return link;
    }
  }
};

Try this query:

mutation {
  post(url: "website", description: "lol") {
    id
    url
    description
  }
}

Our result:

R.merge, R.pick, and R.pipe

I think using Ramda here is overkill, but let’s experiment!

  • R.merge merges two objects
  • R.pick returns a shallow copy of an object’s chosen keys
  • R.pipe will allow merge and pick to beautifully flow, left-to-right

For more detail on pipe, see my article on it!

const { merge, pick, pipe } = require('ramda');

Mutation: {
  post: (root, args) =>
    pipe(
      pick(['url', 'description']),
      merge({ id: `link-${links.length}` }),
      (link) => {
        // OMG side-effect! O_o"
        links.push(link);

        return link;
      }
    )(args);
}

pick returns { url, description }, merge fuses it with an object containing the new id, and our last arrow function returns the new link after pushing it into the links array.

Each function’s output is supplied to the next via pipe!

Amazingly, our query results haven’t changed.

We’ve fulfilled the Create and Read portions of CRUD, now let’s do Update.

Edit src/schema.graphql

type Mutation {
  post(url: String!, description: String!): Link!
  updateLink(id: ID!, url: String, description: String): Link
}

We require an ID and optionally take the new url and description. Updated link is returned (if found).

Now src/index.js

We’ve already covered the pattern of matching objects by ID, so we can reuse idEq here.

Mutation: {
  // post: ...
  updateLink: (root, args) => {
    let newLink;

    links = links.map((link) => {
      if (idEq(link.id, args)) {
        newLink = { ...link, ...args };

        return newLink;
      }

      return link;
    });

    return newLink;
  };
}

Try this query:

mutation {
  updateLink(
    id: "link-0"
    url: "[https://bit.ly/2IzZV4C](https://bit.ly/2IzZV4C)"
  ) {
    id
    url
    description
  }
}

Successfully updated!

R.when and R.merge

updateLink’s mapping function had an if without an else.

if (idEq(link.id, args)) {
  newLink = { ...link, ...args };

  return newLink;
}

return link;

R.when is a great function to express that logic.

doubleSmallNums = when((num) => num < 10, (num) => num * 2);

doubleSmallNums(9); // 18
doubleSmallNums(10); // 10

If the first function returns true, run the second function. Else, do nothing.

See my article on when() for more info.

Spoiler alert: We’ll be using when to delete links too, so let’s abstract it right now.

const { when } = require('ramda');
const doIfMatchingId = (id) => when(idEq(id));

updateLink: (root, args) => {
  let newLink;
  const updateLink = (link) => {
    newLink = merge(link, args);
    return newLink;
  };

  links = links.map(doIfMatchingId(args.id)(updateLink));

  return newLink;
};

Our new logic reads like a sentence: “When args and link IDs are equal, create and return newLink.”

We don’t even need to specify “otherwise, do nothing” because when handles that for us!

Our query results haven’t changed.

Let’s finish off CRUD and implement Delete!

Edit src/schema.graphql

type Mutation {
  post(url: String!, description: String!): Link!
  updateLink(id: ID!, url: String, description: String): Link
  deleteLink(id: ID!): Link
}

We’ll delete and return the link if we can find it by ID.

Now src/index.js

deleteLink: (root, { id }) => {
  let linkToDelete;

  links.forEach((link, index) => {
    const matchAndRemove = (match) => {
      linkToDelete = match;
      links.splice(index, 1);
    };

    return doIfMatchingId(id)(matchAndRemove, link);
  });

  return linkToDelete;
};

Try this query:

mutation {
  deleteLink(id: "link-1") {
    id
    url
    description
  }
}

We get the expected response:

{
  "data": {
    "deleteLink": {
      "id": "link-1",
      "url": "[www.howtographql.com](http://www.howtographql.com)",
      "description": "Fullstack tutorial for GraphQL"
    }
  }
}

Then try this query:

query {
  link(id: "link-1") {
    id
    url
    description
  }
}

And get nothing back, if everything worked out:

{
  "data": {
    "link": null
  }
}

Again, this is intentionally overkill.

Ramda truly shines when you enforce immutable data structures. Here, it’s at least intriguing and helping us to think laterally because it’s such a flexible library.