DEV Community

Yuki Nishikawa
Yuki Nishikawa

Posted on • Edited on

Tirne: The Explicit, Go-Inspired Web Framework for Bun, Node,Deno and Workers

👉 ⭐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) });
Enter fullscreen mode Exit fullscreen mode

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:

  1. If it can’t be written in 5 lines, it probably shouldn’t exist.
  2. Functions > frameworks. Always.
  3. Return errors. Don’t throw them.
  4. Run parallel like a grown-up.
  5. 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
Enter fullscreen mode Exit fullscreen mode
//index.ts
import { createRouter, json } from "tirne";

const routes = [
  { method: "GET", path: "/", handler: () => json({ msg: "Hello" }) },
];

Bun.serve({ fetch: createRouter(routes) });
Enter fullscreen mode Exit fullscreen mode
bun run index.ts
Enter fullscreen mode Exit fullscreen mode

Deno

touch index.ts  deno.json
Enter fullscreen mode Exit fullscreen mode
//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 });
Enter fullscreen mode Exit fullscreen mode
//deno.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["deno.ns", "deno.dom","dom"],
    "strict": true
  }
}

Enter fullscreen mode Exit fullscreen mode
deno run --allow-net index.ts
Enter fullscreen mode Exit fullscreen mode

Cloudflare Workers

npm install -g wrangler
wrangler init tirne-worker
cd tirne-worker
npm install tirne
Enter fullscreen mode Exit fullscreen mode
// 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,
};
Enter fullscreen mode Exit fullscreen mode
wrangler dev
Enter fullscreen mode Exit fullscreen mode

Node

npm init -y
npm install --save-dev ts-node typescript tirne
touch index.ts
Enter fullscreen mode Exit fullscreen mode
//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");
});

Enter fullscreen mode Exit fullscreen mode
npx ts-node index.ts
Enter fullscreen mode Exit fullscreen mode

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.

👉 Tirne 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)

Collapse
 
nevodavid profile image
Nevo David

Gotta say, calling out hidden 'magic' in frameworks hits home for me - tired of dealing with stuff I can’t see coming.

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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 🙏

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

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

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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.

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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.

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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 👀

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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]

Collapse
 
afjsdkfdfkfds profile image
Roberto

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.

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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 🙌

Collapse
 
afjsdkfdfkfds profile image
Roberto

keep it going!

Thread Thread
 
yukinisihikawa profile image
Yuki Nishikawa

Thanks!

Collapse
 
Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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.

Collapse
 
dotallio profile image
Dotallio

This approach feels super refreshing - love the functional routing. How well does Tirne handle stuff like auth or error propagation across routes?

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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 🙏

Collapse
 
yukinisihikawa profile image
Yuki Nishikawa

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.

Thread Thread
 
yukinisihikawa profile image
Yuki Nishikawa

If you find this interesting, feel free to support us with a ⭐️ on GitHub! 
github.com/Tirne-ts/Tirne