GraphQL API style guide

This document outlines the style guide for the GitLab GraphQL API.

How GitLab implements GraphQL

We use the GraphQL Ruby gem written by Robert Mosolgo. In addition, we have a subscription to GraphQL Pro. For details see GraphQL Pro subscription.

All GraphQL queries are directed to a single endpoint (app/controllers/graphql_controller.rb#execute), which is exposed as an API endpoint at /api/graphql.

Deep Dive

In March 2019, Nick Thomas hosted a Deep Dive (GitLab team members only: https://gitlab.com/gitlab-org/create-stage/issues/1) on the GitLab GraphQL API to share domain-specific knowledge with anyone who may work in this part of the codebase in the future. You can find the recording on YouTube, and the slides on Google Slides and in PDF. Everything covered in this deep dive was accurate as of GitLab 11.9, and while specific details may have changed since then, it should still serve as a good introduction.

GraphiQL

GraphiQL is an interactive GraphQL API explorer where you can play around with existing queries. You can access it in any GitLab environment on https://<your-gitlab-site.com>/-/graphql-explorer. For example, the one for GitLab.com.

Authentication

Authentication happens through the GraphqlController, right now this uses the same authentication as the Rails application. So the session can be shared.

It’s also possible to add a private_token to the query string, or add a HTTP_PRIVATE_TOKEN header.

Limits

Several limits apply to the GraphQL API and some of these can be overridden by developers.

Max page size

By default, connections can only return at most a maximum number of records defined in app/graphql/gitlab_schema.rb per page.

Developers can specify a custom max page size when defining a connection.

Max complexity

Complexity is explained on our client-facing API page.

Fields default to adding 1 to a query’s complexity score, but developers can specify a custom complexity when defining a field.

The complexity score of a query can itself be queried for.

Request timeout

Requests time out at 30 seconds.

Breaking changes

The GitLab GraphQL API is versionless which means developers must familiarize themselves with our Deprecation and Removal process.

Breaking changes are:

  • Removing or renaming a field, argument, enum value, or mutation.
  • Changing the type of a field, argument or enum value.
  • Raising the complexity of a field or complexity multipliers in a resolver.
  • Changing a field from being not nullable (null: false) to nullable (null: true), as discussed in Nullable fields.
  • Changing an argument from being optional (required: false) to being required (required: true).
  • Changing the max page size of a connection.
  • Lowering the global limits for query complexity and depth.
  • Anything else that can result in queries hitting a limit that previously was allowed.

See the deprecating schema items section for how to deprecate items.

Breaking change exemptions

Two scenarios exist where schema items are exempt from the deprecation process, and can be removed or changed at any time without notice. These are schema items that either:

Global IDs

The GitLab GraphQL API uses Global IDs (i.e: "gid://gitlab/MyObject/123") and never database primary key IDs.

Global ID is a convention used for caching and fetching in client-side libraries.

See also:

We have a custom scalar type (Types::GlobalIDType) which should be used as the type of input and output arguments when the value is a GlobalID. The benefits of using this type instead of ID are:

  • it validates that the value is a GlobalID
  • it parses it into a GlobalID before passing it to user code
  • it can be parameterized on the type of the object (for example, GlobalIDType[Project]) which offers even better validation and security.

Consider using this type for all new arguments and result types. Remember that it is perfectly possible to parameterize this type with a concern or a supertype, if you want to accept a wider range of objects (such as GlobalIDType[Issuable] vs GlobalIDType[Issue]).

Types

We use a code-first schema, and we declare what type everything is in Ruby.

For example, app/graphql/types/issue_type.rb:

graphql_name 'Issue'

field :iid, GraphQL::Types::ID, null: true
field :title, GraphQL::Types::String, null: true

# we also have a method here that we've defined, that extends `field`
markdown_field :title_html, null: true
field :description, GraphQL::Types::String, null: true
markdown_field :description_html, null: true

We give each type a name (in this case Issue).

The iid, title and description are scalar GraphQL types. iid is a GraphQL::Types::ID, a special string type that signifies a unique ID. title and description are regular GraphQL::Types::String types.

Note that the old scalar types GraphQL:ID, GraphQL::INT_TYPE, GraphQL::STRING_TYPE, GraphQL:BOOLEAN_TYPE, and GraphQL::FLOAT_TYPE are no longer allowed. Please use GraphQL::Types::ID, GraphQL::Types::Int, GraphQL::Types::String, GraphQL::Types::Boolean, and GraphQL::Types::Float.

When exposing a model through the GraphQL API, we do so by creating a new type in app/graphql/types. You can also declare custom GraphQL data types for scalar data types (for example TimeType).

When exposing properties in a type, make sure to keep the logic inside the definition as minimal as possible. Instead, consider moving any logic into a presenter:

class Types::MergeRequestType < BaseObject
  present_using MergeRequestPresenter

  name 'MergeRequest'
end

An existing presenter could be used, but it is also possible to create a new presenter specifically for GraphQL.

The presenter is initialized using the object resolved by a field, and the context.

Nullable fields

GraphQL allows fields to be “nullable” or “non-nullable”. The former means that null may be returned instead of a value of the specified type. In general, you should prefer using nullable fields to non-nullable ones, for the following reasons:

  • It’s common for data to switch from required to not-required, and back again
  • Even when there is no prospect of a field becoming optional, it may not be available at query time
    • For instance, the content of a blob may need to be looked up from Gitaly
    • If the content is nullable, we can return a partial response, instead of failing the whole query
  • Changing from a non-nullable field to a nullable field is difficult with a versionless schema

Non-nullable fields should only be used when a field is required, very unlikely to become optional in the future, and very easy to calculate. An example would be id fields.

A non-nullable GraphQL schema field is an object type followed by the exclamation point (bang) !. Here’s an example from the gitlab_schema.graphql file:

  id: ProjectID!

Here’s an example of a non-nullable GraphQL array:


  errors: [String!]!

Further reading:

Exposing Global IDs

In keeping with the GitLab use of Global IDs, always convert database primary key IDs into Global IDs when you expose them.

All fields named id are converted automatically into the object’s Global ID.

Fields that are not named id need to be manually converted. We can do this using Gitlab::GlobalID.build, or by calling #to_global_id on an object that has mixed in the GlobalID::Identification module.

Using an example from Types::Notes::DiscussionType: