GraphQL

How to use GraphQL API for your project on Sanity.io

Sanity.io has powerful APIs for querying, patching, and mutating data in the real-time content backend. In addition to our GROQ API, we also support deploying GraphQL APIs to query your content.

GraphQL APIs are deployed using our command line tool. The tool inspects your studios schema definitions and generates a GraphQL schema that closely resembles it (type names have their first letter capitalized, eg bookAuthor becomes BookAuthor), then adds queries that allow you to find and filter for the documents stored in your Sanity dataset. To use GraphQL with your project, you need to make sure that you follow the strict schemas conventions.

Migrating from the beta version

Deploying generation 1

In order to use the old beta version of the GraphQL API, you can specify that when running the GraphQL command from the CLI. We call these generations. The beta generation is named gen1, and the current generation is called gen2.

To deploy the old generation (beta), you can use the flag --generation in the CLI like this:

sanity graphql deploy --generation gen1

Breaking changes betwen gen1 and gen2

The second generation of the GraphQL schemas has more features, and does not pluralize the top-level collection types:

  • Generation 1: post, allPosts
  • Generation 2: post, allPost

The CLI will warn you if there are breaking changes to the schema you deploy and show you where they are. The breaking changes prompt can be disabled by running the deploy command with the flag --force.

Queries

For each document type in your Sanity schema, two top-level query fields are added:

  • all<TypeName> - used to fetch all documents of the given type. You can add additional filters, sorting, limits, and offsets. Read more about filters below.
  • <TypeName> - used to fetch a specific document of the given type by specifying its document ID.

Filters

For each object and document type in your Sanity schema, an equivalent filter type is generated. This can be used to constrain which documents are returned for a given query, much like an SQL query.

Most fields in your schema type will have a corresponding field in the filter. For instance, a book schema type may have a title field, which would then have a title filter:

Query

{
  allBook(where: {title: {eq: "A Game of Thrones"}}) {
    title
    author {
      name
    }
  }
}

Result

{
  "allBook": [
    {
      "title": "A Game of Thrones",
      "author": {
        "name": "George. R. R. Martin"
      }
    }
  ]
}

In a similar fashion, the author field would also have a filter type:

Query

{
  allBook(where: {author: {name: {eq: "George R.R. Martin"}}}) {
    title
    author {
      name
    }
  }
}

Response

{
  "allBook": [
    {
      "title": "A Game of Thrones",
      "author": {
        "name": "George R. R. Martin"
      }
    },
    {
      "title": "A Storm of Swords",
      "author": {
        "name": "George R. R. Martin"
      }
    }
  ]
}

Which comparator functions exist depend on the field type. For instance, a number field will have the comparators eq, neq, gt, gte, lt and lte, while a boolean field will only have eq and neq.

In addition to filtering on a per-field basis, document types have additional filters available under the _ field: references and is_draft:

Query

{
  allBook(where: {_: {references: "jrr-tolkien"}}) {
    title
    author {
      name
    }
  }
}

Response

{
  "allBook": [
    {
      "title": "The Lord of the Rings",
      "author": {
        "name": "J. R. R. Tolkien"
      }
    }
  ]
}

For a full overview of the available filters, see the GraphQL filter reference section.

Sorting

You can sort on multiple fields on your top-level documents. You can also sort on your nested objects.

Query

{
  allBook(sort: [ { title: ASC }, { published: DESC } ]) {
    title
  }
}

Result

{
  "allBook": [
    {
      "title": "A Game of Thrones",
      "author": {
        "name": "George. R. R. Martin"
      }
    },
    {
      "title": "The Fellowship of the Ring",
      "author": {
        "name": "J. R. R. Tolkien"
      }
    }
  ]
}

Pagination

We support pagination in the form of the take and skip concept. Pagination can easily be achieved like this:

Query

{
  allBook(limit: 10, offset: 10) {
    title
  }
}

Result

{
  "allBook": [
    {
      "title": "The Two Towers",
      "author": {
        "name": "J. R. R. Tolkien"
      }
    },
    {
      "title": "The Return of the King",
      "author": {
        "name": "J. R. R. Tolkien"
      }
    }
  ]
}

Strict schemas

The schemas for Sanity Studio are a bit more flexible than what GraphQL is able to represent. That means that we can't promise that you'll be able to deploy a GraphQL API without any changes to your Sanity projects. Therefore, you may have to do a few changes (usually these are backward-compatible and does not require any data migration).

