I've been building small utility tools for the web; JSON formatters, CSV processors, PDF text extractors, those kinds of things. The kind of tools where you have a problem, write a 50-line Python function to solve it, and it just works. Then, you want to share it with someone, but they can't be bothered (or don't know how) to set up the script and run it locally.
The obvious solution is "build a web app". The obvious problem is that building a frontend for every small script is a lot of overhead for something that should take an afternoon.
So I built something a little different.
The architecture
TL;DR: a Python function decorated with @tool() automatically becomes a web app. No React. No API endpoint. No routing.
Here's what a tool looks like on the Python side:
from nix_sdk import tool, TextResult
@tool(
name="Simple JSON tool",
description="Validate a JSON file - paste or upload!",
slug="json-tool"
)
def json_formatter(
json_input: str = Input(label="JSON input", placeholder="Paste JSON here..."),
indent: int = Dropdown(label="Indent width", choices=[2, 4, 8], default=2),
sort_keys: bool = Switch(label="Sort keys alphabetically"),
) -> TextResult:
"""Format and validate a JSON document."""
import json
parsed = json.loads(json_input)
return TextResult(value=json.dumps(parsed, indent=indent, sort_keys=sort_keys))
The @tool() decorator inspects the function's type hints and default values, and auto-generates a JSON Schema manifest. That manifest looks something like this:
{
"name": "Simple JSON tool",
"slug": "json-tool",
"input_schema": {
"type": "object",
"properties": {
"json_input": {
"type": "string",
"x-nix": { "widget": "textarea", "label": "JSON input" }
},
"indent": {
"type": "integer",
"enum": [2, 4, 8],
"x-nix": { "widget": "dropdown", "label": "Indent width" }
},
"sort_keys": {
"type": "boolean",
"x-nix": { "widget": "switch", "label": "Sort keys alphabetically" }
}
}
},
"output": { "type": "text" }
}
Separately, the Nix frontend shell reads that manifest and renders the form. Completely automatically.
What the shell does
The Next.js frontend has one dynamic route: /tools/[slug]. It fetches the manifest for that slug from the FastAPI backend, then renders the form using a custom renderer registry. Each x-nix.widget type maps to a Mantine component — textarea becomes a Textarea, dropdown becomes a Select, switch becomes a Switch, file becomes a file upload zone.
The same shell also renders a static landing page at /tools/[slug] with optional seo fields for metadata, H1, and FAQ content so that the content can be posted publicly. One Python file produces both the tool interface and the SEO-optimised landing page.
No React by you. No routing by you. No API wiring by you.
How execution works
The frontend submits the form to POST /api/tools/{slug}/run. FastAPI looks up the tool by slug, validates the input against the JSON Schema, and calls the Python function in a thread via asyncio.to_thread().
For long-running tools, there's a WebSocket path at /ws/tools/{slug}/run. The tool can call progress("Step 2 of 4: processing rows...") from inside the function and those messages stream to the UI in real time.
File uploads are handled as multipart form data. Files are ephemeral - held in temp storage during execution, then discarded. Nothing is persisted.
What's live now
I've used this to build nine tools that are live at nix.tech:
- JSON Formatter & Validator and XML Formatter & Validator; validates with exact line/column error locations, finds all issues at once rather than stopping at the first
- PDF Text Extractor; page-range selection, explicit scanned-PDF detection
- CSV to JSON Converter; handles TSV too
- CSV Deduplicator; column-level dedup with keep-first/keep-last options
- Markdown to HTML; GFM extensions, fragment or full page output
- Base64 Encoder/Decoder; handles files, not just strings
- SEO Keyword Research; uses DataForSEO's API (to see how it works in these tools!) and provides search volume, CPC and ad competition without a Google Ads account
- Alt Text Generator; AI-generated, SEO-focused alt text with optional site context
All the tools have free usage limits, without sign-ups. Files discarded after processing.
Tradeoffs
The manifest-driven approach has obvious limits. Complex multi-step flows are hard to express in a flat JSON Schema. Conditional field visibility requires extensions to the spec. Rich output types (e.g., interactive charts, side-by-side diffs) need custom renderer work.
For single-function utility tools - which is where I was testing for now — none of those limits are an issue. The upside is that adding a new tool is adding one Python file. The frontend, API endpoint, landing page, and form all derive from the manifest automatically.
Whether that tradeoff stays favourable at scale is a different question.
If any of the architecture is interesting to dig into further, happy to go into more detail in the comments!
Top comments (0)