Docs
Launch GraphOS Studio

GraphQL schema basics


Your uses a schema to describe the shape of your available data. This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute.

This article describes the fundamental building blocks of a schema and how to create one for your .

The schema definition language

The specification defines a human-readable schema definition language (or SDL) that you use to define your schema and store it as a string.

Here's a short example schema that defines two object types: Book and Author:

schema.graphql
type Book {
title: String
author: Author
}
type Author {
name: String
books: [Book]
}

A schema defines a collection of types and the relationships between those types. In the example schema above, a Book can have an associated author, and an Author can have a list of books.

Because these relationships are defined in a unified schema, client developers can see exactly what data is available and then request a specific subset of that data with a single optimized .

Note that the schema is not responsible for defining where data comes from or how it's stored. It is entirely implementation-agnostic.

Field definitions

Most of the schema types you define have one or more fields:

# This Book type has two fields: title and author
type Book {
title: String # returns a String
author: Author # returns an Author
}

Each returns data of the type specified. A field's return type can be a scalar, object, enum, union, or interface (all described below).

List fields

A can return a list containing items of a particular type. You indicate list with square brackets [], like so:

type Author {
name: String
books: [Book] # A list of Books
}

List can be nested by using multiple pairs of square brackets [[]].

Field nullability

By default, it's valid for any in your schema to return null instead of its specified type. You can require that a particular doesn't return null with an exclamation mark !, like so:

type Author {
name: String! # Can't return null
books: [Book]
}

These are non-nullable. If your server attempts to return null for a non-nullable , an error is thrown.

Nullability and lists

With a list , an exclamation mark ! can appear in any combination of two locations:

type Author {
books: [Book!]! # This list can't be null AND its list *items* can't be null
}
  • If ! appears inside the square brackets, the returned list can't include items that are null.
  • If ! appears outside the square brackets, the list itself can't be null.
  • In any case, it's valid for a list to return an empty list.

Based on the above principles, the below return types can potentially return these sample values:

Return TypeExample Allowed Return Values
[Book][], null, [null], and [{title: "City of Glass"}]
[Book!][], null, and [{title: "City of Glass"}]
[Book]![], [null], and [{title: "City of Glass"}]
[Book!]![] and [{title: "City of Glass"}]

Supported types

Every type definition in a belongs to one of the following categories:

Each of these is described below.

Scalar types

types are similar to primitive types in your favorite programming language. They always resolve to concrete data.

's default types are:

  • Int: A signed 32‐bit integer
  • Float: A signed double-precision floating-point value
  • String: A UTF‐8 character sequence
  • Boolean: true or false
  • ID (serialized as a String): A unique identifier that's often used to refetch an object or as the key for a cache. Although it's serialized as a String, an ID is not intended to be human‐readable.

These primitive types cover the majority of use cases. For more specific use cases, you can create custom scalar types.

Object types

Most of the types you define in a are . An object type contains a collection of fields, each of which has its own type.

Two can include each other as , as is the case in our example schema from earlier:

type Book {
title: String
author: Author
}
type Author {
name: String
books: [Book]
}

The __typename field

