Join us on Nov 7 for GraphQL Summit Virtual
Join API innovators, GraphQL experts, and AI trailblazers on Nov 7 at GraphQL Summit Virtual. Register for free →
The GraphQL schema makes up the comprehensive picture of everything we can do with the data in a GraphQL service. It's a collection of types and fields, along with the specific menu of operations we can use to interact with the data. In this way, the schema is like a contract between the server and the client.
If we have access to a service's schema, we can easily understand all of its querying capabilities. It's basically a manual, after all: the schema represents data objects with object types, and those object types enumerate any number of fields that give us greater detail about its properties.
type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]vendor: Stall}
This structure also gives us the ability to pick and choose what we want to know about an object. From the Fruit
type above, we could query for everything the service knows, and ask for every field! Or we could go simpler, and ask for just the name
for each fruit variety.
What we ask for from a GraphQL service depends on what we want to do with the data. The schema shows us what's possible, allowing for custom-built requests that can ask only for the types and fields needed.
To let us query anything at all, a schema requires first and foremost the Query
type. This special type defines the rules around all of the possible things we can query from the GraphQL service.
Essentially, the Query
type acts as the front door to the service: its fields give us deeper access to parts of the schema.
The schema below illustrates how the Query
type limits our pathways into the data. Though the schema contains both a Fruit
and a Vegetable
type, since the Query
type only contains fruits
, we can exclusively access data from the Fruit
type. In this schema, data concerning the Vegetable
type is inaccessible.
type Query {fruits: [Fruit!]!}type Fruit {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloathasEdibleSeeds: Booleannutrients: [String]}type Vegetable {id: ID!name: String!quantity: Int!price: Int!averageWeight: FloatvegetableFamily: StringisPickled: Boolean}
In this way, the Query
type acts as a gatekeeper. And as far as it knows, there's just no way to query for vegetables!
Fortunately, the schema is not a document we write once and commit to forever. As our data needs evolve, we can add new types, deprecate and remove fields, and carve out new pathways as fields on the Query
type to enable clients to query and access new chunks of data. As our main source of truth regarding the available data in a GraphQL service, the schema needs to be flexible enough to change quickly and easily, preserving the efficiency of the applications it supports.
We can build our queries gradually, starting with one of the fields on the Query
type, and then selecting individual fields for specific data we'd like to return. The fields that we query can return a scalar type (String
, Int
, Boolean
, Float
, ID
, as well as custom scalars) or they can return another object type. If a field's expected value is another object type, then we need to add at least one of its subfields for our query to be valid.
Let's see what this looks like in practice, using the schema 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!]!}
This Apollo Sandbox lets us send queries to a GraphQL service that uses this schema to represent its data and what we can query.
We can pick fields directly from the Documentation panel on the left, which shows how to navigate into the types the schema describes. We can click into the root Query
type to see that it only has one field for us to query—fruits
. Selecting fruits
shows us that it has an expected value of a non-null list of non-null Fruit
objects.
Because the fruits
field returns an object type, we need to make at least one selection from the Fruit
type's subfields.
Try using the Sandbox above to build a query that requests the name
, price
, and nutrients
of all the fruit in this service.
query GetAllFruit {fruits {namepricenutrients}}
When we run the query, we see the values for each Fruit
. Here, we've limited our query just to Fruit
data. Let's try jumping to another type.
If we inspect the Fruit
type, we can see that one of its fields, vendor
, returns another object type. Just as we jumped into Fruit
from the Query
type via the fruits
field, we can jump into Stall
via the Fruit
type's vendor
field!
Try updating the query below to include the name
and stallNumber
for each Fruit
type's vendor
.
query GetAllFruitAndVendor {fruits {namepricenutrientsvendor {namestallNumber}}}
Notice that the response to this query looks a lot like the first query we ran, but it nests more detailed information about the vendor
for each specific Fruit
type. The vendor
field returns a Stall
object type, from which we were able to select the name
and stallNumber
. And as you might have seen in the Documentation tab of the Sandbox, there were other fields we could have selected from the Stall
type: including details like availableFruits
, which would have brought us back to the Fruit
object type!
GraphQL runs on the power of a graph: we can imagine our Fruit
and Stall
types linked by a relationship. We can see this relationship in the way each type references the other on one of its fields: Fruit
has a particular Stall
as its vendor, and a Stall
represents all the fruit it sells as a list of Fruit
.
This relationship is traversable—meaning that when we have an object type field that references another type, we can run more complicated queries and dig deeper into our data and all the relationships it comprises.
Try using the Sandbox to build a query that returns the name
for each Fruit
, along with the name
for its associated vendor
. Then, see if you can return a list of all the fruit sold by that vendor, including each variety's name
and price
!
query GetAllFruitAndVendor {fruits {namepricenutrientsvendor {namestallNumberavailableFruits {nameprice}}}}
We can see that our query has gone full circle through our object types —starting with Query
, we leapt to Fruit
, then Stall
, and back to Fruit
again! But each time, we picked up more and more data. As we add more types to our schema, we'll see how much more detailed and efficient these queries can become in answering many questions at once.
We can improve the legibility of the schema by adding comments and descriptions to our fields and types. These annotations help anyone else who is looking at the schema understand more precisely what each component represents and how the schema is evolving. In the code, they can point out places that need improvement or extra work!
Single-line comments in GraphQL begin with the #
symbol. Like most code comments, these can be included wherever a note or a "TODO" is needed for someone working directly on the schema code. GraphQL comments are treated like whitespace, which means they're ignored and omitted when a schema is accessed in an environment like Sandbox. For that reason, they shouldn't contain any information that would be essential for someone who wants to understand how to query data from the service. Instead, their purpose is to assist us in the process of development, providing clarification or reminders for things we need to keep in mind as we make future code changes.
type Stall {id: ID!name: String!stallNumber: String! #TODO: Rename field to indicate location on market mapavailableFruits: [Fruit!]!# TODO: Add when data is ready# availableVegetables: [Vegetable!]!}
When we want to provide more descriptive clarification about a type or a field, we can use descriptions. If a description spans multiple lines, it's enclosed by three double quotation marks, """
. Single-line descriptions are enclosed in double quotes, "
, and they appear on the line above the type or field they describe.
Unlike comments, these descriptions are intended for anyone who's looking at the schema and wants to query for some data. While we strive to keep type and field names lean and simple, descriptions allow us to provide more information and context about a field's use. We can also use a description to include other important details, such as a field's unit of measurement, which can help anyone using the data make any necessary conversions or calculations.
Each of the descriptions below contains information that could assist someone pick and choose the fields they'll include in a query.
"""Represents a single kind of fruit available at the market,with details about the vendor that sells it"""type Fruit {id: ID!"The name of the kind of fruit"name: String!"How many units of the fruit are currently in stock"quantity: Int!"The unit cost of each fruit, in cents"price: Int!"Whether or not this fruit contains seeds a human should consume"hasEdibleSeeds: Boolean"A list of the three most abundant nutrients contained in this fruit"nutrients: [String]"A reference to the stall that sells this kind of fruit"vendor: Stall}
Descriptions differ from comments in another important way: they remain intact when the schema is introspected. This means that tooling that reads in a schema and outputs documentation about its types and fields will include the description information that we provide. We've already seen an example of introspection at work: the Apollo Sandbox gives us a GraphQL schema representation we can explore and use to build real queries.
In this Sandbox, we can click on any type or field and clearly see its description. We can also inspect object type descriptions relative to other object types. When we select the vendor
field on the Fruit
type, we can see the description the Fruit
type defines for it—"A reference to the stall that sells this kind of fruit".
The Apollo Sandbox can read the types of data available to query from a GraphQL service using introspection. When a schema is open to introspection, we can see the types that we're allowed to query, as well as the entry points on the Query
type that we can choose from to get started. When introspection is disabled—as is the case with most graphs running production apps—we're not able to get any information about these kinds of details.
Keeping your schema open to introspection is advisable only during development, or for a graph that exists to serve educational purposes. (Public APIs that expose and share data are another exception, as they'll often need to keep introspection on to be accessible.) This is because unless there are other security safeguards in place, anyone can access the schema—running operations to read out and change potentially sensitive data!
Up Next: EnumsAbout
GraphQL.com is maintained by the Apollo team. Our goal is to give developers and technical leaders the tools they need to understand and adopt GraphQL.
GraphQL.com 2024