Like to get input on hierarchy for GraphQL API I'm building

Hi I’m trying to model a GraphQL scheme and I’m pondering how should I design my hierarchy. My first idea is something like this.

workspace(id: "1") {
  name
  channel(id: "1") {
    name
    type
    foobar(id: "1") {
      name
      comments(first: 100) {
        id
        text
      }     
    }
    foobars(first: 100) {
      id
      name
    }
  }
}

I need to validate that user is authorized for workspace and joined that channel. In future schema might evolve into quite deep hierarchy if I’m gonna use that approach. So I’m thinking of instead creating more flat structure like this.

workspace(id: "1") {
  name
}
channel(id: "1") {
  name
  type
}
foobar(id: "1") {
  name
}
foobars(channelId: "1", first: 100) {
  id
  name
}
foobarComments(foobarId: "1", first: 100) {
  id
  text
}

When quering foobarComments I would anyway have to get workspace and channel through foobarId from database/cache and validate that current user is allowed to query them. Anyone have any experience which approach is better in the long run?

My personal view is that with GraphQL nesting is not a problem, as long as it makes sense given the relations in your data. GraphQL is in fact designed for navigating a graph of entities, and you can even reach the same entity in via different “paths”.

I am not sure I fully understand your domain model, but the first option looks better to me: you get the first 100 comments for a foobar, in a channel, within a workspace. At any level of that hierarchy, you can authorize what appropriate (like, at the workspace level you check if the current user can see this workspace, at the channel level you check the channel, etc.).

If this was a REST API, I would probably design it very differently, but again, GraphQL is meant to represent graphs.

3 Likes

Well said. REST is great to represent flat data, GraphQL was invented because real world information is relational and is usually stored in a RDMS (with foreign keys) or a graph database, and traversing related data isn’t easy to do in REST, at least out of the box.

There’s not much sense in having a flat GraphQL API.

My pro-tip: nest everything and share the same resolvers, so inner fields can use the same filters. If you have a top level query such as:

authors(country: "France", nameStartsWith: "Alexandre") { ... }

it makes sense to also support those same filters in an inner field:

books(genre: "Adventure") {
  authors(country: "France", nameStartsWith: "Alexandre") { ... }
}
3 Likes

I think there are two aspects to this. On the one hand, I tend to favor graphs and querying deeply because doing so tends to let the structure of the Graph help guide me to what I want without having to specify a ton of filters or inputs.

On the other hand when looking things up by ID specifically I really like the node pattern:

query ($fooId: ID!, $barID: ID!) {
  foo: node(id: $fooId) { ... on Foo { whatever } }
  bar: node(id: $barId) { ... on Bar { baz } }
}

This saves you from needing to write a zillion boilerplate top level “fetch X by id” fields.

4 Likes

Care to explain how does this work in practice?

How does your resolver know if it should load the data from table “foo” or table “bar”? Or does this pattern not apply to regular relational databases?

I have a zillion “fetch X by id” top level queries because I need to tell the system what to fetch where.

Ah, a key part of the node pattern is that the ids are not simply integers. If I do something like:

query { users { id } }

where the users field returns [User], then the id values are Base.encode64("User:#{user.id}").

This basically annotates each id value with the GraphQL type. This has lots of its own benefits, in that you can use middleware when ids are taken as arguments to validate that the correct type of id is being passed in.

Then in the case of the top level node field, you create a function that takes the decoded type as an argument and spits out the database table to query, with some kind of white list in place and some auth rules to make sure that the current user can in fact query that item.

All of the helpers for this sort of pattern are in the Absinthe.Relay package, which I highly recommend even if not using Relay (we aren’t). The node pattern is just simply too useful.

1 Like