👉 ⭐Star on GitHub — Let’s kill the magic, not your debugger.
"Minimal" Frameworks Are Lying to You
Let’s cut the crap: Hono and Elysia aren’t scalable web frameworks.
They’re great for toy projects, fast demos, and Hacker News points — but if you've ever tried to scale them in a production-grade app, you already know the pain:
- Middleware logic hidden in plugins
- Global state chaos
- Decorators breaking in silence
- Implicit routing behavior you can’t trace
They look clean on day one. By day thirty, it’s unmaintainable abstraction soup.
Tirne: Functional, Explicit, Built to Scale
Tirne is a Bun-native web framework designed by engineers who got burned by magic-driven toolchains.
We didn’t want to build a framework.
We wanted to stop debugging decorators at 2am.
Tirne takes inspiration from Go’s net/http
— where functions are first-class, structure is visible, and control is always yours.
// index.ts
import { createRouter, compose, json } from "tirne";
const logger = async (ctx, next) => {
console.log(`[${ctx.method}] ${ctx.url.pathname}`);
return await next();
};
const routes = [
{
method: "GET",
path: "/",
handler: compose([logger], () => json({ hello: "Tirne" })),
},
];
Bun.serve({ fetch: createRouter(routes) });
Why Tirne Exists (and Why You Might Need It)
- Because you shouldn’t need to read framework internals to understand a handler.
- Because Go-style error handling is safer than
try/catch
gymnastics. - Because routing and middleware should be functions, not chained spellbooks.
- Because “zero-boilerplate” should not mean “zero control.”
Tirne is what you'd get if Go had a baby with Bun — and it was raised by a backend engineer who hates decorators.
The Real Comparison: Tirne vs the Toys
Axis | Tirne ✨ | Hono 🌿 | Elysia 🧠 |
---|---|---|---|
Structure | ✅ Pure data & functions | ❌ Chained DSL | ❌ Magic macros |
Middleware | ✅ compose(fn[])
|
❌ Global spaghetti | ❌ Plugin jungle |
Type Safety | ⚪️ Composable & readable | ⚪️ Mediocre | 🔴 Too heavy, too brittle |
Runtime | ✅ Bun, Node, Workers, Deno | ✅ Mostly | ❌ Bun only, not portable |
Scaling | ✅ Predictable & explicit | ❌ Hidden side effects | ❌ Framework lock-in |
You want to scale? Start by making your architecture boring — and visible.
Philosophy: No Magic, Just Code
Tirne follows five unapologetic rules:
- If it can’t be written in 5 lines, it probably shouldn’t exist.
- Functions > frameworks. Always.
- Return errors. Don’t throw them.
- Run parallel like a grown-up.
- One file should be enough. Always.
You don’t need a framework. You need a structure. Tirne is that structure.
Install It. Use It. Judge It.
Bun
bun init
bun add tirne
touch index.ts
//index.ts
import { createRouter, json } from "tirne";
const routes = [
{ method: "GET", path: "/", handler: () => json({ msg: "Hello" }) },
];
Bun.serve({ fetch: createRouter(routes) });
bun run index.ts
Deno
touch index.ts deno.json
//index.ts
import { createRouter, json } from "https://deno.land/x/tirne@v1.0.2/mod.ts";
const routes = [
{
method: "GET",
path: "/",
handler: () => json({ message: "Hello from Deno + Tirne!" }),
},
];
const router = createRouter(routes);
console.log("Server is running on http://localhost:8000");
Deno.serve({ handler: router });
//deno.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["deno.ns", "deno.dom","dom"],
"strict": true
}
}
deno run --allow-net index.ts
Cloudflare Workers
npm install -g wrangler
wrangler init tirne-worker
cd tirne-worker
npm install tirne
// src/index.ts
import { createRouter, json } from "tirne";
const routes = [
{
method: "GET",
path: "/",
handler: () => json({ message: "Hello from Tirne on Cloudflare Workers!" }),
},
];
const router = createRouter(routes);
export default {
fetch: router,
};
wrangler dev
Node
npm init -y
npm install --save-dev ts-node typescript tirne
touch index.ts
//index.ts
import { createServer, IncomingMessage } from "http";
import { createRouter, json } from "tirne";
const routes = [
{
method: "GET",
path: "/",
handler: () => json({ message: "Hello from Node.js + Tirne!" }),
},
];
const router = createRouter(routes);
const server = createServer(async (req, res) => {
const url = `http://localhost:3000${req.url}`;
const headers = new Headers(
Object.entries(req.headers || {})
.filter(([_, value]) => typeof value === "string")
.map(([key, value]) => [key, value as string])
);
const request = new Request(url, {
method: req.method,
headers,
body: req.method !== "GET" && req.method !== "HEAD" ? await streamToBuffer(req) : undefined,
});
async function streamToBuffer(stream: IncomingMessage): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
const response = await router(request);
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
const body = await response.text(); //
res.end(body);
});
server.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
npx ts-node index.ts
That’s it. No macros. No CLI. No hidden behavior. It scales because it’s explicit.
👉 If this feels right to you — give it a ⭐ on GitHub.
🚀 Help Shape a Better Web Framework
If you believe code should be predictable, testable, and debuggable — not enchanted — give Tirne a ⭐ on GitHub.
No magic. No macros. Just functions that scale.
Your ⭐ helps spread a better way of building.
Let’s stop worshiping decorators. Let’s build systems we actually understand.
You don’t need more abstraction. You need less.
Use code. Not ceremony.
Top comments (19)
Gotta say, calling out hidden 'magic' in frameworks hits home for me - tired of dealing with stuff I can’t see coming.
Absolutely feel you — invisible behavior is the worst kind of complexity.
That moment when you're debugging something “magical” for hours… yeah.
Thanks for resonating with Tirne’s core idea 🙏
If Tirne feels like it’s speaking your language — would love a ⭐️
They help us know this path is worth walking 🙌 github.com/Tirne-ts/Tirne
pretty cool seeing someone finally say enough with all the magic stuff, i always end up wanting to rip out layers just to see what’s going on underneath honestly you think picking explicit tools actually helps projects last longer or do people just swap frameworks no matter what
Totally get that — you're not alone in wanting to tear through the layers just to understand your own app.
We felt exactly the same: frameworks started hiding more than helping.
That frustration is basically why Tirne exists in the first place.
As for longevity: we believe understandable code outlives shiny stacks.
People may still swap frameworks — but if the tool teaches good structure and control,
then even the exit is graceful.
Explicit tools leave fewer landmines for future devs.
If you believe frameworks should be readable, debuggable, and honest — give Tirne a ⭐️
Stars help us keep building tools for developers who value structure over magic.
Thank you for supporting explicit backends 🙌 give Tirne a → github.com/Tirne-ts/Tirne
Curious to hear from folks who've tried to build large-scale apps on Hono or Elysia.
Did you feel the same friction we did — implicit context, plugin chaos, decorator hell?
Tirne is our attempt to fix all that by going back to functions. Let me know your thoughts 👀
Thanks to @ben for the support and reactions 🙌
Really appreciate the feedback – if anyone has thoughts on framework scalability or DX, I’d love to hear it!
Also: ⭐ on GitHub if you’re into debuggable web stacks 😎
[github.com/Tirne-ts/Tirne]
I’ve shipped a production service on Hono and… yeah, middleware and global state turned into a silent nightmare after month one.
Tirne’s approach feels like finally having an adult conversation with your own code.
Exactly. That phrase — “silent nightmare” — is everything.
The worst part about magic is how quiet it is… until it's too late.
We built Tirne to make backend code something you can reason about again.
Welcome to the rebellion 🙌
keep it going!
Thanks!
Heads up: the above comment by Charles appears to be spam/phishing.
Do not click the link — stay safe, everyone!
I've reported it to Dev.to.
This approach feels super refreshing - love the functional routing. How well does Tirne handle stuff like auth or error propagation across routes?
Thank you! That’s exactly the kind of feedback we hoped for — Tirne’s routing is just plain functions because we want every piece to be inspectable, composable, and debuggable.
“Refreshing” is the highest compliment for something trying to be minimal 🙏
For auth, just wrap it as middleware — you can inject context or return early.
Tirne’s
compose(fn[])
makes layered logic super predictable.For errors: you can return
error()
anywhere, or use Result for full control.No
try/catch
gymnastics — just plain flows like Go.If you find this interesting, feel free to support us with a ⭐️ on GitHub!
github.com/Tirne-ts/Tirne