EnriqueMark
Essays

Backend interaction

2026-02 English

I know the onion model: all the requests come in, and on the way out they go through part of it again.

Middleware, the way I understand it, is a bit like the @ decorator in Python. Everything goes through here before any function gets called. If some repeated operation has to run on every route access, that operation basically belongs in middleware.

About tokens

Usually called JWT. This thing is mainly for the backend to use, for something like auth. You can’t have the frontend send a request and go query the database every single time, that’s too slow and too much hassle, and the IO cost gets heavy.

So there’s the token mechanism:

  1. The frontend sends the token along with every request, and the backend uses it to check login state.
  2. If the check passes, the user doesn’t get bounced to the login page.
  3. A token usually carries some basic information, like: (a) user ID (b) permission info (c) the token’s expiry time

Generally you put the token check in middleware, verify permissions before each route transition, and confirm whether the user has the right to enter the page. And the other thing is the JSON response data, which is for the frontend (unless you want the frontend to go through the hassle of parsing the token every time). The frontend uses the data in there to decide whether to do the next render step and so on.

About handling passwords

Best to turn it into a hash on the backend, otherwise you run into an awkward security problem. If the hash is generated on the frontend, an attacker only has to edit the JS a bit and they can skip the validation logic, talk to the database, and log in, which makes the whole encryption pointless. So it’s better to handle all the security checks on the backend (the server side), because the backend runs on a server, and even if someone puts work into cracking it in the browser frontend, they still can’t change the backend’s logic. The backend treats the frontend on a zero-trust basis.

About storing the JWT token on the frontend

Usually two choices: drop it straight into LocalStorage, or wrap it up as a cookie. The backend has to handle its side to match. The reason the cookie gets its own category is that a cookie is more secure.

If the token just sits in LocalStorage, one line of JS reads all of it out. In that situation there’s no way to defend against XSS, one JS injection on the page side can strip every token clean, and a cookie avoids that.

And there’s an important difference here. The F12 console really can get the cookie directly, but that’s my top-level privilege as the browser’s user, and the other case is JS running on the page side, which is a completely different thing. For JS running on the page side, a cookie with HttpOnly set is simply out of reach, while with LocalStorage the page-side JS can get it directly. That’s a big difference.

About route paths

This follows relative-path logic (both the main route and the sub-routes).

Say the main route is /api. All /api requests get forwarded here, then forwarded on to a sub-route based on the dependency behind it. /api/xx gets forwarded to /xx.

How to write the sub-route: if you’re writing middleware for a sub-route and the middleware is already mounted on that sub-route, the path is just a *, and inside the sub-route module you write its relative path separately:

// dashboard.ts
const dashboardRoute = new Hono()

// 1. Protect "all" the paths under this sub-route
dashboardRoute.use('/*', jwtMiddleware) 

// 2. Define the specific paths (note you don't write /dashboard here anymore, only the relative path)
dashboardRoute.get('/profile', (c) => c.text('My profile'))
dashboardRoute.get('/settings', (c) => c.text('Settings'))

The way this star * works is actually kind of interesting. With /* you generally get something like a wildcard, and everything after that path matches. But a lone star means everything inside this route matches. The trap here is that on the sub-route you have to include the /*.

Then on the main route, a lone star means a global match under the main route. When you mount a route group, you have to write the sub-route’s path out in full so it attaches to the main route and doesn’t get mismatched:

// app is the main route, takes the sub-route's full path
app.route('/api', authRoute)
app.route('/api/dashboard', dashboardRoute)

About generating and debugging the API

There are pretty good libraries here that generate the docs in one shot. Once the endpoints are done, have the AI write the comments, then hook it into a tool like Swagger and the API docs come out automatically. You also get online debugging and auto-updates. Very handy. There’s another option, of course, which is to do it in one go and let the AI handle all of it. This tool does look optional today, but if you have some debugging needs, adding it is probably better. Or even the unit tests for debugging can go to the AI, that works fine too.


Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 后端交互.