DEV Community

ggwork
ggwork

Posted on

How to Parse TOML in the Browser Without a Backend

While working on a configuration-heavy project recently, I kept running into the same annoying problem. I'd receive TOML config files from teammates or clients, and I needed to quickly verify the structure, check for syntax errors, or convert them to JSON for debugging. Opening a terminal, installing a parser, and writing a script for a one-off check felt like overkill.

I tried using online tools, but most were either cluttered with ads, required uploading my config to a server, or only supported JSON/YAML. For a format that's supposed to be "obvious and minimal," the tooling around it felt anything but.

So I did what any reasonable developer would do: I built my own browser-based TOML parser. Because apparently I enjoy reinventing wheels.

The Problem With Existing Solutions

Before diving into code, let me explain why existing tools weren't cutting it.

Most online parsers have a fundamental flaw: they send your data to a server. For config files containing API keys, database credentials, or internal service endpoints, that's a non-starter. Even for non-sensitive data, the round-trip adds latency and breaks the flow of quick debugging.

The other issue was format support. Many "universal" config converters handle JSON and YAML well but treat TOML as an afterthought. When they do support it, the error messages are often cryptic — just "parse error" without line numbers or context.

I wanted something that:

  • Runs entirely in the browser (no uploads)
  • Parses TOML 1.0.0 spec correctly
  • Gives helpful error messages with line numbers
  • Shows results in both JSON and tree view

Choosing the Right Parser Library

The first decision was which TOML library to use. I evaluated two options:

// Option 1: smol-toml (ESM, modern)
import { parse } from 'https://esm.sh/smol-toml';

// Option 2: @iarna/toml (UMD, battle-tested)
const { parse } = require('@iarna/toml');
Enter fullscreen mode Exit fullscreen mode

I went with smol-toml for a few reasons. It's actively maintained, follows the TOML 1.0.0 spec strictly, and provides detailed error messages with line/column info. The ESM format works cleanly with modern browser imports.

But there's a catch: if the CDN fails or the user is offline, the tool breaks. I needed a fallback strategy.

Handling CDN Failures Gracefully

This was one of those "it worked on my machine" situations that almost bit me in production. I initially loaded the library with a simple import statement:

import { parse } from 'https://esm.sh/smol-toml';
Enter fullscreen mode Exit fullscreen mode

The problem? If that CDN request fails, the entire script crashes. No error message, no graceful degradation — just a blank page.

The fix was wrapping the import in a dynamic import with error handling. If the library fails to load, I show a clear message and fall back to a minimal built-in parser for basic cases:

let tomlParser = null;

async function loadTomlParser() {
  try {
    const module = await import('https://esm.sh/smol-toml');
    tomlParser = module.parse;
  } catch (e) {
    showError('Failed to load TOML parser library. Check your connection.');
    tomlParser = fallbackParser; // minimal built-in
  }
}
Enter fullscreen mode Exit fullscreen mode

The fallback parser handles basic key-value pairs and simple tables, but for complex nested structures, it honestly struggles. The lesson here: always plan for dependency failures, even with CDNs.

Building the Tree View

The most interesting part was the tree visualization. JSON output is straightforward — just stringify the parsed object with indentation. But a collapsible tree view requires recursive rendering.

Here's the core of the tree renderer:

function renderTree(obj, indent = 0) {
  if (Array.isArray(obj)) {
    return obj.map(item => renderTree(item, indent + 1)).join('');
  }

  if (typeof obj === 'object' && obj !== null) {
    return Object.entries(obj).map(([key, val]) => `
      <div>
        <span class="tree-toggle" onclick="toggleNode(this)">▸</span>
        <span class="tree-key">"${key}"</span>: 
        ${typeof val === 'object' && val !== null 
          ? `<div class="tree-children">${renderTree(val, indent + 1)}</div>`
          : `<span class="tree-${typeof val}">${formatValue(val)}</span>`}
      </div>
    `).join('');
  }

  return `<span class="tree-${typeof obj}">${formatValue(obj)}</span>`;
}
Enter fullscreen mode Exit fullscreen mode

The recursive approach works well for most cases, but I hit a performance wall with deeply nested structures. A config with 5+ levels of nesting and large arrays would freeze the browser momentarily. The fix was adding a depth limit — if the tree goes beyond 10 levels, I collapse it by default and let users expand manually.

The AI-Assisted Development Process

Full disclosure: I used Claude to help build this. Here's how the collaboration actually went.

First attempt: I described the tool requirements in detail — the layout, the features, the i18n support, the dark mode. The AI generated a complete HTML file in one shot. It looked impressive at first glance.

The problems emerged immediately:

  • The tree view didn't handle arrays properly (showed them as objects)
  • Error messages from the parser weren't being captured with line numbers
  • The i18n implementation was incomplete — some strings were hardcoded in Chinese

The iteration loop:

  1. I'd test a specific interaction
  2. Report the bug to the AI with a screenshot or error message
  3. Get a fix, but sometimes a new bug would appear elsewhere

The most frustrating part was the AI's tendency to over-engineer. It kept adding features I didn't ask for — like a settings panel and localStorage persistence — while missing the core error handling.

What the AI nailed:

  • The CSS design system with variables for dark mode
  • The responsive layout (grid to single column below 640px)
  • The basic parsing logic with debounced input

Where I had to step in:

  • Error handling edge cases (empty input, malformed TOML, circular references)
  • The tree view's array rendering
  • Making error messages actually helpful with line numbers

A Key Lesson: Debouncing Matters

When I first wired up the input to parse on every keystroke, it was laggy. Not because parsing is expensive, but because rendering the tree view on every change was wasteful.

let debounceTimer;
input.addEventListener('input', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(parseAndRender, 300);
});
Enter fullscreen mode Exit fullscreen mode

A simple 300ms debounce made the tool feel instant. This is one of those "obvious in hindsight" optimizations that makes a huge difference in perceived performance.

What I'd Do Differently

If I were building this again, I'd spend more time on the error messages. The TOML spec has specific error cases — unclosed strings, invalid dates, duplicate keys — and each deserves a clear, actionable message. The library gives me the line number, but converting that into "you forgot to close a quote on line 12" requires more work than I initially thought.

The Result

During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles TOML parsing, JSON conversion, tree visualization, and error reporting — all locally in the browser. If you're dealing with TOML configs regularly, you can check it out at Craftvo.

The real takeaway here isn't the tool itself — it's the process. Building browser-based dev tools that respect user privacy (no server uploads) is increasingly important. And with AI assistance, what used to take days can now take hours. The catch is that AI gives you a working solution, not a correct one. The debugging and edge-case handling still require human judgment.

That's the honest truth about AI-assisted development: it's a great rubber duck, but it's not a senior engineer. Yet.

Top comments (0)