Types bring order into the growing world of data: like big labeled boxes, they let us sift through the mess and logically sort "text" away from "numbers", and "timestamps" away from "unique identifiers."
But grouping things into their types comes with more than just a label telling us what's inside. When we understand the type of data we're working with, we know what we can and can't do with it while programming: we can do certain things with numbers that we can't with text, for example! The kind of data we work with determines the kinds of operations and transformations we can perform on it.
Using types to organize data has an additional benefit: visibility. With a common understanding of what our data is and how it can be used, we can create a single source of truth that everyone on our team can consult whenever they need to.
GraphQL uses a type system to help us understand what kind of a thing a value is. A type system is a set of rules that defines the possible types of data that we can work with in a programming language. Working within these rules helps us organize our data and how we're using it, and it also makes it easier for us to represent relationships between different types of things, as we'll see in this article.
To get started, let's dig into the two main categories of types in GraphQL: object types and scalar types.
Object types represent something with properties, or fields, that we want to retrieve data for. Think of these as characteristics that we want to know about, or the details that make the object important for whatever we're building.
A fruit stall at a farmer's market could represent Fruit
as an object type, and define fields such as name
, quantity
, price
and hasEdibleSeeds
. We can define a new object type by using the type
keyword, and giving it a name.
We might want to represent a few kinds of fruit using this type - like blueberries, pears, and bananas! But the Fruit
type itself is non-specific: it doesn't automatically represent a particular kind of fruit, but rather serves as a broader category that many different kinds of fruit could fit into. The fields name
, quantity
, averageWeight
, price
, and hasEdibleSeeds
can all be applied to fruit varieties and hold different values.
Types are here to assist us in understanding the kind of data we're working with in an application. They help us with consistency (we know what data to expect from a particular type), and they help us be more efficient (types can reference other types, which we'll see in a moment.) To that end, the object types that we create and maintain should reflect what's needed from the data and how it's being used.
For these reasons, it makes sense to keep our Fruit
object type broad and non-specific: ultimately, it should encode the common characteristics and individual values we'll be able to provide for many kinds of fruit. This gives us lots of flexibility in the range of fruit we can represent - now, and in the future! - without giving up the ability to write concise, reusable code.
In this example, the Fruit
type defines a structure for the kind of data that anything wanting to categorize itself as a "fruit" should provide. It's more like a big label we can stick on any category of fruit that fits the qualifications.
But what about the actual types of data being provided for each of those fields? How do we know that name
returns a word like "blueberry", and that quantity
returns some kind of number we can do calculations on?
Let's take another look at our Fruit
type, this time as we might actually implement it in a GraphQL schema.
type Fruit {name: Stringquantity: Intprice: IntaverageWeight: FloathasEdibleSeeds: Boolean}
Here's our type Fruit
definition, with the fields we've discussed contained within a pair of curly brackets. Each field specifies the type of data that it should hand back when it's queried. To understand this new syntax, let's dive into another kind of type: scalar types.
To describe what kind of a value each field on an object type represents, we need to provide more detail. This means that we'll notate each field with the specific type of data it should return. The most basic types of data are called scalar types; unlike object types, scalar types don't have fields of their own. We'll use them to categorize values such as text, numbers, and true or false, along with a few others.
Let's unpack the Fruit
type's fields, along with an example of an actual fruit (and its real properties) that we could use the type to represent.
If we ask for the fruit's name
, we expect a specific word or label that will help us identify the particular fruit we're looking at. In GraphQL, we represent this kind of data - characters and text, either in single words or strung together in sentences - using the scalar type String
.
When a field is "typed" as a String
in this way, the text it hands back as its value should be written with Unicode characters contained in quotation marks. Here, we can understand "characters" as the units of human writing systems (like the letters of the Latin alphabet, which English uses).
There are three fields on the Fruit
type that return number values, but we're actually representing different kinds of data. Let's start by taking a look at the quantity
and price
fields.
The quantity
field, which keeps track of how many of an item are in stock, shouldn't be a fraction (we won't sell half a blueberry, or maintain a stock of 13.33 pears). For this reason, the numbers that are valid as the value of the quantity
field should be represented by the scalar type Int
.
The Int
type, short for "integer", is ideal for this use case as it's used to represent 32-bit numbers (positive or negative non-fractional numbers, including zero, that can be written in binary using up to 32 digits of 1s and 0s).
So if the Int
type is used to represent non-fractional numbers, why would we use it to represent the type of data we expect the price
field to provide? After all, we would probably expect an item's price to look something like this: 2.99
.
Well, it's actually not the wisest practice to use fractional numbers, or decimals, when storing currency data. Most modern systems will represent monetary amounts like an item's cost in terms of the currency's smallest indivisible component - such as a penny! This lets us represent prices in a form that won't result in lost data as more and more computations are done on the number. In this case, instead of 2.99
, the equivalent amount in pennies would be 299
!
Check out the important note in the following section on the Float
type to learn more.
In contrast to quantity
and price
, the averageWeight
field on the Fruit
type is an actual decimal - or floating point - number! Numbers represented by the Float
type allow for more granular fluctuations in value as they increase or decrease. The weight of a pear in kilograms - 0.18
, on average - differs considerably from the weight of a single blueberry at 0.0005
kilograms! Without this ability to represent fractional numbers, we couldn't visualize these very precise differences. We'd either need to truncate both items' averageWeight
to 0
kilograms, or inflate them both to 1
! Not terrifically accurate. To represent numbers of this type, we'll use the scalar type Float
.
Finally, the last field on the Fruit
type should provide something like a "yes" or "no" answer to whether a particular fruit type contains edible seeds. We've given this field a value of Boolean
, which tells us that we can expect a true
or false
value when we query a fruit type's hasEdibleSeeds
field. There is no halfway point between true
and false
- no "maybe" or "almost" - so we can think of Boolean
fields like light switches, because we toggle them between two discrete values much like "on" and "off".
GraphQL provides another scalar type out of the box that we haven't yet used here. It's the ID
type, and like any ID (such as your driver's license or passport), its purpose is to help us identify something unique. When an object type includes a field with type ID
, we can use it to definitively specify which object we're talking about, particularly if other fields - even the seemingly unique name
field on our Fruit
type - have the potential to be the same!
We can supplement our Fruit
type with an ID simply by defining a new field and setting it as type ID
. A standard name for this field is id
, because many frameworks working with GraphQL know how to handle it by default!
type Fruit {id: IDname: Stringquantity: Intprice: IntaverageWeight: FloathasEdibleSeeds: Boolean}
The Apollo Sandbox below includes an operation that currently requests the name
and quantity
fields for mostPopularFruit
. Use the left-hand panel titled "Documentation" to explore the Fruit
type and test out adding other fields, then run the query to see what data appears in the Response panel.
The Documentation panel in Apollo Sandbox lets you explore all of the types of fields in a GraphQL service's schema. You can collapse or reopen it by toggling the two arrows at the top of the panel.
As you can see from the fields we've discussed on the Fruit
type, we expect fields denoted with a scalar type to hand back a single, self-contained value such as a name, a line of text in quotations, a number, or a true/false value.
This works well in a lot of cases: sometimes we only need a single piece of data for each field. But GraphQL also gives us a way to represent that a field should have the ability to return more than one item. We can achieve this using the List type.
The List type is considered a "wrapping type" in GraphQL, because unlike the scalar types we've seen so far, we don't use a particular label or keyword to denote a field as a list; instead, we use square brackets ([]
) to enclose the name of the data type the list contains.
nutrients: [String]
This means that a field can return more than one item - but it doesn't have to! A list might contain just one item, or even no items at all. But when an object has multiple bits of data that relate to just one of its fields, lists can be particularly useful.
Let's add the nutrients
field we saw above to the Fruit
type. This new field allows us to express more than the single most prevalent vitamin or mineral that the fruit contains. We might use it to provide information about the three most concentrated nutrients in a fruit, and let this drive customers' purchasing decisions.
type Fruit {id: IDname: Stringquantity: Intprice: IntaverageWeight: FloathasEdibleSeeds: Booleannutrients: [String]}
Try it out! Click into mostPopularFruit
from the Documentation panel, then add the nutrients
field to the query below.
Nullability is a term that describes whether or not a field is allowed to return null
, the programming value for nothing.
In many cases, deciding which fields can be null and which cannot is a matter of looking at the data and determining where missing values might cause the most disruption to your ability to use and understand it. These rules are also often determined by business logic and what a product's optimal user experience should look like.
Take a look at the Fruit
type listed below. What data seems most important for a customer trying to decide which fruit to buy? Which fields would make the decision even harder if they were missing data?
type Fruit {id: IDname: Stringquantity: Intprice: IntaverageWeight: FloathasEdibleSeeds: Booleannutrients: [String]}
You can see how a field's nullability hinges on what we decide is essential functionality, or necessary for the person using it!
When we want to allow a field to return a null
value in GraphQL, we don't need to change anything about its definition. The field's name, along with the type of data it returns, is all the configuration we need to allow null
as a possible return value.
But when we want to express that a field should never return null
- that is, we want to ALWAYS require that we get a value back that matches the field's expected type - we can attach an exclamation point (!
).
This is the Non-Null
type, and like List
, it's considered a "wrapping type." This means that we attach it to a field's data type definition. Whatever comes directly before the exclamation point is the element that cannot be null
. This rule becomes essential when decoding more complex types where the List
and Non-Null
types are combined.
nutrients: [String!]!
Beginning from the outside and moving inward:
nutrients
field returns a List
type and, regardless of what's inside of the list, the list itself cannot be null
. At the very least, it can be empty, returning a value of []
.String
. None of the String
items contained in the list are permitted to return a value of null
.Putting the Non-Null
constraints in place ensures that an error will occur anytime a null
value is returned.
We can pick and choose which fields on a type should restrict null
values. Let's look at one implementation of the Fruit
type.
type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]}
The fields we've denoted as non-nullable include id
, name
, quantity
, and price
. From the perspective of a fruit stall selling its products, providing customers with accurate information about how many items are in stock (is there enough for me to make a purchase?) as well as how much each item costs (do I have enough money to buy this item?) are essential to the success of the business. Providing a name
for each fruit should also be required, as this field serves as the human-friendly label that customers use to make their fruit selection.
While id
also serves a role as an identifier, its specific designation as type ID
says that the GraphQL service is responsible for ensuring that this field's value is unique between different kinds of fruits. By preventing the id
field from returning null
for any fruit we might query, we can protect the integrity and completeness of our data, as well as avoid introducing errors.
In addition to returning scalar type data, fields on an object type can return information about another object type.
Consider the two types defined below.
type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]}type Stall {id: ID!name: String!stallNumber: String!}
We have a Fruit
type, which defines properties that can be applied broadly to many different kinds of fruit. Additionally, we have a Stall
type, which we can query for details about the different food and produce stalls running in a farmer's market, like their names and location number.
When thinking about a particular stall, we might ask what kinds of fruit it sells. And alternatively, when considering a particular type of fruit, we might want to know which stall is selling it. We can traverse, or move across, the relationship between these two types to find out more information: we can go from fruit, to the stall that sells it, to other fruits available at the same stall.
When an object type includes a field that returns another object type, we can think of this like a point of entry to the second object type's fields. When types are linked in this way, we can use the connecting field like a portal: we can jump between types, gathering the information we need in one bundle, and returning home from our journey with all of it in-hand.
This is the power of the graph at work, which results from building your web of data with GraphQL: types that represent their relationships to one another in this way allow us to build more and more complex queries to drill deeper into data and discover new insights.
Let's see how this relationship can be modeled in GraphQL.
type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]vendor: Stall}type Stall {id: ID!name: String!stallNumber: String!availableFruits: [Fruit!]!}
Notice that in this example, each kind of Fruit
has a single vendor
, which is the field that holds information about the specific Stall
that sells the fruit. (Now, it might be possible for a kind of fruit to exist in the database even without a farmer who is currently selling it, which is why the value is nullable!)
We'll see something different on the Stall
type. Each stall is given a field called availableFruits
, which returns a list of the many different kinds of fruit it sells. Notice that in this case, we expect the availableFruits
field to hand back either an empty list, or a list containing actual Fruit
items. (The list itself cannot be null
, and if it contains items, those items cannot be null
.)
To query a GraphQL service for any kind of data requires a special kind of object type: the Query
type. This type defines how, and what, we can read from a service, and like the other types we've talked about, it lives in a GraphQL service's schema.
Query
is considered a "root" type because it acts as the entry point to everything we're allowed to query for. Asking for data without it would be a lot like trying to enter a warehouse without a door. In the same way that a front door lets us into a building with hallways leading to different rooms, the Query
type provides us with the possible query pathways we can use to get to data.
Throughout this chapter, you might have encountered Apollo Sandbox, the integrated sandbox environment that lets us build and test queries we send to a GraphQL service. Under the hood, these Sandboxes include a Query
type that defines that when we ask for fruits
, we should receive a list of Fruit
objects.
This is because fruits
exists as a field on the Query type, and its return value - what we expect it to hand back to us - is a list of Fruit
! Here's what this looks like implemented in GraphQL.
type Query {fruits: [Fruit!]!}
The Query
type is like the springboard that gets us into the graph, where we can jump from one object type's fields to another and build complex queries that give us exactly the data we're looking for. Right now, fruits
is the only way that we can enter the graph and begin querying for data.
When we build a query from the root Query
type, we start by selecting one of its fields. If this field returns an object type - like fruits
returning a list of Fruit
- we need to make at least one selection from that object type's subfields. Let's see an example of this in the next section.
If a query includes a field that returns an object type, it also has to include at least one subfield from that object type. To illustrate this, consider the Fruit
and Stall
types below.
type Query {fruits: [Fruit!]!}type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]vendor: Stall}type Stall {id: ID!name: String!stallNumber: String!availableFruits: [Fruit!]!}
All queries start with the available fields on the Query
type. Currently fruits
is our only entry point - the only door we can enter through to get some data - and we expect a list of Fruit
from it. Because we've now jumped to another object type - Fruit
- we need to select at least one of its fields to return in our query.
query GetAllFruit {fruits {}}
Note: The query syntax shown above begins with the
query
keyword and is followed by a descriptive operation name. Within the first set of curly braces we can define the field on theQuery
type that gives us access to specific data within our graph.
Try building this query in the embedded Apollo Sandbox below. Starting with the root Query
type, build a pathway to get the name
, price
, and quantity
of all available fruit. Want a challenge? See if you can pull the name
and stallNumber
for each fruit's vendor
field!
About
GraphQL.com is maintained by the Apollo team. Our goal is to give developers and technical leaders around the world all of the tools they need to understand and adopt GraphQL.
GraphQL.com 2023