Skip to content

@tetsujs/request-id

@tetsujs/request-id gives every request an id, in the context and on the response, so the log lines and failure reports of one request can be joined. It is one beforeParse hook.

Terminal window
bun add @tetsujs/request-id
import { requestId } from "@tetsujs/request-id";
const id = requestId();
createApp({ hooks: { beforeParse: [id] }, routes });

Every response gets an x-request-id header, and ctx.requestId holds the id for the rest of the request. The records of @tetsujs/request-log carry it when this hook runs before them.

Mounted on the application, ctx.requestId is there at runtime but not in a route’s types: the application cannot type the routes it holds (see Context and its types). To have it typed in a handler, mount the hook on the route:

const id = requestId();
const list = route({
method: "GET"

The method this route answers.

Kept as a literal rather than widened to

Method

: the method is half of a route's identity, and an application that remembers its routes — for a generated client, for tooling — needs to know which one this is.

method
: "GET",
path: "/orders"

Route path with :param segments, e.g. "/orders/:id/cancel".

Must start with /, contain no empty segments and no trailing slash; a malformed literal is a compile error.

path
: "/orders",
hooks: { beforeParse: [id] },
handler: (ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
requestId: string;
}) => void

The endpoint logic; ctx is fully inferred, never annotate it.

The return type is inferred rather than demanded, and checked twice over. Against the route's own contract, by the

HandlerResult

bound on R: answering with something response never declared is a compile error that says so, instead of a structural diff against Response. And against what the framework can serialize at all, by the intersected

ValidateResult

: a stream handed over bare is refused whether or not the route declared anything, because that is the case no contract covers — without a response schema HandlerResult is unknown and accepts every value there is.

handler
: (
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
requestId: string;
}
ctx
) =>
const logger: {
info(fields: object, message: string): void;
}
logger
.info({ requestId:
ctx: {
readonly req: Request & {
readonly cookies?: Bun.CookieMap;
};
readonly server: Bun.Server<unknown>;
readonly out: Outgoing;
readonly route: RouteInfo;
readonly startedAt: number;
readonly params: {};
requestId: string;
}
ctx
.requestId }, "listing"),
requestId: string
});

Or declare it where it is read, with Requires<{ requestId: string }>, and the compiler checks that something provides it. An application hook mounted after requestId() sees it typed too.

Option Default
header "x-request-id" the header the id is written to, and read from when trusted
trustIncoming false use the id the client sent; an empty header counts as none
generate crypto.randomUUID how a new id is made
const id = requestId({
header?: string | undefined

The header the id is written to, and read from when trusted. Defaults to x-request-id.

header
: "x-correlation-id",
trustIncoming?: boolean | undefined

Whether an incoming header is trusted.

Off by default, and deliberately: an id from the outside is a value a client chose, so trusting it lets one client stamp another's log lines. Turn it on only behind a proxy that overwrites the header. An empty header counts as none, and the id is generated.

trustIncoming
: true });

Turn on trustIncoming only behind a proxy that overwrites the header. An incoming id is chosen by the client, and trusting it lets one client stamp another’s log lines.

  • Failures share the id when you pass reportError to createApp and log ctx?.requestId with the error. See Logging.
  • The package also exports the types RequestIdOptions and RequestIdHook. Type a hook with ReturnType<typeof requestId> rather than AnyHook, which erases both the slot and the requestId it adds to the context.

Code that never receives ctx, such as a repository several calls down, can read the id through AsyncLocalStorage, filled by a hook mounted after requestId(). Logging shows the hook and a pino mixin that puts the id on every line.