Skip to content
v0.5.2: sidecars and edge middleware

Two things in 0.5.2, and they have little in common, except that both started as annoyances.

Sidecars ​

Every backend eventually grows something that is not a request handler. A mail sender. A queue consumer. A cron runner.

This was needed in every project, for as long as I can remember. I just never named it. So everyone did the same: reached for a third-party builder, and built the extra service outside the framework.

A reasonable move, but a workaround for a missing piece: a folder that builds something, but does not serve anything.

Then came the big August refactor, and the idea finally crystallized. Not as a nice-to-have. As a must-have.

So, as of 0.5.2, sidecars are generally available: a source folder that declares an entry point to build, and nothing else. No base URL, no routes, no pages/, no api/.

create a sidecar, a mailer, say
sh
pnpm sidecar
added to src/ just like any source folder
txt
src/
  front/      React + Hono, base "/"
  mailer/     sidecar, no pages/ no api/
    entry.ts
    run.ts

Everything else just keeps working, because it is still a source folder. ~/ and @/ resolve as usual, typecheck covers it, and pnpm build mailer builds it on its own.

The service lives in entry.ts: how to start it, and how to stop it.

ts
// src/mailer/entry.ts
import { defineService } from "_/sidecar";

export default defineService({
  async start() {
    const server = await listen();
    return async () => {
      await server.close();
    };
  },
});

The seeded run.ts is the process around it, catching signals and shutting down cleanly. In development kosmo serve imports the entry and reloads it on every change. In production, your process manager runs the runner:

sh
node dist/mailer/sidecar/run.js

Not a new kind of folder. The same folder, finally allowed to not answer requests.

Edge middleware ​

The other one took a while to show up.

Validation runs before middleware. Usually that is what you want: by the time your code runs, the request is known to be well-formed.

For auth it is exactly wrong. A request with an expired token and a bad body answers 400, and auth, the one that would have answered 401, never gets a turn.

So now a slot whose name starts with edge: moves to the front of the route's chain, before validation. Same use(), same handler, nothing new to learn:

ts
// api/use.ts
export default [
  use(requireSession, {
    slot: "edge:auth",
  }),
];

Now an expired token gets 401, whatever the body looks like.

It is still a slot, so it can live in api/use.ts, in a cascading use.ts, or in a route, and a subtree can replace it by reusing the name.

One thing it is not: app middleware in api/app.ts still runs first, for every request, matched or not. Edge middleware starts at the route, and nothing reaches it unless a route matched.

More in the edge middleware docs.

Released under the MIT License.