<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Rahul Sharma</title>
    <description>The latest articles on Forem by Rahul Sharma (@rahul_sharma_634ea8ae27ef).</description>
    <link>https://forem.com/rahul_sharma_634ea8ae27ef</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2305104%2F28265cdf-dfd6-41b8-aa94-e57e106072bc.png</url>
      <title>Forem: Rahul Sharma</title>
      <link>https://forem.com/rahul_sharma_634ea8ae27ef</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rahul_sharma_634ea8ae27ef"/>
    <language>en</language>
    <item>
      <title>I Built a Free Cypher Query Formatter for Neo4j — Here's Why and How</title>
      <dc:creator>Rahul Sharma</dc:creator>
      <pubDate>Tue, 10 Mar 2026 18:37:42 +0000</pubDate>
      <link>https://forem.com/rahul_sharma_634ea8ae27ef/i-built-a-free-cypher-query-formatter-for-neo4j-heres-why-and-how-gc4</link>
      <guid>https://forem.com/rahul_sharma_634ea8ae27ef/i-built-a-free-cypher-query-formatter-for-neo4j-heres-why-and-how-gc4</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Free Cypher Query Formatter for Neo4j — Here's Why and How
&lt;/h1&gt;

&lt;p&gt;If you've worked with Neo4j for any length of time, you've probably stared at a wall of unformatted Cypher that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:FRIENDS_WITH&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;friend:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:WORKS_AT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;friend.age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="k"&gt;STARTS&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="s1"&gt;'Neo'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;friend&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;friend&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:LIVES_IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;city:&lt;/span&gt;&lt;span class="n"&gt;City&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city.population&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;friend.name&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;friends&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not exactly readable. I kept copy-pasting queries from logs, Slack messages, and Stack Overflow answers that looked like this — and manually reformatting them was eating into my time. So I built a tool to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://cypher-formatter.pages.dev/" rel="noopener noreferrer"&gt;Cypher Query Formatter&lt;/a&gt;&lt;/strong&gt; — a free, browser-based tool that takes a messy Cypher query and formats it instantly with proper indentation, line breaks, and syntax highlighting.&lt;/p&gt;

&lt;p&gt;The same query from above becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:FRIENDS_WITH&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;friend:&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:WORKS_AT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;friend.age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
    &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="k"&gt;STARTS&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="s1"&gt;'Neo'&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;friend&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cnt&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;OPTIONAL&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;friend&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:LIVES_IN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;city:&lt;/span&gt;&lt;span class="n"&gt;City&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;city.population&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;friend.name&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;friends&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;city.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instant formatting&lt;/strong&gt; — paste your query, click Format (or hit &lt;code&gt;Ctrl+Enter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syntax highlighting&lt;/strong&gt; — keywords, functions, labels, properties, strings and parameters all highlighted in distinct colors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configurable indent&lt;/strong&gt; — 2 spaces, 4 spaces, or tabs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword case conversion&lt;/strong&gt; — UPPERCASE, lowercase, or Capitalize&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dark and light theme&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy to clipboard&lt;/strong&gt; or &lt;strong&gt;download as &lt;code&gt;.cypher&lt;/code&gt; file&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% client-side&lt;/strong&gt; — your queries are never sent to any server&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works — The Technical Side
&lt;/h2&gt;

&lt;p&gt;This was a fun vanilla JS project. Here's the approach I took:&lt;/p&gt;

&lt;h3&gt;
  
  
  Tokenizer
&lt;/h3&gt;

&lt;p&gt;The core is a hand-written tokenizer that walks the query character by character and emits typed tokens: &lt;code&gt;clause&lt;/code&gt;, &lt;code&gt;sub_clause&lt;/code&gt;, &lt;code&gt;function&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;comment&lt;/code&gt;, &lt;code&gt;parameter&lt;/code&gt;, &lt;code&gt;operator&lt;/code&gt;, &lt;code&gt;bracket&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;The tricky part was handling &lt;strong&gt;multi-word clauses&lt;/strong&gt; correctly. Cypher has things like &lt;code&gt;OPTIONAL MATCH&lt;/code&gt;, &lt;code&gt;ON CREATE SET&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;STARTS WITH&lt;/code&gt;, and &lt;code&gt;IS NOT NULL&lt;/code&gt;. A naive word-by-word approach would break &lt;code&gt;STARTS WITH&lt;/code&gt; into the operator &lt;code&gt;STARTS&lt;/code&gt; and the major clause &lt;code&gt;WITH&lt;/code&gt; — which is wrong.&lt;/p&gt;

&lt;p&gt;The solution was to match multi-word sub-clauses &lt;em&gt;before&lt;/em&gt; checking major clauses, using a longest-match-first sorted list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MULTI_WORD_SUB_CLAUSES_SORTED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IS NOT NULL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STARTS WITH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENDS WITH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WITH HEADERS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IS NULL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By checking these first, &lt;code&gt;STARTS WITH&lt;/code&gt; is correctly identified as a sub-clause operator before &lt;code&gt;WITH&lt;/code&gt; can be matched as a major clause.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatter
&lt;/h3&gt;

&lt;p&gt;After tokenization and classification, the formatter rebuilds the query string with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A newline before each major clause (&lt;code&gt;MATCH&lt;/code&gt;, &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;RETURN&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;An indent + newline before &lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;XOR&lt;/code&gt; inside &lt;code&gt;WHERE&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;An indent + newline after commas in &lt;code&gt;RETURN&lt;/code&gt;, &lt;code&gt;WITH&lt;/code&gt;, and &lt;code&gt;SET&lt;/code&gt; clauses&lt;/li&gt;
&lt;li&gt;Proper spacing around operators and brackets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Syntax Highlighter
&lt;/h3&gt;

&lt;p&gt;The highlighter runs the same tokenizer on the &lt;em&gt;already formatted&lt;/em&gt; output and wraps each token type in a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; with a CSS class. Context matters here — an identifier after a &lt;code&gt;:&lt;/code&gt; is a label (red), after a &lt;code&gt;.&lt;/code&gt; is a property (blue), otherwise it's a variable (pink).&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Writing a tokenizer from scratch is surprisingly fun.&lt;/strong&gt; The edge cases are where it gets interesting — escaped quotes inside strings, backtick-escaped identifiers, block comments, parameters starting with &lt;code&gt;$&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-word keyword matching needs careful ordering.&lt;/strong&gt; This tripped me up early on and produced some very wrong formatting before I got it right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages is a great free host for static tools like this.&lt;/strong&gt; Zero config, instant deploys from git, generous free tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://cypher-formatter.pages.dev/" rel="noopener noreferrer"&gt;cypher-formatter.pages.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's free, open to everyone, and works in any modern browser. If you hit a query, it formats incorrectly. I'd genuinely love to hear about it in the comments. Edge cases in Cypher are plentiful, and I'm sure I've missed some.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with vanilla JavaScript. No frameworks, no dependencies, no server.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>neo4j</category>
      <category>cypher</category>
      <category>graphdatabase</category>
    </item>
  </channel>
</rss>
