Versatile Npm-Free Web Stack
Heavy full-stack frameworks like Next.js are less useful than ever because AI can assemble the underlying building blocks with more flexibility and specificity.
Meanwhile, the declarativeness of React, the terseness of Tailwind and the composability of Express still hold a lot of value. However, I've always disliked the heaviness and wonky npm dependencies they brought. Combining Hono, Preact and Tailpipe gets you all these things without the dependencies.
Below we'll show how to conveniently set this stack up, sourcing Preact, Hono, etc directly via git without pulling opaque bundles from the npm registry.
| Stack | Build | Server | Frontend | Styling* | Packages† | Install size |
|---|---|---|---|---|---|---|
| Next.js | built-in | built-in | React | Tailwind | 42 | 342 MB |
| TanStack Start | Vite | h3 | React | Tailwind | 167 | 132 MB |
| common | Vite | Express | React | Tailwind | 105 | 62 MB |
| ours | esbuild‡ | Hono | Preact | Tailpipe | 0 | 14 MB |
* Adding Tailwind (or Tailpipe) is not required in any of the stacks.
† Packages downloaded from npm.
‡ Or bun / qn, which bundle with built-ins instead of an external tool.
In our stack, Hono serves APIs and static files, Preact renders a SPA and Tailpipe generates Tailwind classes at runtime. There are no transitive dependencies.
Setup. We have a companion repo with minimal full-stack example setups for Node, Bun and our own experimental Qn runtime. We'll continue with Node for now, you can read more about Bun and Qn below.
...
"scripts": {
"prepare": "node install.js",
"start": "node server.js",
"dev": "node --watch server.js --dev"
},
"sourceDependencies": {
"preact": {
"git": "https://github.com/preactjs/...
"rev": "21dd6d04c1a9a43e5b60976bb5eb...
"exports": { ".": "./src/index.js", ...
},
...
}
While the standard dependencies field in package.json accepts git URLs, many packages don't work that way (e.g. Hono) or require different build tools. So we declare them in a custom sourceDependencies field instead, each pinned to a git repo and commit hash. The optional exports map covers packages whose source layout doesn't match their published dist/: it points the install step at the right source files.
A small install.js script (run via the prepare hook on every npm install1) realizes those declarations: each repo is cloned at its pinned commit and built for Node via esbuild. The result is a node_modules dir that is functionally equivalent to one with npm downloads. On Bun the script is simpler, skipping esbuild and only rewriting exports to point at source. On Qn no script is needed at all: qn install handles sourceDependencies natively.
The pinned commit hashes provide full source-based supply chain integrity2. In the past, hand-rolling a manifest like this might have been inconvenient. With AI coding, it's practical and explicit.
The server.js runs directly without bundling and shells out to esbuild to build the frontend while rebuilding automatically in --dev mode.
Runtimes. Aside from Node, this stack runs even more elegantly on Bun and Qn. Both come with integrated bundlers and run Hono without @hono/node-server. They also accept unbundled Typescript dependencies in node_modules, which lets you skip the build step during install and get end-to-end type checking across client and server.
| Runtime | Bundler | HTTP | Binary size | Example |
|---|---|---|---|---|
| Node | esbuild | @hono/node-server | 118 MB | /node |
| Bun | Bun.buildbuilt-in | Bun.servebuilt-in | 97 MB | /bun |
| Qn | qn:bundlebuilt-in | qn:httpbuilt-in | 3.7 MB | /qn |
- Or
bun install. It can also be invoked manually asnode install.js.qn installdoesn't need the script at all. - A git commit hash is a content hash over the full source tree. Pinning commit hashes across repos extends git's Merkle DAG into a single tree covering all dependencies.