You may find that “anonymous“ object types have to be given a name and declared in the top-level scope. Take this example:

// schemas/blogPost.js
export default {
  name: 'blogPost',
  title: 'Blog post',
  type: 'document',
  fields: [
    // ... other fields ...
    {
      name: 'sponsor',
      title: 'Sponsor',
      type: 'object',
      fields: [
        {
          name: 'name',
          title: 'Name',
          type: 'string'
        },
        {
          name: 'url',
          title: 'URL',
          type: 'url'
        }
      ]
    }
  ]
}

In the code above, the sponsor field is an object type declared inline. This means it cannot be used outside of the blogPost type. This is not compatible with GraphQL – all object types have to be defined in a global scope. To fix this, you should move the sponsor declaration to a separate file and import it into your schema explicitly, then have the sponsor field refer to it by name.

Example:

// schemas/blogPost.js
export default {
  name: 'blogPost',
  title: 'Blog post',
  type: 'document',
  fields: [
    // ... other fields ...
    {
      name: 'sponsor',
      title: 'Sponsor',
      type: 'sponsor'
    }
  ]
}

// schemas/sponsor.js
export default {
  name: 'sponsor',
  title: 'Sponsor',
  type: 'object',
  fields: [
    {
      name: 'name',
      title: 'Name',
      type: 'string'
    },
    {
      name: 'url',
      title: 'URL',
      type: 'url'
    }
  ]
}

Protip

While "lifting"/"hoisting" the type to the top-level scope, it can be helpful to consider whether the type should be altered to make it more reusable in other contexts. If you think the type is only relevant to the specific schema type, consider prefixing it to make it clearer (eg. blogPostSponsor in the above case).

Deploying the GraphQL API

Run sanity graphql deploy from your Sanity Studio project folder. This will deploy an API for the dataset configured in sanity.json. If you want to deploy to a different dataset, use the --dataset flag, eg: sanity graphql deploy --dataset staging.

Gotcha

Keep in mind that changing the schema in your local Sanity studio does not automatically change the GraphQL API – you'll have to run sanity graphql deploy to make the API reflect the changes.

Tagged endpoints

We also support deploying multiple endpoints of the GraphQL schema to the same dataset by using the --tag flag, eg --tag beta. This tag will figure as the last segment in the endpoint URL. This will let you test schema changes without breaking existing applications. If you don't specify any tag, the tag will be default.

Since we provide a way to deploy multiple GraphQL endpoints, there exist a CLI command which lists all your existing endpoints. You can do this by running the following command:

sanity graphql list

The playground

When deplying, you will be asked if you want to enable a GraphQL playground. By answering yes, an interactive GraphQL user interface will be deployed to the API endpoint, allowing you to more easily test queries.

If you are deploying the GraphQL API as part of a continous integration process or similar, you can use the --playground and --no-playground flags to enable/disable it.

GraphQL endpoints

There are two endpoint URLs you can run GraphQL queries against. The first is against the API where all your content changes will be available immediately. The complexity of the query will of course add to the response time:

https://<yourProjectId>.api.sanity.io/v1/graphql/<dataset>/<tag>

Caching / CDN

