DEV Community

Cover image for The Forgotten Joy of `node app.js`
Viktor Lázár
Viktor Lázár

Posted on

The Forgotten Joy of `node app.js`

There used to be a moment, ten years or so ago, when you could go from "I have an idea" to "I have a running web server" in about thirty seconds:

// app.js
const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('hello'))

app.listen(3000)
Enter fullscreen mode Exit fullscreen mode
node app.js
Enter fullscreen mode Exit fullscreen mode

That was the whole thing. One file. One command. You could paste it into a Slack message. You could drop it in a Gist and someone could run it. A tiny webhook receiver, a debug dashboard, an internal tool, a stub API — the entire project lived in a single buffer in your editor.

Then frontend frameworks happened, and somewhere along the way we collectively decided that "starting a new project" meant something else entirely.

The scaffold tax

Today, the canonical first step in starting a new app is no longer writing code. It is running a command that writes code for you:

npx create-next-app@latest my-app
npx create-react-app my-app
npm create vite@latest
npx create-remix
Enter fullscreen mode Exit fullscreen mode

What comes back is not a file. It is a tree. Configuration files for tooling you have not yet decided to use. A pages/ or app/ directory with conventions you must learn before you can write a single line. A tsconfig.json you did not write. ESLint rules. Prettier rules. A .gitignore. A README.md describing the scaffold itself. A package.json with twelve dependencies and four scripts you did not pick.

And, critically, there is no path back to a single file. The scaffold is the unit of starting. There is no framework dev ./App.jsx. There is only framework new my-project, which produces forty files, of which you will edit two.

This is fine when you are starting a real product. It is absurd when you are not.

What we lost

The single-file app is not a relic of a less mature ecosystem. It is a fundamentally different mode of working — one the modern frontend toolchain has quietly priced out of existence.

Specifically, we lost:

The throwaway. The five-minute hack to verify that an idea works. The "let me just see what this looks like rendered" experiment. With a scaffold, the cost of starting is high enough that you don't bother. You either pollute an existing big project, or you open the browser DevTools console.

The teaching artifact. A blog post used to be able to say here, run this file. Now it says clone this repo. The reader is no longer reading code; they are operating a project.

The micro-app. The three-route admin tool. The internal status page. The webhook that posts a Slack message. Things that should be one file are now twenty, because the framework demands it.

The shareable Gist. I cannot send you a single .jsx file and have you run it. I have to send you a repository — or a CodeSandbox URL, which is its own confession that the local toolchain has gotten too heavy.

The curl-and-run. Plain Node lets you stream a program straight from a URL into the runtime, no file on disk: curl https://gist.githubusercontent.com/.../app.js | node. No clone, no install, no project to set up. The source travels over the wire, lands in the interpreter, runs. The same pattern should work for a single-file frontend app — curl https://.../App.jsx | npx some-framework dev - — and the fact that this is unimaginable today is the most concrete possible measurement of how heavy "starting a frontend app" has become. We have a JavaScript-shaped hole in our shells that the language used to fit through.

There is a fractal version of this same pain one level down. Even inside a project, modern React's "use client" directive forces single features to be sharded across multiple files for purely mechanical reasons — the same disease, at smaller scale. I wrote about that version separately in The "use client" Tax. What follows here is the project-level shape of the same problem: even when the whole app should be one file, you are not allowed to write it that way.

The shape of the fix

Imagine, for a second, that this just worked:

npx next dev ./App.jsx
Enter fullscreen mode Exit fullscreen mode

One file. One command. The framework picks it up, runs it, hot-reloads it, serves it. No next.config.js, no pages/, no app/, no package.json. If you decide later that you want a real project, you make the directory, add the config, split the file. The framework grows with you instead of demanding everything upfront.

The technology to do this is not hard. Frameworks already build on dev servers — Vite, esbuild, Turbopack — that can resolve and bundle a single entry point. The framework conventions (file-based routing, layouts, server components) are conventions over the bundler, not replacements for it. There is no fundamental reason a framework's CLI cannot accept a path to a .jsx file and Just Work, with the conventions kicking in only once you opt into a directory layout.

The reason it doesn't work is not technical. It's cultural. We have decided, somewhere along the way, that the project is the unit of frontend code, and the file is merely an implementation detail. Backend frameworks never made that mistake. You can still write a fifteen-line server.js and run it. You can still write a Flask app in one file. You can still put a Go HTTP handler in main.go and ship it. Scaffolds are offered as a convenience, not enforced as a precondition.

