Using GraphQL APIs

Documentation on specific APIs

Totara uses GraphQL as our main API technology. We use GraphQL to implement several distinct API endpoints, each with its own specific functionality and behaviour, such as authentication mechanisms. See Available APIs for a summary of the different APIs and their uses. For more specific information on a particular GraphQL endpoint, see the documentation linked below:

The rest of this page contains information that applies to GraphQL in general and impacts all GraphQL APIs.

GraphQL overview

Background

GraphQL is a query language and server-side runtime for APIs, originally developed by Facebook. In GraphQL, the API is defined via a strictly typed schema, giving the client the ability to specify the data they need in their request.

Differences from REST

Some of the key differences between REST and GraphQL include:

  • Single vs multiple endpoints: In GraphQL all requests to a specific API typically go via a single endpoint (URL) with the body of the request determining the data being requested, whereas in REST the URL path and parameters will typically vary depending on the data being requested.
  • Request syntax: In REST the nature of the response is typically more static - the fields returned are controlled by the server so the client can receive more data than required (overfetching). Additionally, it can be hard to obtain data across objects within a single request, requiring multiple independent requests (underfetching).
  • Status codes: REST makes use of HTTP status codes to indicate different response types (such as successful request vs server error vs authentication issue). In GraphQL it is more common for all responses to be delivered with a 200 status code, with the contents of the response varying depending on how the request was handled by the server.
  • Introspection: GraphQL has native support for introspection, meaning it is possible to query the API for information about the structure of the API. GraphQL's typed schema makes it possible for GraphQL clients to tightly integrate with the APIs structure (providing features such as auto-completion and inline documentation).

Learning more about GraphQL

The official Introduction to GraphQL is a good place to start for an introduction to the technology. For detailed information, see the GraphQL specification.

GraphQL clients

In order to interact with GraphQL, you will likely choose to use a GraphQL client rather than making direct HTTP requests. The steps to configure a client depend on the authentication mechanism of the endpoint being used. See Making external requests via a GraphQL client for the external API and Making developer requests via a GraphQL client for the developer API.

Request structure

HTTP request structure

Unlike REST API, GraphQL requests are all sent to the same endpoint for a specific API. For a list of APIs and their endpoint URLs, see the Available APIs page.

Requests should use the HTTP POST method and set the Content-type header to application/json. We recommend using UTF-8 encoding on your database and within requests.

Some other headers are required for authentication, but the format and content is dependent on the endpoint type being accessed. View the documentation for the specific endpoint type for details.

Persisted vs custom queries

GraphQL has two query formats that can be used in a request: persisted queries and custom queries.

Persisted queries are named, predefined queries that exist in .graphql files in the server codebase. When using persisted queries, the client only needs to specify the operationName instead of the full query string. The server looks up the operationName and loads the query, combines it with any user-specified variables, and executes it.

Persisted queries can reduce the amount of bandwidth sent from the client to the server, and can improve security by locking down the queries that can be sent to the server. On the other hand they are less flexible since they must be defined in advance.

We support both types in our GraphQL implementation, but use them for different GraphQL endpoint types:

API endpoint typeSupported query types
External APICustom only
Dev APICustom only
Ajax APIPersisted only
Mobile APIPersisted only

Custom query structure

The request body should contain a JSON payload, consisting of a query parameter containing a string in GraphQL query language, and a variables parameter containing a JSON object of variables from the client:

{
  "query": "query { totara_webapi_status { status } }",
  "variables": "{}"
}

In a simple GraphQL request, the query string might look like this:

query my_query {
  totara_webapi_status {
    status
  }
}

Persisted (or named) query structure

Persisted queries still use a JSON payload, but instead of specifying the GraphQL query as a string in the GraphQL language they specify an operationName, which references a known persisted query that the server can locate.

Typically the body of a persisted query would look like this:

{
  "operationName": "totara_webapi_status",
  "variables": "{}"
}

See Locations for files relating to services in Developing GraphQL APIs for Totara and Implementing GraphQL services for more details about how the persisted query is located on the server.

GraphQL request structure

Both persisted and custom queries follow the same structure within the GraphQL portion of the request, and the only difference is how the GraphQL is sourced. The GraphQL query structure is as follows:

The 'query' keyword

This indicates this is a read-only request. 'mutation' is used for requests which write data. The keyword is optional, but when provided, the specific actions within the request must match the keyword given.

Request name (my_query)

Optional string to allow the request author to describe the purpose of the request. It is not used by the server.

Query or mutation name (totara_webapi_status)

Within the outer curly braces you must specify at least one query or mutation to execute (typically just one but requesting multiple is allowed).

The available queries and mutations are determined by the schema for the endpoint type being requested. See the API reference documentation or in Totara via Quick access menu > Development > API > API Documentation for a full list of available queries and mutations, their arguments, and return values.

Query structure

In GraphQL it is up to the query author to specify the data they want to receive in the response.

The curly braces and field names within them specify the data structure. They must match the structure of the type that is returned by that query or mutation.

Types can be complex (containing properties that represent other types), so additional curly braces are used to obtain properties of subresources in one request. For example, to get a list of course names and the name of each course's category:

query get_courses {
  get_courses {
    fullname
    category {
      name
    }
  }
}

Arguments and variables

Some queries and mutations have optional or required arguments which require additional data to be passed with the request.

User-provided data is kept separate from the body of the request as variables. Variables are passed as a separate JSON object with the query. To make use of variables, the structure of the query changes slightly:

Query

query get_course($courseid: core_id!) {
  core_course(courseid: $courseid) {
    fullname
  }
}

Variables

{
  "courseid": 2
}

