Skip to main content

How a GraphQL Formatter Turns a Minified Query Back Into Readable Code

A practical look at formatting GraphQL queries and schemas: consistent selection-set indentation, reading a minified query, and processing everything locally in your browser.

Published By Li Lei
#graphql #formatting #developer-tools #web

How a GraphQL Formatter Turns a Minified Query Back Into Readable Code

A GraphQL query has shape. The selection set inside user is one level deeper than user itself, and the selection set inside posts is one level deeper than that. When the indentation matches the nesting, you can read a 200-line query the way you read code: by glancing at the left edge. When the indentation is gone — collapsed to a single transport line, or hand-indented by four different people — you lose that, and the query becomes a wall of braces you have to parse by counting.

A GraphQL formatter restores the shape. Paste the document, and every nested selection set gets pushed one level right, every field lands on its own line, and the arguments line up so a deep query stays readable instead of sprawling. This post walks through what that actually does to a query, why the minified form is so hard to read, and why doing the whole thing in your browser matters for queries that touch real data.

Why a Minified Query Is Unreadable

GraphQL over the wire is a JSON envelope: {"query": "...", "variables": {...}}. The value of that query key is a single string. Newlines are optional in GraphQL syntax, so a client library is free to ship the entire operation as one line to save bytes and skip newline escaping. That is great for the network and terrible for a human.

Here is what that looks like when you fish a query out of a webhook log or a network tab:

query Feed($cursor:String,$limit:Int=20){viewer{id name posts(after:$cursor,first:$limit){edges{node{id title publishedAt author{id name}}}pageInfo{hasNextPage endCursor}}}}

Everything is technically there, but you can't tell at a glance how deep node sits, or which id belongs to the post and which belongs to the author. The braces carry all the structure and your eyes have to do the bracket-matching by hand.

What Formatting Actually Does

Run that line through the GraphQL Formatter with the default 2-space indent, and you get the structure back:

query Feed($cursor: String, $limit: Int = 20) {
  viewer {
    id
    name
    posts(after: $cursor, first: $limit) {
      edges {
        node {
          id
          title
          publishedAt
          author {
            id
            name
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
}

Look at the left edge now. The rule is simple and it is the whole point: each nested selection set indents exactly one level deeper than its parent, and fields within a set all line up at the same column. viewer sits at 2 spaces; its fields at 4; posts at 4 with its fields at 6; node at 8 with its fields at 10. Because the columns are consistent, the post's id (at column 10) and the author's id (at column 12) read as clearly different things — you can see the author block is one rung deeper without counting a single brace.

The arguments follow the same discipline. posts(after: $cursor, first: $limit) stays on the opening line because it is short; a longer argument list would wrap, one argument per line, aligned under the first. That keeps a query with heavy arguments from running off the right side of the screen while still being scannable. And the variable declarations in the header — $cursor: String, $limit: Int = 20 — stay where they belong, telling you exactly what the operation expects before you ever look at the body.

Schemas Get the Same Treatment

The same indentation logic applies to Schema Definition Language, which is its own flavor of nesting. A type, interface, input, or enum opens a block, and its fields sit one level in:

type Post implements Node {
  id: ID!
  title: String!
  author: User!
  comments(first: Int = 10): [Comment!]!
}

When you have hand-edited an SDL file — added an interface, pasted in three types from a teammate, fixed a field by hand — the indentation drifts. Formatting normalizes the whole file to one style so a stray two-space block doesn't hide next to a four-space one. It also means a printed schema pulled from a gateway, which usually arrives as one undifferentiated blob, becomes something you can actually drop into a wiki and read.

A Detail I Keep Reaching For

I spend a fair amount of time reading queries I didn't write — captured from a client I'm debugging, or pasted into a ticket by someone who grabbed it from their network tab. They almost always arrive minified, and the first thing I do is paste them into the formatter just to find out what the query is even asking for. The moment that earns its keep, though, is the variables panel under the output. A minified header like ($id:ID!,$includeDrafts:Boolean=false) is easy to skim past, but the panel spells out each one: $id is ID! and therefore required, $includeDrafts is a Boolean with a default of false. That is exactly the contract I need to build a matching variables object, and I no longer have to re-read the header squinting for the difference between ID and ID!. It has saved me from more than one "why is the server rejecting my request body" rabbit hole.

Reading, Diffing, and Stripping Back Down

Once a query is formatted consistently, two more things get easier.

Diffing stops lying to you. If two queries are supposedly identical but a snapshot test shows sixty changed lines, the difference is usually field order, not meaning. Format both with sort-fields and sort-arguments turned on, and the reordering noise collapses — the one real change, a missing id somewhere, finally stands out instead of drowning. Sorting deliberately leaves fragment spreads and inline fragments in place, because moving those would change the query's shape rather than just tidy it.

And when you need the compact form back — to POST a query raw with curl for a one-off job — minify mode collapses the formatted document to a single line. Comments get dropped here on purpose: a GraphQL # comment runs to end-of-line, so once the newlines are gone there is no way to terminate it, and keeping it would swallow every field after it on the line. Strip first, then paste straight into an HTTP body with no newline-escape gymnastics.

It All Stays in Your Tab

The reason any of this is safe to do with real queries is that none of it leaves the browser. There is no GraphQL endpoint in the loop and no graphql-js bundle phoning home — the parser is a small hand-written recursive-descent routine that runs in the page. Open DevTools, watch the Network panel while you format, and you'll see zero outbound requests carrying your document. Only your option choices (indent, sort flags, mode) ever touch the URL; the query body never does, because production operations reference internal field names you wouldn't want sitting in a shared link.

One thing the formatter is not: a schema validator. It checks GraphQL syntax — it will pin an unexpected } to an exact line and column — but it won't tell you a field doesn't exist on your server's type. For that you still want codegen or an introspection client. And if what you actually have is the JSON wire envelope, pull the query string out of it first; a JSON Formatter makes that value easy to find before you bring the raw GraphQL over here.

Formatting isn't glamorous, but a query you can read at a glance is a query you can reason about. The indentation does the work your eyes would otherwise have to.


Made by Toolora · Updated 2026-06-13