Every in your schema automatically has a named __typename (you don't need to define it). The __typename returns the 's name as a String (e.g., Book or Author).

use an object's __typename for many purposes, such as to determine which type was returned by a that can return multiple types (i.e., a union or interface). relies on __typename when caching results, so it automatically includes __typename in every object of every .

Because __typename is always present, this is a valid for any :

query UniversalQuery {
__typename
}

The Query type

The Query type is a special that defines all of the top-level entry points for queries that clients execute against your server.

Each of the Query type defines the name and return type of a different entry point. The Query type for our example schema might resemble the following:

type Query {
books: [Book]
authors: [Author]
}

This Query type defines two : books and authors. Each returns a list of the corresponding type.

With a REST-based API, books and authors would probably be returned by different endpoints (e.g., /api/books and /api/authors). The flexibility of enables clients to both resources with a single request.

Structuring a query

When your clients build queries to execute against your , those queries match the shape of the you define in your schema.

Based on our example schema so far, a client could execute the following , which requests both a list of all book titles and a list of all author names:

query GetBooksAndAuthors {
books {
title
}
authors {
name
}
}

Our server would then respond to the with results that match the query's structure, like so:

{
"data": {
"books": [
{
"title": "City of Glass"
},
...
],
"authors": [
{
"name": "Paul Auster"
},
...
]
}
}

Although it might be useful in some cases to fetch these two separate lists, a client would probably prefer to fetch a single list of books, where each book's author is included in the result.

Because our schema's Book type has an author of type Author, a client could instead structure their like so:

query GetBooks {
books {
title
author {
name
}
}
}

And once again, our server would respond with results that match the 's structure:

{
"data": {
"books": [
{
"title": "City of Glass",
"author": {
"name": "Paul Auster"
}
},
...
]
}
}

The Mutation type

The Mutation type is similar in structure and purpose to the Query type. Whereas the Query type defines entry points for read , the Mutation type defines entry points for write .

Each of the Mutation type defines the signature and return type of a different entry point. The Mutation type for our example schema might resemble the following:

type Mutation {
addBook(title: String, author: String): Book
}

This Mutation type defines a single available , addBook. The accepts two (title and author) and returns a newly created Book object. As you'd expect, this Book object conforms to the structure that we defined in our schema.

Structuring a mutation

Like queries, match the structure of your schema's type definitions. The following mutation creates a new Book and requests certain of the created object as a return value:

mutation CreateBook {
addBook(title: "Fox in Socks", author: "Dr. Seuss") {
title
author {
name
}
}
}

As with queries, our server would respond to this with a result that matches the mutation's structure, like so:

{
"data": {
"addBook": {
"title": "Fox in Socks",
"author": {
"name": "Dr. Seuss"
}
}
}
}

A single can include multiple top-level of the Mutation type. This usually means that the will execute multiple back-end writes (at least one for each ). To prevent race conditions, top-level Mutation are resolved serially in the order they're listed (all other can be resolved in parallel).

Learn more about designing mutations

The Subscription type

See Subscriptions.

Input types

Input types are special that allow you to provide hierarchical data as arguments to (as opposed to providing only flat ).

An input type's definition is similar to an 's, but it uses the input keyword:

input BlogPostContent {
title: String
body: String
}

Each of an input type can be only a scalar, an enum, or another input type:

input BlogPostContent {
title: String
body: String
media: [MediaDetails!]
}
input MediaDetails {
format: MediaFormat!
url: String!
}
enum MediaFormat {
IMAGE
VIDEO
}

After you define an input type, any number of different object can accept that type as an :

type Mutation {
createBlogPost(content: BlogPostContent!): Post
updateBlogPost(id: ID!, content: BlogPostContent!): Post
}

Input types can sometimes be useful when multiple require the exact same set of information, but you should reuse them sparingly. Operations might eventually diverge in their sets of required .

Take care if using the same input type for fields of both Query and Mutation. In many cases, that are required for a are optional for a corresponding . You might want to create separate input types for each type.

Enum types

An enum is similar to a type, but its legal values are defined in the schema. Here's an example definition:

enum AllowedColor {
RED
GREEN
BLUE
}

Enums are most useful in situations where the user must pick from a prescribed list of options. As an additional benefit, enum values autocomplete in tools like the Apollo Studio Explorer.

An enum can appear anywhere a is valid (including as a ), because they serialize as strings:

type Query {
favoriteColor: AllowedColor # enum return value
avatar(borderColor: AllowedColor): String # enum argument
}

A might then look like this:

query GetAvatar {
avatar(borderColor: RED)
}

Internal values (advanced)

Sometimes, a backend forces a different value for an enum internally than in the public API. You can set each enum value's corresponding internal value in the resolver map you provide to .

This feature usually isn't required unless another library in your application expects enum values in a different form.

The following example uses color hex codes for each AllowedColor's internal value:

const resolvers = {
AllowedColor: {
RED: '#f00',
GREEN: '#0f0',
BLUE: '#00f',
},
// ...other resolver definitions...
};

These internal values don't change the public API at all. accept these values instead of the schema values, as shown:

const resolvers = {
AllowedColor: {
RED: '#f00',
GREEN: '#0f0',
BLUE: '#00f',
},
Query: {
favoriteColor: () => '#f00',
avatar: (parent, args) => {
// args.borderColor is '#f00', '#0f0', or '#00f'
},
},
};

Union and interface types

See Unions and interfaces.

Growing with a schema

As your organization grows and evolves, your grows and evolves with it. New products and features introduce new schema types and . To track these changes over time, you should maintain your schema's definition in version control.

Most additive changes to a schema are safe and backward compatible. However, changes that remove or alter existing behavior might be breaking changes for one or more of your existing clients. All of the following schema changes are potentially breaking changes:

  • Removing a type or
  • Renaming a type or
  • Adding nullability to a
  • Removing a 's s

A management tool such as Apollo Studio helps you understand whether a potential schema change will impact any of your active clients. Studio also provides -level performance metrics, schema history tracking, and advanced security via safelisting.

Descriptions (docstrings)

's () supports Markdown-enabled documentation strings, called descriptions. These help consumers of your discover and learn how to use them.

The following snippet shows how to use both single-line string literals and multi-line blocks:

"Description for the type"
type MyObjectType {
"""
Description for field
Supports **multi-line** description for your [API](http://example.com)!
"""
myField: String!
otherField(
"Description for argument"
arg: Int
)
}

A well schema helps provide an enhanced development experience, because development tools (such as the Apollo Studio Explorer) auto-complete names along with descriptions when they're provided. Furthermore, Apollo Studio displays descriptions alongside usage and performance details when using its metrics reporting and client awareness features.

Naming conventions

The specification is flexible and doesn't impose specific naming guidelines. However, it's helpful to establish a set of conventions to ensure consistency across your organization. We recommend the following:

  • Field names should use camelCase. Many are written in JavaScript, Java, Kotlin, or Swift, all of which recommend camelCase for names.
  • Type names should use PascalCase. This matches how classes are defined in the languages mentioned above.
  • Enum names should use PascalCase.
  • Enum values should use ALL_CAPS, because they are similar to constants.

These conventions help ensure that most clients don't need to define extra logic to transform the results returned by your server.

Query-driven schema design

A is most powerful when it's designed for the needs of the clients that will execute against it. Although you can structure your types so they match the structure of your back-end data stores, you don't have to! A single 's can be populated with data from any number of different sources. Design your schema based on how data is used, not based on how it's stored.

If your data store includes a or relationship that your clients don't need yet, omit it from your schema. It's easier and safer to add a new field to a schema than it is to remove an existing field that some of your clients are using.

Example of a query-driven schema

Let's say we're creating a web app that lists upcoming events in our area. We want the app to show the name, date, and location of each event, along with the weather forecast for it.

In this case, we want our web app to be able to execute a with a structure similar to the following:

query EventList {
upcomingEvents {
name
date
location {
name
weather {
temperature
description
}
}
}
}

Because we know this is the structure of data that would be helpful for our client, that can inform the structure of our schema:

type Query {
upcomingEvents: [Event!]!
}
type Event {
name: String!
date: String!
location: Location
}
type Location {
name: String!
weather: WeatherInfo
}
type WeatherInfo {
temperature: Float
description: String
}

As mentioned, each of these types can be populated with data from a different (or multiple ). For example, the Event type's name and date might be populated with data from our back-end database, whereas the WeatherInfo type might be populated with data from a third-party weather API.

Designing mutations

In , it's recommended for every 's response to include the data that the mutation modified. This enables clients to obtain the latest persisted data without needing to send a followup .

A schema that supports updating the email of a User would include the following:

type Mutation {
# This mutation takes id and email parameters and responds with a User
updateUserEmail(id: ID!, email: String!): User
}
type User {
id: ID!
name: String!
email: String!
}

A client could then execute a against the schema with the following structure:

mutation updateMyUser {
updateUserEmail(id: 1, email: "jane@example.com") {
id
name
email
}
}

After the executes the and stores the new email address for the user, it responds to the client with the following:

{
"data": {
"updateUserEmail": {
"id": "1",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
}

Although it isn't mandatory for a 's response to include the modified object, doing so greatly improves the efficiency of client code. And as with queries, determining which mutations would be useful for your clients helps inform the structure of your schema.

Structuring mutation responses

A single can modify multiple types, or multiple instances of the same type. For example, a that enables a user to "like" a blog post might increment the likes count for a Post and update the likedPosts list for the User. This makes it less obvious what the structure of the 's response should look like.

Additionally, are much more likely than queries to cause errors, because they modify data. A mutation might even result in a partial error, in which it successfully modifies one piece of data and fails to modify another. Regardless of the type of error, it's important that the error is communicated back to the client in a consistent way.

To help resolve both of these concerns, we recommend defining a MutationResponse interface in your schema, along with a collection of that implement that interface (one for each of your ).

Here's what a MutationResponse interface might look like:

interface MutationResponse {
code: String!
success: Boolean!
message: String!
}

And here's what an object that implements MutationResponse might look like:

type UpdateUserEmailMutationResponse implements MutationResponse {
code: String!
success: Boolean!
message: String!
user: User
}

Our updateUserEmail would specify UpdateUserEmailMutationResponse as its return type (instead of User), and the structure of its response would be the following:

{
"data": {
"updateUserEmail": {
"code": "200",
"success": true,
"message": "User email was successfully updated",
"user": {
"id": "1",
"name": "Jane Doe",
"email": "jane@example.com"
}
}
}
}

Let’s break this down by field:

  • code is a string that represents the status of the data transfer. Think of it like an HTTP status code.
  • success is a boolean that indicates whether the was successful. This allows a coarse check by the client to know if there were failures.
  • message is a human-readable string that describes the result of the . It is intended to be used in the UI of the product.
  • user is added by the implementing type UpdateUserEmailMutationResponse to return the newly updated user to the client.

If a modifies multiple types (like our earlier example of "liking" a blog post), its implementing type can include a separate for each type that's modified:

type LikePostMutationResponse implements MutationResponse {
code: String!
success: Boolean!
message: String!
post: Post
user: User
}

Because our hypothetical likePost modifies on both a Post and a User, its response object includes for both of those types. A response has the following structure:

{
"data": {
"likePost": {
"code": "200",
"success": true,
"message": "Thanks!",
"post": {
"id": "123",
"likes": 5040
},
"user": {
"likedPosts": ["123"]
}
}
}
}

Following this pattern provides a client with helpful, detailed information about the result of each requested . Equipped with this information, developers can better react to operation failures in their client code.

Previous
Previous versions
Next
Unions and interfaces
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company