Not because middleware is bad.
But because I was using it for things it was never meant to guarantee.
The pattern we all write
app.use(authMiddleware)
app.get("/me", (c) => {
return c.json(c.get("user"))
})
This works. It’s simple. It’s familiar.
But it relies on an assumption:
“
userwill be there when I need it.”
And nothing actually enforces that.
Where things break
A few very normal mistakes:
- You forget to apply middleware to a route
- You register it in the wrong order
- You refactor something and break the chain
Everything still compiles. The app still runs.
The failure shows up later — usually when it matters.
Middleware isn’t the problem
Frameworks like Hono and Elysia do middleware really well:
- Hono keeps things minimal and close to Web standards
- Elysia pushes type safety further than most frameworks
And middleware itself is great for:
- logging
- compression
- request/response transformations
That’s exactly what it’s designed for.
The real issue
The problem is when we use middleware for something else:
data dependencies
When a handler depends on user, that dependency is implicit.
It’s not declared anywhere. It’s just assumed.
What I tried instead
Instead of replacing middleware, I separated concerns:
- Middleware → handles flow
- Relics → enforce what must exist
Defining a contract
const UserCtx = token<{ id: string }>("user")
const authRelic = relic(UserCtx, async (ctx) => {
const user = await verify(ctx.req)
if (!user) return err(Unauthorized)
return user
})
This says:
“I provide
UserCtx, or I fail.”
Using it in routes
app.scope("/user", authRelic, (r) => {
r.get("/me", (ctx) => {
return ctx.json(ctx.relic(UserCtx))
})
})
Now the handler doesn’t assume anything.
If it runs, the dependency is already satisfied.
What changed
Before:
“this should exist”
Now:
“this must exist”
And if it doesn’t, the app fails at startup.
Comparing approaches (in good faith)
| Concern | Middleware (Hono / Elysia) | Relics (Tomoe) |
|---|---|---|
| Flow control | Excellent | Not the goal |
| Cross-cutting concerns | Excellent | Not the goal |
| Data dependencies | Implicit | Explicit |
| Guarantees | None | Enforced |
| Failure timing | Runtime | Startup |
They’re not competing tools — they solve different problems.
Why this felt better
Most of my bugs weren’t about routing or performance.
They were about assumptions.
Middleware made those assumptions easy to write.
Relics made them explicit.
Final thought
I didn’t replace middleware.
I stopped asking it to do something it was never designed to do.
I’m building this idea into a small framework called Tomoe.
Still early, but I’d love feedback:

Top comments (0)