Understand Prisma

Introduction to GraphQL Server Development

This page is introduces the basics of GraphQL server development. It is only relevant if you are not familiar with concepts such as the GraphQL schema, its root fields and resolver functions. If you already know about these, you can skip this page.

The GraphQL schema

The types in the GraphQL schema define the API operations

At the core of every GraphQL API there is a GraphQL schema that clearly defines all available API operations and data types. The schema is written using a dedicated syntax called Schema Definition Language (SDL). SDL is simple, concise and straightforward to use.

Here is an example demonstrating how to define a simple type User that has two fields, id and name:

type User {
  id: ID!
  name: String!
}

Every GraphQL schema has three special root types: Query, Mutation and Subscription. The fields on these root types are called root fields and define the actual operations of the API.

  • Query: Operations that read data.
  • Mutation: Operations that cause side effects on the server-side (e.g. write data to the database).
  • Subscription: Operations that let you subscribe to events and receive continuous updates in realtime from the server (via a long-lived connection).

Root fields define the entry-points for the API

As an example, consider the Query and Mutation types:

type Query {
  users: [User!]!
}

type Mutation {
  createUser(name: String!): User!
}

type User {
  id: ID!
  name: String!
}

Based on this GraphQL schema, it is possible to precisely derive what the available API operations are:

  1. The users field on Query means the API exposes a users query:
# The `users` root field on `Query` allows for a query like this
query {
  users {
    id
    name
  }
}
  1. Similarly, the createUser root field on the Mutation type means that it is possible to send a creareUser mutation to the API:
# The `createUser` root field on `Mutation` allows for a mutation like this
mutation {
  createUser(name: "Sarah") {
    id
  }
}

The first field in a query, mutation or subscription operation always has to be a root field from the respective GraphQL schema - otherwise the operation will be rejected by the GraphQL server.

The collection of all fields and their arguments inside a query/mutation/subscriptions is called the selection set of the operation. The type of the root field determines which fields can be further included in the operation's selection set. In the case of the above example, the types of the root fields are User and [User!]! which in both cases allows to include any fields of the User type.

If the root field had a scalar type, it wouldn't be possible to include any further fields in the selection set. As an example, consider the following GraphQL schema:

type Query {
  hello: String!
}

A GraphQL API defined by this query only accepts a single operation:

query {
  hello
}

Because the type of the hello root field is String! (which is a scalar type), it is not possible to include further fields in the selection set of the hello query.

Resolver functions

Schema definition vs Resolver implementation

GraphQL has a clear separation of schema definition and implementation. While the SDL schema definition only describes the API operations and data types, the concrete implementation is achieved with so-called resolver functions.

The combination of both, the schema definition and resolver implementations, is often referred to as an executable schema.

Every field in the GraphQL schema is backed by one resolver function, meaning there are precisely as many resolver functions as fields in the GraphQL schema (this also includes fields on types other than root types).

The resolver function for a field is responsible for fetching the data for precisely that field. For example, the resolver for the users root field above needs to fetch (and return) a list of User objects.

The GraphQL query resolution process therefore merely becomes an action of invoking the resolver functions for the fields contained in the query, because each resolver returns the data for its field.

Anatomy of a resolver function

A resolver function always takes four arguments (in the following order):

  1. parent (also sometimes called root): Queries are resolved by the GraphQL execution engine which invokes the resolvers for the fields contained in the query. Because queries can contain nested fields, there might be multiple levels of resolver execution. The parent argument always represents the return value from the previous resolver call. See here for more info.
  2. args: Potential arguments that were provided for that field (e.g. the name of the User in the example of the createUser mutation above).
  3. context: An object that gets passed through the resolver chain that each resolver can write to and read from (basically a means for resolvers to communicate and share information).
  4. info: An AST representation of the query or mutation. You can read more about in details in this article: Demystifying the info Argument in GraphQL Resolvers.

Here is a possible way how we could implement resolvers for the above schema definition (the implementation assumes there's some global object db that provides an interface to a database):

const Query = {
  users: (parent, args, context, info) => {
    return db.users()
  },
}

const Mutation = {
  createUser: (parent, args, context, info) => {
    return db.createUser(args.name)
  },
}

const User = {
  id: (parent, args, context, info) => parent.id,
  name: (parent, args, context, info) => parent.name,
}

The sample schema definition from above has exactly four fields. This resolver implementation now provides the four respective resolver functions. Notice that the resolvers for the User type can actually be omitted since their implementation is trivial and is inferred by the GraphQL execution engine.