In most cases you want to run GraphQL queries against the CDN endpoint. It will typically result in faster queries (especially on high-volume sites, but it can take 15 to 30 seconds before changes are visible:

https://<yourProjectId>.apicdn.sanity.io/v1/graphql/<dataset>/<tag>

Gotcha

Even though the generation of the GraphQL API is gen2, the URL will still be at v1. This is because the contract between the API didn't change, but the generated GraphQL types and names changed.

Security

The GraphQL API generally has the same rules as the GROQ API - dataset visibility is respected. Authenticated users see only the documents they have access to.

However; keep in mind that the schema of your GraphQL is public, so all types and fields will be introspectable by anonymous users.

Mutations

Mutations are not exposed through the GraphQL API, but rather through our powerful mutation API.

Filters reference

Scalars

ID, String, Datetime, Date

  • Equals: field { eq: "" }
  • Not equals: field { ne: "" }
  • In: field { in: [ "apple", "banana", "pineapple" ] }
  • Not in: field { nin: [ "apple", "banana", "pineapple" ] }
  • Matches: field { matches: "" }

Int

  • Equals: field { eq: "" }
  • Not equals: field { ne: "" }
  • Greater than: field { gt: 42 }
  • Greater than or equal: field { gte: 42 }
  • Lesser than: field { lt: 42 }
  • Lesser than or equal: field { lte: 42 }

Float

  • Equals: field { eq: 42.0 }
  • Not equals: field { ne: 42.0 }
  • Greater than: field { gt: 42.0 }
  • Greater than or equal: field { gte: 42.0 }
  • Lesser than: field { lt: 42.0 }
  • Lesser than or equal: field { lte: 42.0 }

Boolean

  • Equals: field { eq: true|false }
  • Not equals: field { ne: true|false }

Types

The schema generator will generate filtering types for your documents. It will provide filtering options for most of the fields defined in your schema. On top-level documents, it provides some special filters which can be accessed through _.

Document

  • References: field { references: "jrr-tolkien" }
  • Is draft: field { is_draft: true }

Array

We unfortunately don't provide any filtering for your array fields, yet.

Portable Text

The schema generator will expose a <your-type-name>Raw field, which gives you all the Portable Text contents in raw JSON. It will not resolve references by default, but there are arguments which you can pass to resolve references.

Query

{
  allBook(where: {title: {eq: "A Game of Thrones"}}) {
    title
    rawBody
  }
}

Response

{
  "title": "Game of Thrones",
  "rawBody": [{
      "_type": "block",
      "_key": "da5f884c9804",
      "style": "normal",
      "children": [{
          "_type": "span",
          "_key": "da5f884c98040",
          "text": "Game of Thrones is a book series written by ",
          "marks": []
        },
        {
          "_type": "span",
          "_key": "da5f884c98041",
          "text": "George R. R. Martin",
          "marks": [
            "strong",
            "<markDefId>"
          ]
        },
        {
          "_type": "span",
          "_key": "da5f884c98042",
          "text": ".",
          "marks": []
        }
      ],
      "markDefs": [{
        "_type": "link",
        "_key": "<markDefId>",
        "href": "https://en.wikipedia.org/wiki/George_R._R._Martin"
      }]
    },
    {
      "_key": "26a6985c2917",
      "_ref": "b7eea53a-26b9-4b29-82c3-5b688198c393",
      "_type": "reference"
    }
  ]
}

In the example above, the body field is of type array of block and author. Since we aren't telling the API to specifically resolve references, we won't get any output on the author type nested in the array. To tell the API to resolve these kind of references, you will have tell it how deep it should resolve references. Here's an example of how to do that:

Query

{
  allBook(where: {title: {eq: "A Game of Thrones"}}) {
    title
    rawBody(resolveReferences: { maxDepth: 5 })
  }
}

Response

{
    "title": "Game of Thrones",
    "rawBody": [{
        "_type": "block",
        "_key": "da5f884c9804",
        "style": "normal",
        "children": [{
            "_type": "span",
            "_key": "da5f884c98040",
            "text": "Game of Thrones is a book series written by ",
            "marks": []
          },
          {
            "_type": "span",
            "_key": "da5f884c98041",
            "text": "George R. R. Martin",
            "marks": [
              "strong",
              "<markDefId>"
            ]
          },
          {
            "_type": "span",
            "_key": "da5f884c98042",
            "text": ".",
            "marks": []
          }
        ],
        "markDefs": [{
          "_type": "link",
          "_key": "<markDefId>",
          "href": "https://en.wikipedia.org/wiki/George_R._R._Martin"
        }]
      },
      {
        "_id": "b7eea53a-26b9-4b29-82c3-5b688198c393",
        "_type": "author",
        "name": "George R. R. Martin",
        "bio": "George Raymond Richard Martin (born George Raymond Martin; September 20, 1948), also known as GRRM, is an American novelist and short story writer in the fantasy, horror, and science fiction genres, screenwriter, and television producer."
      }
    ]
  }
  

Gotcha

Since Portable Text by nature is somewhat loosely typed, the generation doesn't take into account all the types you provide for it, yet.

Schema generation issues

Since the schema is generated in node.js instead of in a browser environment, certain imported modules might cause issues. Things that reference the window in a global context is a prime example. If you encounter issues, we'd be interested in hearing which modules cause problems to see if we can work around them.

Was this article helpful?