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
querykey 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.Intwithout!means optional.
Common types:
| Type | Meaning |
|---|---|
Int! | Required integer |
String! | Required string |
String | Optional string |
Boolean | True 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.