📐 GraphQL Formatter & Beautifier
Make your ugly GraphQL queries and schemas beautiful again. Easily format and pretty print your code instantly in your browser.
What Formatting A Query Actually Changes
Lead: the formatter reindents every selection set to two spaces per nesting level, puts one field on each line, and normalizes the spacing around arguments, aliases and directives. It does not touch what the query asks for.
query{me:user(id:1){name email posts(limit:5){title comments{body author{name}}}}}query {
me: user(id: 1) {
name
email
posts(limit: 5) {
title
comments {
body
author {
name
}
}
}
}
}Paste that one-line query above into the tool and click Format Code to see the same result. Every opening brace gets its own indentation level, every field that was crammed onto one line gets its own line, and the alias in front of user is kept exactly where it was, just with normalized spacing around the colon. Nested selection sets, the field inside a field inside a field, each add two more spaces of indentation than their parent, which is how comments ends up one level deeper than posts and author one level deeper still.
Under the hood this runs on Prettier's GraphQL parser, loaded from a public CDN the first time you click Format Code. Prettier reads your text into a syntax tree, meaning it understands the difference between a field, an argument, a directive and a fragment spread, and then prints that tree back out using a fixed set of rules rather than pattern-matching on your original whitespace. That is why the result looks the same whether you pasted a single 400-character line or something already half-indented with a mix of tabs and spaces: the source formatting is discarded entirely and rebuilt from the parsed structure.
| What you wrote | What comes out |
|---|---|
| A field crammed next to its siblings | Its own line, inside the same selection set, in the order you wrote it |
An argument like id:1 | id: 1, one space after the colon, none before it |
An alias like me:user(...) | me: user(...), kept in front of the field it renames |
A directive like @include(if:$x) | @include(if: $x), printed immediately after whatever it modifies |
A nested { } selection set | Its own indentation level, exactly two spaces deeper than its parent field |
What does not change is just as important. The formatter never reorders fields, never removes a field it thinks is redundant, and never renames anything. If you wrote name before email, the output has name before email. If you aliased a field twice with two different names to fetch it under two different keys, both aliases survive untouched. Reformatting is purely a print step over the parsed structure. It cannot rewrite the query's meaning because it never modifies the tree it parsed, only the text used to render that tree back onto the page.
How Indentation And Line Wrapping Get Decided
Lead: every nested selection set adds exactly two spaces, regardless of how the source was indented, and an argument list only breaks onto separate lines once the whole line would run past roughly 80 characters.
query { search(firstArgument: "some long value here", secondArgument: "another long value", thirdArgument: true) { id } }query {
search(
firstArgument: "some long value here"
secondArgument: "another long value"
thirdArgument: true
) {
id
}
}A short argument list stays exactly where it is: user(id: 1) prints on one line because the whole field fits comfortably under the width limit. The three-argument search call above does not fit, so the formatter drops each argument to its own line, one indentation level deeper than the field itself, and lines the closing parenthesis back up with search. There is no in-between state. A field's arguments are either all inline or all stacked, never a mix of some arguments on the field's line and others below it.
The indentation decision ignores your source completely. A query pasted with tabs, one pasted with four-space indentation, and one pasted as a single unbroken line all produce identical output once formatted, because the parser strips whitespace out during parsing and the printer rebuilds it from scratch using its own two-space rule. There is nothing to configure and nothing you can do in the input box to change the indent width; it is fixed at two spaces per level for every construct the formatter handles.
| Situation | Rule applied |
|---|---|
| Source uses tabs, four spaces, or no indentation at all | Ignored. Output is always two spaces per nesting level |
| An argument list fits on the field's own line | Stays inline, e.g. posts(limit: 5) |
| An argument list would push the line past the width limit | Breaks to one argument per line, indented under the field, no trailing comma |
An object value like {name:"bob",active:true} | Spaces added inside the braces: { name: "bob", active: true } |
A list value like ["a","b","c"] | Spaces added after each comma: ["a", "b", "c"] |
| Two or more blank lines left between top-level definitions | Collapsed down to exactly one blank line |
You never need to line anything up by hand before pasting. Since the parser throws away your original whitespace and rebuilds indentation from the structure it detected, spending time manually aligning braces in your source before formatting buys you nothing. Paste it exactly as it sits in your terminal, your browser's network tab, or a chat message, and let the formatter do the alignment.
Aliases, Directives, Fragments And Variables Reformat, They Do Not Get Rewritten
Lead: aliases, directives, fragment spreads, inline fragments and operation variables all keep their exact names and structure. Formatting only changes their spacing and indentation, never what they point to or how they are wired together.
fragment PostFields on Post { title publishedAt }
query GetFeed($includeDrafts: Boolean!) { feed: posts(status: "published") { ...PostFields drafts: posts(status: "draft") @include(if: $includeDrafts) { title } } }fragment PostFields on Post {
title
publishedAt
}
query GetFeed($includeDrafts: Boolean!) {
feed: posts(status: "published") {
...PostFields
drafts: posts(status: "draft") @include(if: $includeDrafts) {
title
}
}
}A fragment definition is printed the same way an operation is: one field per line inside its own selection set, at the top level of the document rather than nested inside anything. Where you place the fragment definition relative to the query that uses it is left exactly as you wrote it, before or after, since the formatter does not reorder top-level definitions in a file. A fragment spread like ...PostFields prints on its own line among the other selected fields, and an inline fragment such as ... on User { name } gets its own indented selection set exactly like a regular nested field would.
Variables declared on an operation, including ones with default values such as $limit: Int = 10, stay inside the parentheses right after the operation name, each one separated by a comma and a space. Directives stay bound to whatever they were written on: a directive on a field, like @include(if: $includeDrafts) above, prints immediately after that field's arguments and before its selection set, and a directive on an argument stays attached to that argument. The formatter also accepts a shorthand anonymous operation with no query keyword and no name at all, an opening { straight into the selection set, and indents it exactly the same way as a fully named query.
| Construct | How it prints |
|---|---|
| Field alias | alias: field(...), spacing normalized like any other field |
| Fragment spread | ...FragmentName, its own line inside the selection set |
| Inline fragment | ... on TypeName { }, indented like a nested field |
| Directive | Stays immediately after the field or argument it modifies |
| Variable with a default | $name: Type = default, inside the operation's parentheses |
| Anonymous shorthand query | No query keyword required; indents the same as a named one |
This is print formatting, not query resolution. The formatter never inlines a fragment's fields into the place where it is spread, never checks that $includeDrafts is actually declared before it is used, and never confirms that @include is a directive your schema recognizes. Those are jobs for a GraphQL client or a schema-aware linter running against your actual API, not for a formatter that only reads and re-prints syntax.
A Schema Definition Is Formatted On Different Rules Than A Query
Lead: paste a type definition instead of a query and the same Format Code button switches behavior on its own: types, interfaces, enums, unions and inputs each get one field or value per line, and a description string always expands onto three lines of its own, even a one-line one.
type User{"""The user's unique id"""id:ID! name:String! email:String posts:[Post!]!}type User {
"""
The user's unique id
"""
id: ID!
name: String!
email: String
posts: [Post!]!
}The formatter does not ask whether your pasted text is an operation or a schema. It parses whatever you give it and prints back whatever kind of document it found. Feed it a query and you get query-shaped output; feed it type, interface, enum, union and input definitions and you get schema-shaped output, with every field of a type, every value of an enum, and every argument of an interface method placed on its own line the same way an operation's fields are. Non-null and list markers, ID!, [Post!]!, and so on, are preserved exactly as written, since those characters are part of the type itself rather than something the printer generates.
Description strings behave differently from ordinary field formatting, and it is worth knowing this before you rely on it: a triple-quoted description gets expanded onto three lines, an opening """, the text on its own line, a closing """, even when the whole description would easily fit on one line, as the seven-word example above shows. This is not a bug to work around; it is simply how the schema printer always renders block descriptions, and it applies whether the description sits on a type, a field, an argument, an enum value or an input field.
Field order in a schema is never alphabetized. A type with id, name, email and posts declared in that order comes back out in that same order. The same holds for operations: querying zebra, apple, then mango returns them in that order, and a field repeated twice in one selection set is printed twice rather than being deduplicated. If you want a specific field order, you have to write it that way; the formatter only reindents what is already there.
Comments Survive The Format. Most Blank Lines Do Too.
Lead: a # comment, whether it sits on its own line or trails after a field, passes through untouched. Blank lines between top-level definitions survive as well, but never more than one in a row.
# This is a comment
query {
# inline comment
user(id: 1) {
name # trailing comment
}
}# This is a comment
query {
# inline comment
user(id: 1) {
name # trailing comment
}
}Notice that the output above is identical to the input, aside from the reindentation it would already apply to an uncommented version of the same query. A standalone comment on its own line keeps its position relative to the field or definition that follows it, and a trailing comment stays attached to the end of the line it was written on. This matters in practice because comments are one of the few places developers leave notes for teammates directly inside a query, such as flagging a field that is only there for a legacy client, and a formatter that silently dropped them would quietly erase that context.
Blank lines get gentler treatment than comments. A single blank line left between two top-level definitions, two separate operations in the same document, or an operation and a fragment definition below it, is preserved as a visual separator. Leave two, three, or more blank lines in a row and the formatter collapses all of them down to exactly one; it never removes the separation entirely, and it never keeps extra padding beyond a single line. Inside a selection set, between individual fields, blank lines are not preserved at all, since fields inside braces are printed one directly after another regardless of spacing in the source.
Trailing whitespace and stray blank lines in the input box do not carry meaning. Because the parser reads structure rather than raw text, an extra blank line at the very top of your pasted code, or trailing spaces at the end of a line, have no effect on the result. The only whitespace the formatter treats as meaningful is a single blank line separating two top-level definitions, everything else is discarded during parsing.
Broken GraphQL Does Not Get Flagged. It Gets Guessed At.
Lead: this is a formatter, not a validator. When your pasted code has a real syntax error, nothing on the page tells you so. The tool silently falls back to a crude find-and-replace pass that does not understand GraphQL at all.
query { me: user(id: 1) { name email posts(limit: 5) { title query {
me: user(id: 1) {
name email posts(limit: 5) {
title The output above is not a formatting mistake in this article, it is exactly what the tool produces, double spaces before braces and all. Here is why. Behind the Format Code button, Prettier's real GraphQL parser does correctly detect that this query is broken; feeding it the same unclosed text produces a precise error, Syntax Error: Expected Name, found <EOF>, pointing at the exact line and column where the query runs out. But the component wraps that call in a try/catch block, and on any failure it logs the error to the browser's developer console, where you will never see it unless you go looking, and returns the output of a fallback function instead of an error message on the page.
That fallback is four chained text replacements: collapse all whitespace to single spaces, insert a line break and two-space indent after every {, insert a line break after every }, insert a line break and two-space indent after every comma. It has no concept of nesting depth, which is why every opening brace above gets the same flat two-space indent regardless of how deep it actually sits, and no concept of a matching closing brace, which is why an unclosed query like this one simply trails off with the structure never closed. The doubled space before each { is a side effect of the replacement order: the whitespace-collapse step leaves one space before the brace, and the brace-replacement step adds another without checking what is already there.
The same silent fallback fires if the formatting library itself never loads, not only on a syntax error in your text. The real parser is not bundled with the page; it is fetched from a public CDN the first time you click Format Code. If that request is blocked, offline, or fails for any other network reason, the try/catch treats it identically to a parse error and returns the same crude, non-nested output, even for a completely valid query. There is no on-page indicator that distinguishes "your GraphQL has a syntax error" from "the real formatter failed to load" from "the real formatter ran and this is genuinely correct output." A result that looks flat and oddly spaced, with doubled spaces before braces, is the tell that you are looking at the fallback rather than a real formatting pass.
The practical takeaway is that this tool cannot be used to check whether GraphQL is valid. If you need actual validation, confirmation that every field exists on the type it is queried against, that every variable used is declared, that every directive is spelled correctly, that has to come from a GraphQL client, an IDE extension backed by your real schema, or a dedicated linter. This formatter's job stops at reindenting syntax it can successfully parse.