[GraphQL] Architecture

Architectural Use Cases

  1. GraphQL server with a connected database

    • Often used for greenfield projects
    • Uses single web server that implements GraphQL
    • Server resolves queries and constructs response with data that it fetches from the database
  2. GraphQL server to integrate existing system

    • Compelling use case for companies with legacy infrastructures and many different APIs
    • GraphQL can be used to unify existing systems and hide complexity of data fetching logic
    • The server doesn’t care about what the data sources are (databases, web services, 3rd party APIs, …)
  3. A hybrid approach with a connected database and integration of existing system

Resolver functions

  • GraphQL queries/mutations consist of set of fields
  • GraphQL server has one resolver function per field
  • The purpose of each resolver is to retrieve the data for its corresponding field

example:

1
2
3
4
5
6
7
8
9
query {
User(id: "er3txsa9frju") {
name
friends(first: 5) {
name
age
}
}
}

Resolver functions

1
2
3
4
User(id: String!): User
name(user: User!): String!
age(user: User!): Int!
friends(first: Int, user: User!): [User!]!

GraphQL Clients

  • Client doesn’t care where the data is coming from
  • Opportunity for new abstractions on the frontend

GraphQL execution

GraphQL doesn’t just specify a way to describe schemas and a query language to retrieve data from those schemas, but an actual execution algorithm for how those queries are transformed into results. This algorithm is quite simple at its core: The query is traversed field by field, executing “resolvers” for each field. So, let’s say we have the following schema:

1
2
3
4
5
6
7
8
9
10
11
12
type Query {
author(id: ID!): Author
}

type Author {
posts: [Post]
}

type Post {
title: String
content: String
}

The following is a query we would be able to send to a server with that schema:

1
2
3
4
5
6
7
8
query {
author(id: "abc") {
posts {
title
content
}
}
}

every field in the query can be associated with a type:

1
2
3
4
5
6
7
8
query: Query {
author(id: "abc"): Author {
posts: [Post] {
title: String
content: String
}
}
}

The execution starts at the query type and goes breadth-first. This means we run the resolver for Query.author first. Then, we take the result of that resolver, and pass it into its child, the resolver for Author.posts. At the next level, the result is a list, so in that case, the execution algorithm runs on one item at a time. So the execution works like this:

1
2
3
4
5
Query.author(root, { id: 'abc' }, context) -> author
Author.posts(author, null, context) -> posts
for each post in posts
Post.title(post, null, context) -> title
Post.content(post, null, context) -> content

At the end, the execution algorithm puts everything together into the correct shape for the result and returns that.

One thing to note is that most GraphQL server implementations will provide “default resolvers” - so you don’t have to specify a resolver function for every single field.

Batched Resolving

DataLoader

DataLoader is a generic utility to be used as part of your application’s data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching