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 →

GraphQL.com

LearnTutorialsCommunityEvents

Table of contents

    GRAPHQL BASICS

  • What is GraphQL?
  • The Query
  • Introducing Types
  • Scalars, Objects, and Lists
  • Nullability
  • Querying between Types
  • SchemaInspecting the schemaUsing the Query typeBuilding a queryAnnotating the schemaIntrospection
  • Enums
  • Interfaces & Unions
  • Arguments
  • Mutations
  • FAQ


Try GraphOS  
The GraphQL developer platform

Schema

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.

Inspecting the schema

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: Float
hasEdibleSeeds: Boolean
nutrients: [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.

Using the Query type

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: Float
hasEdibleSeeds: Boolean
nutrients: [String]
}
type Vegetable {
id: ID!
name: String!
quantity: Int!
price: Int!
averageWeight: Float
vegetableFamily: String
isPickled: 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.

Building a query

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: Float
hasEdibleSeeds: Boolean
nutrients: [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 {
name
price
nutrients
}
}

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 {
name
price
nutrients
vendor {
name
stallNumber
}
}
}

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 {
name
price
nutrients
vendor {
name
stallNumber
availableFruits {
name
price
}
}
}
}

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.

Annotating the schema

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!

An apple that has had a bite taken out of it, along with a note reminding
someone to finish the rest later.

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 map
availableFruits: [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".

Introspection

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: Enums  

About

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