Introduction to GraphQL Server Development
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:
- The
users
field onQuery
means the API exposes ausers
query:
# The `users` root field on `Query` allows for a query like this
query {
users {
id
name
}
}
- Similarly, the
createUser
root field on theMutation
type means that it is possible to send acreareUser
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):
parent
(also sometimes calledroot
): 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. Theparent
argument always represents the return value from the previous resolver call. See here for more info.args
: Potential arguments that were provided for that field (e.g. thename
of theUser
in the example of thecreateUser
mutation above).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).info
: An AST representation of the query or mutation. You can read more about in details in this article: Demystifying theinfo
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.