GraphQL is all about getting only needed data, for faster data-fetching & easier development.
To specify data we need from a server, the server must first know the structure of its data, that's the role of a GraphQL schema!
A schema can be conceived as a scaffold, a blueprint, or a representation that describes an object or entity.
A GraphQL schema is at the core of any GraphQL server implementation. It defines the capabilities of a GraphQL server, for example, the possible queries, mutations, subscriptions, and additional types and directives.
The schema defines a hierarchy of types with fields that are populated from your back-end data stores. It also specifies exactly which data is available for clients to read and write or delete from the server.
<aside> ➡️ The schema is not responsible for defining where data comes from or how it's stored. It is entirely the resolver's duty to do that( How to work with resolvers?).
</aside>
While GraphQL schemas can be written in a programming language, they are now most often specified using what’s known as the GraphQL SDL (schema definition language), also sometimes referred to as just GraphQL schema language.
A schema defines a collection of types and the relationships between those types. In SDL there are four basic GraphQL types:
Scalar
Scalar types represent primitive leaf values in a GraphQL type system. GraphQL responses take the form of a hierarchical tree; the leaves on these trees are GraphQL scalars. They always resolve to concrete data.
GraphQL's built-in scalar types are:
Int
: A signed 32‐bit integerFloat
: A signed double-precision floating-point valueString
: A UTF‐8 character sequenceBoolean
: true
or false
ID
(serialized as a String
): A unique identifierObject - This includes the three special root operation types: Query
, Mutation
, and Subscription
.
Object types are specific to a GraphQL service, are defined with the type keyword, and start with a capital letter by convention. They define the type name and the fields present under that type. Each field in an object type can be resolved to either other object types or scalar types.
Most types in your schema will just be normal object types, but there are two types that are special within a schema:
These two types are also known as root types.
Only the Query root type is required in all GraphQL schemas for reading data, but the Mutation root type will most often also be present when the service allows for updating, adding, or deleting data. Additionally, a Subscription root type is also available (but not in Magento), to define fields that client is interested in when their values will change.
Input - Similar to object types but their purpose is to pass data like arguments to fields
Input types are similar to object types but their purpose is to pass data like arguments to fields. This can be helpful when executing a query with some filtering, or while passing the values that need to be sent to the backend in a mutation.
Enum - similar to a scalar type, but its legal values are defined in the schema
An enum is similar to a scalar type, but its legal values are defined in the schema. Enums are most useful in situations where the user must pick from a prescribed list of options.
<aside> ➡️ There's a type called: subscription which is not yet supported by Magento's GraphQL core implementation!
</aside>
Before writing a schema, you need to have answers to some questions:
To define a schema for specific data you should already have answers to the above-mentioned questions. Based on them you will have a blueprint on what data and operations the client should be supplied with. Those questions can be easily answered if you have a look at the database structure of your data.
Your schema.graphqls
file should be located under your ./etc
folder inside your Magento 2 module, if not you can create it and start structuring your own schema!
Add a @doc("description about your field, what is its purpose, etc")
after the field you want to describe.
The operator!
is put after the return type to indicate that this field is non-nullable, which means it can't return null.
Types are specifying what an object data should look like, let's have a close look at a type from the example:
type Customer @doc(description: "Customer defines the customer name and address and other details") {
created_at: String @doc(description: "Timestamp indicating when the account was created")
firstname: String @doc(description: "The customer's first name")
middlename: String @doc(description: "The customer's middle name")
lastname: String @doc(description: "The customer's family name")
email: String! @doc(description: "The customer's email address. Required")
date_of_birth: String @doc(description: "The customer's date of birth")
id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on the server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client-side.")
is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\\\Magento\\\\CustomerGraphQl\\\\Model\\\\Resolver\\\\IsSubscribed")
gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)")
...
}
So you start with type
, the name of the type, open and close curly braces, then you define your fields.
To pass arguments to your queries, add them after your field's name like so:
extends type Query {
...
isEmailAvailable( email: String! ): IsEmailAvailableOutput
}
So you add your arguments inside the ()
like a function, and you can access those from the resolver function. (How to work with Resolvers?)
Let's see how it's done in the example:
type Query {
customer: Customer @resolver(class: "Magento\\\\CustomerGraphQl\\\\Model\\\\Resolver\\\\Customer") @doc(description: "The customer query returns information about a customer account")
...
}
After assigning the return value to a field you add @resolver(class: "VENDOR\\\\MODULE\\\\<PATH_TO_RESOLVER_CLASS>")
attribute to the field. (How to work with Resolvers?)
Learn more about resolvers:
<aside>
➡️ The resolver return value should be the same type as your output type fields, so from the example above, the customer field output is Customer
type, so the resolver should return an object with the same structure and fields as the customer object has.
</aside>
Input types are used to convey complex arguments to queries or field resolvers, or simply to establish a common name for frequently used arguments.
So let's see it in the example:
type Mutation {
createCustomer (input: CustomerInput!): Customer
...
}
input CustomerInput {
firstname: String @doc(description: "The customer's first name")
middlename: String @doc(description: "The customer's middle name")
lastname: String @doc(description: "The customer's family name")
email: String! @doc(description: "The customer's email address. Required for customer creation")
date_of_birth: String @doc(description: "The customer's date of birth")
password: String @doc(description: "The customer's password")
is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter")
...
}
The createCustomer
mutation has an argument of type CustomerInput
. That's an input object type that constructs an object of arguments for reusability to be used as arguments for other fields.
<aside> ➡️
It's considered good practice to name your input according to the name of the type it's going to serve, for example, if your input will be used as arguments for Customer
type then the input's name will be CustomerInput
.
</aside>