The schema is the central document defining a GraphQL API. It is a mandatory part, which means you can be sure it is present for every deployment.
The schema defines types and fields. For example, a simple User object with a username and an email can be defined:
type User {
username: String!
email: String
}
Fields can reference other types too. For example, Users can belong to a Group:
type Group {
name: String!
}
type User {
username: String!
email: String
group: Group
}
With just these two things, you can define complex structures that can model to all sorts of use-cases.
The schema defines the structure of the graph of objects. In the above example, Users have a link to a Group, but that only means the client can move from a specific User object to its corresponding Group object.
Apart from types defined in the schema, fields can be scalar too. Some are defined by GraphQL so they are guaranteed to be available in every implementation:
String
Int
Float
Boolean
ID
ID
is a String, it's just not meant to be used for anything other than identifying things.
There are three special types in GraphQL: Query, Mutation, and Subscription.
The Query defines the entry points for a client query. Think about it like REST endpoints that users can call with some parameters and they return some data. The idea here is the same, but instead of returning a fixed structure, a Query provides the first object(s) in the graph.
For example, a query that returns a user by its username:
# schema
type Query {
user(username: String!): User
}
This gets a username
, which is a String
(and it's required, as we'll soon see) and returns a User
. Then the client can specify what parts of the User
it needs by specifying the result fields. And this can span through multiple objects, traversing the graph.
This client query asks for a specific user, its fields, then its group:
# query
query MyQuery {
user(username: "user1") {
username
email
group {
name
}
}
}
And the result:
{
"data": {
"user": {
"username": "user1",
"email": null,
"group": {
"name": "group 1"
}
}
}
}
Mutations are similar to queries, the difference is only conceptually. They also can get arguments and they can also return objects that clients can use to navigate the graph. But mutations are used to change data instead of query it.
# schema
type Mutation {
addUser(username: String!, email: String): User
}
And to add the user:
# query
mutation addUser {
addUser(username: "new user"){
username
email
}
}
This call creates a User object using the arguments provided (username
), then return the username and the email fields of the new User.
The only difference between a Query and a Mutation is conceptual:
But there is nothing that enforces this distinction. We'll see in the Resolvers chapter how to implement what a Query/Mutation does and nothing stops you if you write a Query that modifies data. There are things, such as Subscriptions and caching, that depend on a clear separation of read-only/read-write operations, but otherwise they work the same.