TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Writing queries

Fields, arguments, nested objects, aliases and operation names.

A query is a list of fields, written as a tree. You start at the top, pick an entry point the API offers, and keep picking fields until every branch ends in a plain value. This lesson walks through the parts of that tree using the books, products and countries on this API. The GraphQL reference lists every entry point if you want to try others.

Fields and selection sets

The curly braces after a field are called a selection set. They say which of that object's fields you want. Every field either holds a plain value (a string, a number, true or false) or an object. Plain values can't have a selection set. Objects must have one.

Both rules are enforced. Ask for a product's rating without saying which part of the rating you want, as in { product(id: 3) { title rating } }, and the server refuses the whole query:

Response (400, trimmed)
{
  "errors": [
    {
      "message": "Field \"rating\" of type \"Rating!\" must have a selection of subfields. Did you mean \"rating { ... }\"?",
      "locations": [{ "line": 1, "column": 26 }],
      "path": ["product"]
    }
  ]
}

The fix is to go one level deeper. There is no way to say "give me everything" in GraphQL. No *, no wildcard. You always list the fields, which is annoying when exploring and useful in production, because adding a field to the API can't make existing responses bigger.

Query
{
  product(id: 3) {
    title
    price
    rating {
      rate
      count
    }
  }
}
Response
{
  "data": {
    "product": {
      "title": "Mechanical Keyboard",
      "price": 89.99,
      "rating": {
        "rate": 4.6,
        "count": 214
      }
    }
  }
}

Asking for a selection on a plain value fails the other way round. title { length } gets Field "title" must not have a selection since type "String!" has no subfields.

Arguments

Arguments go in parentheses after a field name. book(id: 1) picks one book. books(genre: "programming") filters a list. Strings take double quotes; numbers and true/falsedon't. Single quotes aren't valid GraphQL at all.

Some arguments are required and some are optional, and the schema says which. book needs an id; leave it out and you get The argument `id` is required. The genre on books is optional. Without it you get all 25 books.

Arguments are where you'll find most bugs, so try the edges:

QueryWhat this API does
book(id: 999)Returns "book": null with no error.
book(id: -1)Also null. Negative IDs aren't rejected.
book(id: 99999999999)Rejected with a 400: the number doesn't fit in an Int, which is 32-bit.
book(id: "1")Rejected: The specified argument value does not match the argument type.
books(genre: "Programming")The same five books as lowercase. The filter ignores case.
products(category: "nothing")An empty list, [].
book(isbn: "9780132350884")Two errors: isbn doesn't exist, and id is missing.

None of these is clearly wrong, but each one is a decision. Whether genre should ignore case, or whether a negative ID deserves an error rather than null, is a question for whoever owns the API. Your job is to find out what it does, then check that the docs say so.

Nested objects and lists

A field can hold an object, a list of plain values, or a list of objects. A country on this API has all three:

Query
{
  country(code: "JP") {
    name
    capital
    population
    currency {
      code
      symbol
    }
    languages
    timezones
  }
}
Response
{
  "data": {
    "country": {
      "name": "Japan",
      "capital": "Tokyo",
      "population": 125700000,
      "currency": {
        "code": "JPY",
        "symbol": "¥"
      },
      "languages": ["Japanese"],
      "timezones": ["UTC+09:00"]
    }
  }
}

currency is an object, so it needs its own selection. languages and timezonesare lists of strings, so they don't. In the response a list always comes back as a JSON array, even when it has one item, like languages here. A test that expects "Japanese" as a plain string will fail, and should.

Asking for the same field twice

Keys in the response are named after the fields in the query. So what happens if you ask for two books in one request?

Query
{
  book(id: 1) { title }
  book(id: 2) { title }
}
Error message (400)
"Encountered fields for the same object that cannot be merged."

Both answers would need the key book, and a JSON object can't hold the same key twice. An alias gives each one its own name. Write the name you want, a colon, then the field:

Query
{
  first: book(id: 1) { title }
  second: book(id: 2) { title }
}
Response
{
  "data": {
    "first": { "title": "Clean Code" },
    "second": { "title": "The Pragmatic Programmer" }
  }
}

Try it:

POST/graphql
{"query":"{ first: book(id: 1) { title } second: book(id: 2) { title } }"}
This request uses an endpoint for signed-in members.Sign inCreate a free account

Aliases also rename fields when a client wants different keys from the schema's names, for example name: title. For a tester they mean something else as well: one HTTP request can run the same lookup hundreds of times. Lesson 8 comes back to that.

Operation names and comments

So far every query has started with a bare {. That's a shorthand. The full form starts with the word query and a name you choose:

Query
# Books for the team reading list
query ReadingList {
  books(genre: "programming") {
    title   # titles only, the page has no room for more
  }
}

The server ignores the name when answering, but it shows up in server logs, tracing tools and error reports. Since every GraphQL request hits the same URL, the operation name is often the only way to tell from a log which feature made the request. Ask the developers to name every operation in the app. When a test fails in CI, "ReadingList failed" is far easier to follow up than "POST /graphql failed".

Lines starting with #are comments. Commas between fields are optional, and spacing and line breaks don't matter, so { book(id: 1) { title author } } on one line is the same query as the indented version.

__typename

Every object has a field called __typenamethat you can always ask for, even though no schema lists it. It returns the name of the object's type:

Query
{
  country(code: "IN") {
    __typename
    name
    currency { code symbol }
  }
}
Response
{
  "data": {
    "country": {
      "__typename": "Country",
      "name": "India",
      "currency": { "code": "INR", "symbol": "₹" }
    }
  }
}

On this API that's rarely surprising. On APIs where one field can return several types (a search that returns books or authors, say), __typename is how a client and a test tell them apart.

Exercise

Write one query, named Compare, that fetches the title, rating and price of books 1, 2 and 3 using aliases. Then add the name and currency code of the country with code GB to the same query. Before you send it, write down the exact JSON you expect back, key by key. Then send it and compare.

Practise and go further