Frontend should be no different.

One file in, one file out

The single-file dev story is only half of the picture. The other half is what comes out when you build.

Today, building a frontend project produces another tree. A .next/ directory. A dist/ directory. A .output/ directory. Hundreds of chunked JavaScript files, manifests, server bundles, client bundles, route maps — and a node_modules you must ship alongside it, or carefully fold into the deployment artifact. Running the result usually means another framework-specific command (next start, node .output/server/index.mjs) that depends on the surrounding directory structure being intact.

It should be possible to do this:

some-framework build ./App.jsx -o app.js
node app.js
Enter fullscreen mode Exit fullscreen mode

One file in. One file out. No node_modules, no config, no manifest, no dist/ to preserve. A single .js that boots an HTTP server, serves the assets it needs (inlined or referenced), and runs on any Node install with nothing else next to it.

Backend developers have had this for years, just under different names. Go produces a static binary. Deno compiles to a single executable. esbuild can bundle a Node program into one file. The pattern is universal: take everything the program needs, fold it into one artifact, ship that. Nothing about a React app — even a server-rendered, server-component-heavy React app — fundamentally prevents the same thing.

What this unlocks is bigger than convenience:

  • Trivial deployment. scp app.js server:/srv/ && node app.js. No CI artifact pipelines, no Docker images for a webhook receiver, no Kubernetes for a status page.
  • Reproducibility. The artifact is a file. You can hash it, version it, archive it, email it. Not a directory whose contents quietly differ depending on which npm install produced it.
  • Sandboxes. A single file is something a sandbox runtime — a serverless platform, a worker, a container — can swallow whole, with no need to mount a node_modules.
  • Distribution. Internal tools become as easy to share as a CLI binary. "Drop this on the server and run it" is a workflow we lost the moment frontends grew a build directory.

The deploy story for a small app should be as small as the app. Right now, even a thirty-line frontend deploys like a monorepo.

And then AI showed up

The scaffold tax used to be paid mostly by humans — a one-time annoyance you absorbed at project start, then forgot about. AI coding tools have quietly turned it into a recurring tax, paid on every interaction.

When you ask an AI to modify a single-file app, it can read the entire program in one shot, hold the whole behavior in its working memory, and reason about a change with confidence. The file is the project. There is nothing else to discover.

When you ask an AI to modify a scaffolded project, it has to do archaeology first. Where does routing live? Which tsconfig paths are aliased? Is that import resolved by a framework convention or by the bundler? Is app/ the routing root, or a coincidentally-named folder? What does the project's ESLint config forbid? Half the request gets spent loading context that wasn't actually relevant to the change.

This shows up as:

  • Worse answers, because the model is reasoning under a noisier prompt.
  • Slower answers, because more files have to be read before it can act.
  • More expensive answers, because tokens are not free, and a fresh agent re-discovers the same project structure on every session.
  • More fragile answers, because the model has more surface area on which to misread a convention.

A one-file app is, by accident, the ideal substrate for AI-assisted coding: the entire program fits in a single attention window, every symbol resolves locally, and the change you ask for can be made without crawling a directory tree first. The convention overhead we built up to make starting a project "easier" turns out to be overhead we now pay every time we ask a tool to help us edit one.

The same things that made the single-file app pleasant to write by hand — small surface, no hidden conventions, nothing to discover — make it the format AI tools handle best. We just stopped producing apps in that shape.

Why this matters

There is a subtle compounding effect to all of this. When the cost of starting is high, people start fewer things. When people start fewer things, the ecosystem gets less weird, less experimental, less playful. The thirty-line idea that would have become a beloved internal tool never gets written, because the scaffolding tax was higher than the energy budget for the experiment.

The modern frontend stack is extraordinarily capable. It can render server components, stream HTML, hydrate selectively, generate static pages, run on the edge, do incremental builds. None of that is at odds with also being able to do this:

some-framework dev ./App.jsx
Enter fullscreen mode Exit fullscreen mode

It's a small surface area. It's enormously valuable. And it is, conspicuously, missing from almost every option you'd reach for today.

The good news is that almost. If you look around carefully, this capability is starting to reappear in the corners of the ecosystem — runtimes that treat the single file as a first-class entry point, not as a degenerate case of a project. It's worth keeping an eye on.

The thirty-second app deserves to come back.

Top comments (0)