Kranthi Kumar MuppalaEvery developer has a handful of bookmarked online tools - a JSON formatter here, a Base64 encoder...
Every developer has a handful of bookmarked online tools - a JSON formatter here, a Base64 encoder there, a regex tester somewhere else. Each one has ads, signup walls, or sends your data to a server. I wanted them all in one place, running entirely in the browser with zero server dependency.
The result is utils.live: 734 tools across 34 categories, statically generated and served from Cloudflare Pages for $0/month. The entire codebase was built using Claude Code.
Here's how the architecture works.
defineTool()
Every tool in the system is a stateless pure function wrapped in a schema-validated definition. The defineTool() function takes four things: metadata, a Zod input schema, a Zod output schema, and an execute function:
export const jsonFormatter = defineTool({
meta: {
id: "json/formatter",
name: "JSON Formatter",
description: "Format and prettify JSON with configurable indentation",
category: "json",
keywords: ["json", "format", "prettify", "beautify", "indent"],
},
inputSchema: z.object({
input: z.string().describe("JSON string to format"),
}),
outputSchema: z.object({
output: z.string().describe("Formatted JSON string"),
}),
optionsSchema: z.object({
indent: z.number().int().min(0).max(8).default(2),
sortKeys: z.boolean().default(false),
}),
execute: (input, options) => {
const parsed = JSON.parse(input.input);
return {
output: JSON.stringify(parsed, null, options?.indent ?? 2),
};
},
});
This pattern has a few properties I find useful.
Type safety is end-to-end. The generic type parameters on defineTool() infer input/output types from the Zod schemas, so the execute function's parameters are fully typed without any explicit type annotations.
The schemas serve double duty. At execution time, they validate inputs. At build time, they're converted to JSON Schema (via Zod's toJSONSchema()) and used to generate the UI —form fields, editor language selection, file upload settings —all without any per-tool UI code.
Tools are pure. No side effects, no DOM access, no network calls. The tools package has zero React or browser dependencies. It's pure TypeScript functions all the way down.
The executor wraps every tool call with validation, size limits, timeout enforcement, and metadata collection:
export async function executeTool(tool, input, options) {
// 1. Validate input against Zod schema
const inputResult = validateInput(tool.inputSchema, input);
if (!inputResult.success) return failure(inputResult.error);
// 2. Validate options (applies defaults from schema)
const optionsResult = validateOptions(tool.optionsSchema, options);
if (!optionsResult.success) return failure(optionsResult.error);
// 3. Reject inputs > 5MB to prevent memory exhaustion
if (getByteSize(input) > 5 * 1024 * 1024)
return failure("Input too large");
// 4. Execute with 5-second timeout via Promise.race
const output = await Promise.race([
Promise.resolve(tool.execute(inputResult.data, optionsResult.data)),
createTimeoutPromise(5000),
]);
// 5. Return result with execution metadata
return success(output, { executionTimeMs, inputSizeBytes, outputSizeBytes });
}
The result type is a discriminated union (ToolSuccess<T> | ToolFailure), so consumers always handle both paths. Every execution produces metadata: timing in milliseconds, input/output byte sizes, and a timestamp.
With 734 tools, writing per-tool UI configuration would be absurd. Instead, the system infers everything from tool metadata and schema shapes.
There are three layout variants:
Detection is heuristic. It looks at the tool name and input schema structure:
function getToolVariant(tool, inputSchema, diffPatterns) {
const name = tool.name.toLowerCase();
// Name contains "diff", "compare", "difference" -> diff layout
if (diffPatterns.some(p => name.includes(p))) return "diff";
// Name contains "generator", "random", "lorem" -> generator layout
if (GENERATOR_PATTERNS.some(p => name.includes(p))) return "generator";
// All input fields have defaults -> form-based (generator)
if (allFieldsOptionalOrDefaulted(inputSchema)) return "generator";
return "standard";
}
The output renderer follows the same pattern. Category and tool name determine which of the 10 render modes to use:
| Category/Pattern | Renderer | What it does |
|---|---|---|
| json, yaml, css | code |
Monaco editor with syntax highlighting |
| csv | table |
Sortable, filterable data table |
| html | html |
Sandboxed iframe preview |
| markdown | markdown |
Rendered markdown |
| color | color |
Color swatch display |
| "palette", "scheme" in name | color-palette |
Grid of color swatches |
| "diff", "compare" in name | diff |
Side-by-side diff viewer |
| image tools | image |
Image preview with zoom |
| diagram tools | diagram |
Mermaid diagram renderer |
All renderers are lazy-loaded via next/dynamic with ssr: false, so the initial bundle only includes the renderer a specific tool needs.
Converter tools get special handling: a tool ID like json-to-yaml is parsed to detect the target format, and the output editor automatically gets YAML syntax highlighting.
The options panel auto-generates form fields from the JSON Schema:
boolean -> toggle switchenum -> searchable select dropdownnumber with min/max -> range slidernumber without bounds -> number inputstring -> text inputProperty names are converted from camelCase to title case automatically (sortKeys -> "Sort Keys"). The generator layout does the same thing for input schemas, turning structured Zod definitions into interactive forms without any hand-written UI.
Each tool gets its own URL (/tools/{category}/{tool-slug}) and is pre-rendered at build time:
export function generateStaticParams() {
const tools = getAllToolCards(); // Returns all 734 tools
return tools.map(tool => {
const [category, slug] = tool.id.split("/");
return { category, tool: slug };
});
}
The getTool() function bridges the tool system and the Next.js app. It converts Zod schemas to JSON Schema for the client-side form renderer, using React's cache() to avoid redundant conversions:
export const getTool = cache((category, toolSlug) => {
const tool = getToolById(`${category}/${toolSlug}`);
const ui = getToolUIConfig(tool.meta); // Inferred UI config
return {
meta: tool.meta,
ui,
inputSchema: convertSchema(tool.inputSchema), // Zod -> JSON Schema
optionsSchema: convertSchema(tool.optionsSchema),
outputSchema: convertSchema(tool.outputSchema),
examples: tool.meta.examples ?? [],
};
});
The build produces ~7,800 static files, well under Cloudflare Pages' 20,000 file limit.
All 734 tools are registered via side-effect imports in a single file:
import { jsonFormatter, jsonValidator, jsonMinify } from "./json";
import { yamlFormatter, yamlToJson } from "./yaml";
// ... 30+ more category imports
const ALL_TOOLS = [
jsonFormatter, jsonValidator, jsonMinify,
yamlFormatter, yamlToJson,
// ... 734 tools total
];
export function registerAllTools() {
for (const tool of ALL_TOOLS) {
globalRegistry.registerTool(tool);
}
}
registerAllTools(); // Auto-register on import
The registry validates tool IDs against the pattern ^[a-z]+\/[a-z0-9-]+$ and checks that the category portion exists in the 34 predefined categories. Duplicates throw.
This file is 1,700 lines long. It works, but an auto-discovery pattern (glob imports) would be cleaner. It's on the list.
For the standard layout, tools execute automatically as you type with a 300ms debounce:
useEffect(() => {
if (!input) return;
const timer = setTimeout(async () => {
const { executeTool, getToolById } = await import("@utils-live/tools");
const tool = getToolById(toolId);
const result = await executeTool(tool, { input }, options);
setOutput(result);
}, 300);
return () => clearTimeout(timer);
}, [input, options]);
The tools package itself is dynamically imported to keep it out of the initial page bundle. It only loads when a user actually visits a tool page.
The global search (Cmd+K) uses a weighted multi-tier scoring system:
Exact name match -> 1.0
Prefix match -> 0.9 + length bonus
Contains match -> 0.6 + position bonus
Word boundary match -> 0.7
Fuzzy (Levenshtein) -> 0.5 if similarity > 0.5
Scores are weighted by field: name (1.0x), keywords (0.8x), description (0.7x). Searching "base64" instantly finds "Base64 Encode" over a tool whose description merely mentions base64.
Recent tools are tracked in localStorage for quick access.
The entire codebase was built using Claude Code. A few patterns that worked well:
Tool generation is mechanical. Once the defineTool() pattern was established, adding new tools is formulaic: define schemas, write the execute function, add a test, register it. Claude Code handles this efficiently because each tool is self-contained with no cross-dependencies.
Parallel agents for bulk operations. Tasks like "update all 34 category descriptions" or "remove open-source references from every file" benefit from launching 3 agents working on independent file sets. Wall-clock time drops significantly.
Tests as specification. The tools package enforces 100% test coverage thresholds. Writing the test first and having Claude Code implement the tool to pass it produced more reliable results than writing the implementation first.
Schema-first design. Describing what a tool does via Zod schemas before writing the logic turned out to be a natural fit for AI-assisted development. The schemas constrain the implementation enough that the generated code is usually correct on the first pass.
The entire site deploys as a static site on Cloudflare Pages' free tier. No database, no server, no containers. Git push triggers a rebuild.
pnpm turbo build
-> packages/tools builds first (TypeScript -> dist/)
-> apps/web builds second (Next.js static export -> out/)
-> ~7,800 files deployed to Cloudflare's edge network
Total hosting cost: $0/month.
Tool descriptions are too short for SEO. Most are 30-60 characters when meta descriptions should be 150-160. Working on auto-generating longer descriptions from the existing metadata.
No OG images. Static export can't use Next.js's edge-runtime image generation. Planning to add build-time OG image generation with satori + resvg.
The 1,700-line registration file. A single array of 734 imports works but isn't elegant. Auto-discovery via glob imports would be better.
Should have started with fewer, more polished tools. Having 734 tools is impressive as a number but many could use better examples and documentation. Depth over breadth would have been the smarter launch strategy.
If you want to try it: utils.live
The tools I use most: JSON Formatter, Base64 Encoder, JWT Decoder, Regex Tester.