How I Built jsoneditor.io: The Tech Stack Behind a Modern JSON Editor
A deep dive into building a browser-based JSON editor with Next.js 16, Monaco Editor, and AI. Learn the architecture decisions, challenges faced, and lessons learned.
I was tired of switching between five different tools just to work with JSON. One to format, another to validate, a third to convert to YAML, and don't even get me started on trying to compare two files. So I built jsoneditor.io - a single tool that does it all, runs entirely in your browser, and never touches your data.
Here's exactly how I built it.
Why Another JSON Editor?
Before building jsoneditor.io, I analyzed dozens of existing tools. Most fell into two camps:
- Simple formatters - Paste JSON, click button, get formatted output. No editing, no validation, no conversion.
- Desktop apps - Powerful but require installation, and I needed something I could use on any machine instantly.
I wanted something different: a VS Code-quality editor that runs in the browser with zero setup.
The Tech Stack
Next.js 16 + React 19
I chose Next.js 16 for its hybrid rendering capabilities. The editor itself is client-side (Monaco needs the DOM), but the marketing pages and blog are statically generated for SEO.
// Server component for SEO-critical pages
export default async function BlogPostPage({ params }) {
const post = await getPostBySlug(params.slug)
return <BlogPostContent post={post} />
}I chose Next.js over plain React because:
- Static generation for marketing pages (0ms TTFB)
- API routes for AI features without a separate backend
- Built-in image optimization saved me hours
- Turbopack makes dev server start in under 500ms
Monaco Editor: The Heart of the App
Monaco Editor is the same editor that powers VS Code. This was a non-negotiable choice - I wanted developers to feel instantly at home.
import Editor from '@monaco-editor/react'
<Editor
height="100%"
language="json"
theme="vs-dark"
value={content}
onChange={handleChange}
options={{
minimap: { enabled: false },
fontSize: 14,
automaticLayout: true,
formatOnPaste: true,
scrollBeyondLastLine: false,
}}
/>Monaco gives you for free:
- Syntax highlighting for JSON, YAML, XML
- Real-time error detection with squiggly underlines
- IntelliSense and auto-completion
- Code folding for navigating large files
- Find/replace with regex support
- Keyboard shortcuts that match VS Code
The tradeoff is that Monaco adds about 2.5MB to the bundle, however it's worth it for the developer experience.
Zustand for State Management
I tried Redux. I tried Context. Then I found Zustand and never looked back.
import { create } from 'zustand'
interface EditorStore {
tabs: Tab[]
activeTabId: string
addTab: (tab: Tab) => void
closeTab: (id: string) => void
updateContent: (id: string, content: string) => void
}
const useEditorStore = create<EditorStore>((set) => ({
tabs: [],
activeTabId: '',
addTab: (tab) => set((state) => ({
tabs: [...state.tabs, tab],
activeTabId: tab.id,
})),
// ... more actions
}))I chose Zustand because:
- 1KB bundle size (vs Redux's 7KB+)
- No boilerplate, no providers, no action types
- Works perfectly with React 19
- Devtools support when I need to debug
Tailwind CSS 4
Tailwind 4 just dropped and I migrated immediately. The new CSS-first configuration is cleaner.
<article className="rounded-2xl border border-zinc-800
bg-gradient-to-br from-zinc-900 to-zinc-900/50
p-8 transition-all duration-300
hover:border-zinc-700 hover:shadow-2xl
hover:shadow-amber-500/5">The dark theme uses zinc grays with amber accents. I wanted something that feels premium but doesn't strain your eyes at 2am.
AI Integration with Vercel AI SDK
The AI features use the Vercel AI SDK with GPT-4. Users can describe transformations in plain English:
"Extract all email addresses and format as a simple array"
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'
export async function POST(req: Request) {
const { prompt, json } = await req.json()
const result = await streamText({
model: openai('gpt-4-turbo'),
system: `You are a JSON transformation assistant.
Given JSON data and a user request, output
only the transformed JSON. No explanations.`,
prompt: `JSON: ${json}\n\nRequest: ${prompt}`,
})
return result.toDataStreamResponse()
}The streaming response means users see results appear in real-time, not after a long wait.
Architecture Decisions
Client-Side Processing: Privacy by Default
This was the most important decision. All JSON processing happens in your browser. Your data never hits my servers.
This matters because:
- Developers often work with sensitive data - API keys, user records, config files
- Companies have security policies against pasting data into random websites
- It just feels wrong to upload someone's data without explicit consent
The only exception is the AI feature, which requires sending data to OpenAI. I'm clear about this in the UI.
Multi-Tab Workspace
Most JSON tools handle one file at a time. I built a tabbed interface like VS Code:
interface Tab {
id: string
name: string
content: string
language: 'json' | 'yaml' | 'xml'
isDirty: boolean // unsaved changes indicator
}This lets you:
- Work on multiple files simultaneously
- Compare files side-by-side
- Keep reference data open while editing
Conversion Engine
JSON conversion uses battle-tested libraries:
| Conversion | Library | Why |
|---|---|---|
| JSON ↔ YAML | js-yaml | Most popular, well-maintained |
| JSON → CSV | json2csv | Handles nested objects, edge cases |
import yaml from 'js-yaml'
function convertToYaml(jsonString: string): string {
const obj = JSON.parse(jsonString)
return yaml.dump(obj, {
indent: 2,
lineWidth: -1, // Don't wrap lines
noRefs: true, // Don't use YAML anchors
})
}I considered building custom converters but decided against reinventing the wheel. These libraries handle edge cases I'd never think of.
Challenges I Faced
Challenge 1: Monaco Bundle Size
Monaco Editor is huge. The initial implementation added 4MB+ to my bundle.
Solution: The @monaco-editor/react wrapper lazy-loads Monaco from a CDN by default. This means:
- Initial bundle stays small
- Monaco loads in parallel with page render
- Users on slow connections see a loading skeleton, not a blank page
Challenge 2: Large File Performance
Users wanted to open files with 100,000+ lines. Initial attempts froze the browser.
Solution: Monaco's built-in virtualization handles this beautifully. It only renders visible lines, so a 1MB file performs the same as a 1KB file. I just had to trust Monaco and not fight it.
Challenge 3: Mobile Experience
Monaco doesn't work well on mobile. Touch interactions are janky, and the keyboard handling is desktop-focused.
Solution: I detect mobile devices and show a simplified textarea-based editor. It's not as powerful, but it works. Mobile users can still format and validate JSON.
Challenge 4: SSR Hydration Errors
Monaco requires browser APIs (window, document). Next.js server components would crash.
Solution: The Monaco wrapper handles this automatically - it renders a placeholder during SSR and mounts the real editor on the client. No special configuration needed.
Performance Numbers
Real metrics from Lighthouse and build output:
| Metric | Value |
|---|---|
| First Contentful Paint | 0.8s |
| Largest Contentful Paint | 1.2s |
| Time to Interactive | 1.4s |
| Total Bundle (gzipped) | 245KB (without Monaco) |
| Monaco (loaded async) | ~2.5MB |
| Build time | 12s |
| Dev server start | 426ms (Turbopack) |
The editor handles files up to 512MB without crashing. Beyond that, you probably need a database, not a text editor.
SEO and Answer Engine Optimization
I built the marketing pages with AI search engines in mind (ChatGPT, Perplexity, Google AI Overview).
Structured Data
Every page has comprehensive JSON-LD schema:
const schema = {
'@type': 'WebApplication',
name: 'JSON Editor Online',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Any',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD'
},
featureList: [
'JSON formatting',
'JSON validation',
'JSON to YAML conversion',
'JSON to CSV conversion',
],
}HowTo Schema for Tutorials
Step-by-step guides use HowTo schema so Google can show them as rich results:
{
'@type': 'HowTo',
name: 'How to Format JSON Online',
step: [
{ '@type': 'HowToStep', name: 'Open Editor', text: 'Go to jsoneditor.io' },
{ '@type': 'HowToStep', name: 'Paste JSON', text: 'Paste your JSON data' },
{ '@type': 'HowToStep', name: 'Format', text: 'Click the Format button' },
]
}FAQ Schema
The FAQ section uses FAQPage schema. When someone asks Google "how to validate JSON online", there's a chance my answer appears directly in results.
Deployment
The app runs on Vercel:
- Edge Functions for API routes (AI features)
- Global CDN for static assets
- Preview deployments for every PR
- Analytics to understand what features people actually use
Total monthly cost: $0 (Vercel's free tier is generous)
What I'd Do Differently
- Start with TypeScript strict mode - I enabled it later and spent a day fixing type errors
- Add analytics earlier - I built features I thought were important, not features users wanted
- Write tests from day one - Refactoring without tests is terrifying
What's Next
Features I'm working on:
- TypeScript type generation from JSON data
- JSON Schema validation with custom schemas
- Better diff view for comparing documents
- Vim keybindings (most requested feature)
Try It Yourself
Visit jsoneditor.io to try the editor. It's free, requires no signup, and your data never leaves your browser.
Questions or feedback? Find me on X @voidmode.
Building something similar? Feel free to reach out - I'm happy to chat about the architecture decisions in more detail.
André Figueira