Post

GraphQL for JavaScript Beginners — Practical Examples

GraphQL sounds complicated. It isn’t. From JavaScript’s point of view, it’s just a fetch with a string describing what data you want. That’s it.

This article teaches you the four things you need to know to be productive with GraphQL in a Next.js or plain JavaScript project.


What GraphQL actually is

With a REST API you hit different URLs for different data:

1
2
3
GET /api/users/1
GET /api/users/1/posts
GET /api/users/1/posts/5/comments

With GraphQL there is one URL. You describe what you want in the request body and get back exactly that — nothing more, nothing less.

1
POST /graphql

That’s the whole concept.


1. Making a GraphQL request in JavaScript

A GraphQL request is a regular fetch call with:

  • Method: always POST
  • Header: Content-Type: application/json
  • Body: a JSON object with a query key containing your query string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const response = await fetch("https://example.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: `
      {
        user {
          name
          email
        }
      }
    `,
  }),
});

const result = await response.json();

console.log(result.data.user.name);

The server sends back JSON. Your data is always inside result.data.


2. Asking for fields

You list the fields you want inside curly braces. You only get back what you ask for.

Ask for name only:

1
2
3
4
5
{
  user {
    name
  }
}

Response:

1
2
3
4
5
6
7
{
  "data": {
    "user": {
      "name": "Alice"
    }
  }
}

Ask for name and email:

1
2
3
4
5
6
{
  user {
    name
    email
  }
}

Response:

1
2
3
4
5
6
7
8
{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com"
    }
  }
}

You control exactly what comes back. No wasted data over the wire.


3. Passing arguments

You can pass arguments to narrow down what you get — like a WHERE clause in SQL.

Get a specific user by ID:

1
2
3
4
5
6
{
  user(id: 1) {
    name
    email
  }
}

Get a specific page by path:

1
2
3
4
5
{
  route(path: "/about") {
    url
  }
}

Arguments go in round brackets after the field name. The API tells you what arguments each field accepts.


4. Nested data

GraphQL is built for connected data. You can request related fields in one query.

One query, three levels of data:

1
2
3
4
5
6
7
8
9
10
11
{
  user(id: 1) {
    name
    posts {
      title
      comments {
        text
      }
    }
  }
}

Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "data": {
    "user": {
      "name": "Alice",
      "posts": [
        {
          "title": "My first post",
          "comments": [
            { "text": "Great post!" }
          ]
        }
      ]
    }
  }
}

Compare that to REST where you’d need 3 separate API calls to get the same data.


5. Variables — keep queries reusable

You often need to run the same query with different values. Instead of building query strings dynamically (fragile, and a security risk), use variables.

Without variables — don’t do this:

1
2
3
4
5
// Fragile: building a string by hand
const id = 1;
body: JSON.stringify({
  query: `{ user(id: ${id}) { name } }`  // ← string interpolation in a query = bad
})

With variables — do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
const id = 1;

body: JSON.stringify({
  query: `
    query GetUser($id: Int!) {
      user(id: $id) {
        name
        email
      }
    }
  `,
  variables: { id: id }   // ← pass values here, separate from the query string
})

Breaking down the variable syntax:

1
2
3
4
5
6
7
8
9
query GetUser($id: Int!) {
#      ↑         ↑    ↑
#   query name  var  type (Int, non-nullable)
  user(id: $id) {
#         ↑
#    use the variable here
    name
  }
}
  • query GetUser — a name for the query (optional but good practice)
  • $id — the variable, always starts with $
  • Int! — the type. ! means required. Int without ! means optional.

Common types:

TypeMeaning
Int!Required integer
String!Required string
StringOptional string
BooleanTrue or false

The variables object in the request body maps to the $variable names:

1
2
3
4
variables: {
  id: 1,          // maps to $id
  path: "/about", // maps to $path
}

6. What __typename is

Every GraphQL type has a built-in field called __typename that returns the name of the type as a string. You didn’t define it — it’s always there.

1
2
3
4
5
6
{
  user {
    __typename
    name
  }
}

Response:

1
2
3
4
5
6
7
8
{
  "data": {
    "user": {
      "__typename": "User",
      "name": "Alice"
    }
  }
}

It’s useful when a field can return more than one type (called a union). You use __typename to find out which type actually came back.


7. Union types and inline fragments

Sometimes a field can return different types. For example, a route field might return a RouteInternal (a page on your site) or a RouteExternal (a redirect to another site).

The syntax for handling this is called an inline fragment... on TypeName { }.

Plain English: “if what comes back is a RouteInternal, give me these fields. If it’s a RouteExternal, give me those fields instead.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  route(path: "/about") {
    __typename              # always fetch this to know which type came back

    ... on RouteInternal {  # "if it's RouteInternal, I want these fields"
      url
      entity {
        title
      }
    }

    ... on RouteExternal {  # "if it's RouteExternal, I want these fields"
      url
    }
  }
}

In JavaScript, you then check __typename to handle each case:

1
2
3
4
5
6
7
const route = result.data.route;

if (route.__typename === "RouteInternal") {
  console.log(route.entity.title);
} else if (route.__typename === "RouteExternal") {
  redirect(route.url);
}

8. Handling the response

The response always has this shape:

1
2
3
4
{
  "data": { ... },     // your data  present when the query succeeded
  "errors": [ ... ]   // present when something went wrong
}

A real fetch wrapper that handles both:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function query(queryString, variables) {
  const response = await fetch("https://example.com/graphql", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: queryString, variables }),
  });

  const result = await response.json();

  // Errors come back with a 200 status — always check for them
  if (result.errors) {
    console.error(result.errors[0].message);
    return null;
  }

  return result.data;
}

// Use it:
const data = await query(`
  query GetUser($id: Int!) {
    user(id: $id) {
      name
      email
    }
  }
`, { id: 1 });

console.log(data.user.name);

Note: GraphQL almost always returns a 200 status even when there are errors. The errors are in the body, not the HTTP status. Always check result.errors.


9. Discovering what fields exist

You can query the schema itself to find out what fields and types are available. This is called introspection.

What queries can I run?

1
2
3
4
5
6
7
8
9
{
  __schema {
    queryType {
      fields {
        name
      }
    }
  }
}

What fields does the User type have?

1
2
3
4
5
6
7
8
9
10
{
  __type(name: "User") {
    fields {
      name
      type {
        name
      }
    }
  }
}

Most GraphQL APIs also provide a browser-based explorer (GraphiQL) where you can run queries interactively with autocomplete. If you’re using Drupal, it’s at /graphql/explorer.


Quick reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Basic query
{ user { name } }

# With argument
{ user(id: 1) { name } }

# Named query with variable
query GetUser($id: Int!) {
  user(id: $id) {
    name
  }
}

# Union type — handle multiple possible types
{
  search(term: "hello") {
    __typename
    ... on Article { title }
    ... on Page { heading }
  }
}
1
2
3
4
5
6
7
8
9
10
11
// Full fetch
const result = await fetch("/graphql", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: `query GetUser($id: Int!) { user(id: $id) { name } }`,
    variables: { id: 1 },
  }),
}).then(r => r.json());

const name = result.data?.user?.name;

Next steps: Build a 5-Page Headless Drupal 11 + Next.js Site with Paragraphs and GraphQL — puts all of this into practice.

This post is licensed under CC BY 4.0 by the author.