16 September 2026
Giving backend hooks a request to belong to
Building Uzeful around ordinary functions, typed request context, and state that belongs to one request, with adapters for Bun, Cloudflare Workers, and Express.
A route handler knows which request it is serving. The functions beneath it often need pieces of that same information: the environment, a database binding, the current user, or somewhere to keep a value until the request finishes.
Passing those dependencies explicitly works well. It becomes repetitive when several intermediate functions accept a context argument only to hand it to the next function. Putting the values in module-level variables removes the argument, but introduces a different problem: overlapping requests can see each other's state.
I built Uzeful around a request context that ordinary functions can read. The aim is to make small backend hooks practical without turning every dependency into an application-wide singleton.
A hook is an ordinary function
The uze prefix is a convention. There is no registration step, code generation, or component render cycle. A hook can call another hook, return a dependency, or read a value from the current request's environment.
Here is a complete Bun application:
import { BunUzefulApp } from "uzeful/bun";
const app = new BunUzefulApp<{ serviceName: string }>();
const uzeContext = app.hooks.uzeContext;
function uzeServiceName() {
return uzeContext().env.serviceName;
}
Bun.serve({
fetch: app.fetch(
() => Response.json({ service: uzeServiceName() }),
{ getEnv: () => ({ serviceName: "my-api" }) },
),
});app.fetch establishes the context before running the handler. The hook gets its environment type from the application instance, so serviceName is a string throughout that call chain.
The important restriction is where the hook runs. Calling it outside an active context throws. These functions depend on a request even though they do not take one as an argument.
Keep the value with the request
The implementation uses AsyncLocalStorage to carry the context through asynchronous work. The context holds the request, environment, state, and lifecycle helpers. A fresh request normally gets a fresh state object; supplying shared objects explicitly is still the application's responsibility.
A state key gives a hook a stable place to put its value. Define the key once at module scope, then access the value inside the request:
import { createStateKey, uzeRequestState } from "uzeful";
const EVENTS = createStateKey<string[]>("events", () => []);
function uzeEvents() {
const [getEvents] = uzeRequestState(EVENTS);
return getEvents();
}Two calls to uzeEvents() in the same request get the same array. A separate request gets another array. The default factory creates the value when the key is first read, rather than creating one array for the entire process.
The same pattern can hold a resolved user or a lazily constructed service. For asynchronous construction, storing the promise before awaiting it lets concurrent callers within that request share the same work. That is request-local reuse; it does not make the value a persistent or distributed cache.
Keep runtime details at the boundary
Uzeful has adapters for Bun, Cloudflare Workers, and Express. They translate their runtime's inputs into the shared application context, while hooks read the request and typed environment through the same interface.
That does not make every dependency portable. A hook that uses a D1 binding still needs D1. It does mean the surrounding application code can depend on a narrow hook instead of knowing how a Worker binding, Bun server, or Express request reached it.
The lifecycle follows the same approach. uzeBeforeResponse lets code register work that runs before the response is returned, such as setting a header. Background work uses the context's waitUntil helper, with handling supplied by the runtime adapter. The request handler, response processing, and background work have different lifetimes; the API needs to preserve those differences.
Make the hidden dependency testable
Removing an argument makes a call shorter, but it also makes a dependency less visible. I use the uze prefix to signal that a function needs a context, and keep context reads close to the code that actually needs them. Pure transformations can remain ordinary functions with explicit inputs.
The application's test method provides a context without starting an HTTP server. Using the application from the Bun example:
const serviceName = await app.test(
{ env: { serviceName: "test-api" } },
async () => uzeServiceName(),
);
console.assert(serviceName === "test-api");This exercises the same hook with a supplied environment. It is useful for domain code; it does not replace testing the runtime adapter or the real database integration. Hooks that need an actual request should be tested through the fetch handler with a Request.
Start with one useful hook
Uzeful is most useful where request context is already being passed through several layers. A current-user hook, a database accessor, or request-local state is enough to start. There is no need to move every function behind a hook.
Install it with npm install uzeful. The getting-started guide covers the first application, context and hooks explains dependency reuse and testing, and the adapter guide covers runtime setup. The source is on GitHub.