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 dependencies.js",
"start": "node server.js",
"dev": "node --watch server.js --dev"
}
...
While npm supports git repo urls as dependencies, many packages do not support this (e.g. Hono) or require different build tools. So instead we have a prepare script which runs on every npm install1 and installs all dependencies directly from their git source.
[{
name: "preact",
repo: "https://github.com/preactjs/...
hash: "21dd6d04c1a9a43e5b60976bb5eb...
build: () => "esbuild ./src --bundle...
}, ...
].map(({ name, repo, hash, build }) => {
...
execSync(`... git fetch ${repo} ${hash} ...
...
})
The dependencies' sources are cloned from git and built with esbuild, resulting in a node_modules dir that is functionally equivalent to one with npm downloads.
The pinned commit hashes provide full source-based supply chain integrity2. In the past, managing a dependencies.js file might have been inconvenient. With AI coding, it's practical and explicit.
The server.js (or server.ts) 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.
| 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/qn install. It can also be invoked manually asnode dependencies.js. - 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.