In this example the core_course query has an argument, courseid. It must be of the type core_id, and it is required (the ! suffix is used when a field must be provided). Available arguments are listed in the reference documentation for a query or mutation. Although values can also be hardcoded in the query, it is good practice to use variables to support argument validation and query reuse.

Variables are represented by strings starting with the dollar sign ($). Any $variable specified within the body of the request must be defined in the round brackets after the outer query name. When defining a variable you must specify its type. Variables will be validated according to their type before query execution. 

The variables object that is passed with the request must have keys that match the variable names and give values that are compatible with the specified type for that variable.

Query arguments can make use of GraphQL types, so each argument may itself be a complex type (types with properties made of other types). This is particularly common for mutations (where you might be passing an object to be created). For example with this mutation:

mutation mod_perform_add_section($input: mod_perform_add_section_input!) { ... }

The schema definition of mod_perform_add_section_input is:

"""
Input type for adding a new section to an activity
"""
input mod_perform_add_section_input {
  """
  ID of the activity where the section is being added
  """
  activity_id: core_id!
  """
  Sort order to add this section before, if 0 or null given section will be added at the end
  """
  add_before: Int
}

So the variables might look like:

{
  "input": {
    "activity_id": 3,
    "add_before": 1
  }
}

Field arguments

Like queries and mutations, fields can support arguments. For example, here the argument on the timestamp field determines how the field is formatted in the response:

query test {
 totara_webapi_status {
   status
   timestamp(format: DATETIMELONG)
  }
}

Aliases

You can prefix a query, mutation or field with a different name and the server will return it as the key you specify. This can be used to return data to match a certain structure, or to differentiate if you are requesting the same field multiple times. For example:

query test {
  my_query_name: totara_webapi_status {
    status
    long_year: timestamp(format: DATETIMELONG)
    short_year: timestamp(format: DATETIMESHORT)
  }
}

would return:

{
  "data": {
    "my_query_name": {
      "status": "ok",
      "long_year": "27/05/2022, 10:51",
      "short_year": "27/05/22, 10:51"
    }
  }
} 

For more information on this, see the aliases documentation in Introduction to GraphQL.

Response structure

Content type and status codes

Responses are returned as JSON (Content-Type: application/json). The request will typically return one of the following status codes:

  • 200: For successful requests (not necessarily returning any data, but the request was valid and could be processed) AND for requests that are denied for some reason - this could be due to the request being invalid, lack of authentication, or lack of access to the resource. Typically this will return a message giving the reason for the failure. Note that we do not return 400 codes for errors, but we typically pass an errors array property with additional information.
  • 429 Too many requests: Used by rate-limiting middleware in the external API to indicate when requests are being rate limited.
  • 500: Returned when there is some kind of internal server error that prevented a valid response from being generated. 500 errors indicate something wrong with the server or in the code.

Response format

The response body for a successful request will look something like this:

{
  "data": {
    "core_course": {
      "id": "2",
      "fullname": "Course 1",
      "category": {
        "id": "1",
        "name": "Miscellaneous"
      }
    }
  }
}

The data property contains the response data for the specific query requested. Within the data property individual persisted queries are keyed by the name used when requesting the data. This allows you to make multiple requests in one query (see the Batched queries section below).

If the request is not successful then the response will contain an errors property with information about what went wrong. The exact content will depend on debugging settings, but will look something like this:

{
  "error": "this is an error\nthis is another error"
  "errors": [
    {
      "message": "this is an error",
      "extensions": {
        "category": "example category"
      },
      "locations": [
        {
          "line": 4,
          "column": 7
        }
      ],
      "path": [
        "core_user_users",
        "items",
        0,
        "id"
      ],
      "trace": [
        {
          "file": "/var/www/totara/src/main/server/totara/webapi/classes/default_resolver.php",
          "line": 91,
          "call": "core\\webapi\\resolver\\type\\user::resolve('id', instance of core\\entity\\user, array(1), instance of core\\webapi\\execution_context)"
        },
        ...
      ]
    }
  ],
  ...
}

Note that one request can potentially contain multiple errors (so the errors property is an array). We also pass an error property, which is a string of all the error messages concatenated together with the newline character (\n) in case you cannot handle multiple errors.

The response may also include an extensions field at the top level with additional information about the request. See Enabling debugging in GraphQL APIs for details. 

Batched queries

Typically, each request makes a single GraphQL request (query or mutation), which returns a single response. However, it is possible to make multiple GraphQL requests within the same request.

Within a request

When making a request, there is nothing preventing you from requesting multiple queries within the API at the same time. Typically you might do this in development when you want to see a bunch of different data together. It can also be done within a persisted query.

In the response, each query will be keyed by the name of that query, e.g. the request:

query example_composite_query($courseid: core_id!) {
    core_course(courseid: $courseid) {
        id
        fullname
    }
    totara_core_my_current_learning {
        id
        fullname
    }
}

# variables:
{"courseid": 2}

would return a response like:

{
  "data": {
    "core_course": {
      "id": "2",
      "fullname": "Course 1"
    },
    "totara_core_my_current_learning":
      {
        "id": "2",
        "fullname": "Certification program fullname 101"
      },
      {
        "id": "4",
        "fullname": "Course 3"
      },
      {
        "id": "1",
        "fullname": "Program fullname 101"
      }
    ]
  }
}

Note in this case the variables are only provided once, so you cannot have name collisions between the queries. You can request the same query multiple times in one request, but you will need to use aliases to differentiate the responses.

You can also put multiple mutations in a request, but you cannot mix queries and mutations in the same request.

Via JavaScript

We also offer a way to batch queries within Vue.js via Apollo, which does allow you to provide separate variables for each query. This collates the requests in memory, then sends them off together as a single AJAX request and collates the responses and returns them to the individual requests.

Common GraphQL patterns

See this page for information about Common GraphQL patterns.