Awilixify: Making Middlewares End to End Type-Safe
Middleware is useful. Middleware is everywhere. But middleware is also where application flow often becomes implicit.
Awilixify: Making Middlewares End to End Type-Safe

Middleware is useful. Middleware is everywhere. But middleware is also where application flow often becomes implicit.
This article is about one Awilixify feature I care about a lot: type-safe pre-handlers. They let you move auth, tenant resolution, policy checks, and similar request-time logic out of framework middleware and into a typed application pipeline.
Middleware Makes Application Flow Implicit
Middleware is one of the most common patterns in Node.js applications, but it is also one of the easiest places to hide bugs.
Even before talking about type safety, middleware is fragile by nature. Order matters. One middleware mutates the request object. Another middleware assumes that mutation already happened. By the time the route handler runs, req contains fields that were added implicitly somewhere earlier in the middleware chain.
Business Logic Drifts Into Infrastructure
In many backend applications, HTTP middleware slowly becomes the place where too much application logic lives. Authentication goes there. Tenant resolution goes there. Permission checks go there. Sometimes even business validation starts there.
But these concepts are usually business concepts. Middleware is mainly an infrastructure/framework concept. When important checks live there, business logic becomes coupled to the HTTP framework and its request lifecycle.
Middleware Binds Logic To HTTP
But what happens when the same check is needed outside HTTP?
What if a queue consumer needs the same auth rules? What if a cron job needs the same tenant checks? What if a CLI command should reuse the same validation?
The more important logic you put into the HTTP layer, the harder it becomes to reuse that logic anywhere else. Tests also need to reproduce the same request setup, even when the behavior being tested is not really about HTTP.
TypeScript Does Not Know The Chain
And usually TypeScript does not help enough here. A middleware may attach req.user . Another middleware may attach req.tenant. The request handler sees only the final req object and assumes both fields exist.
It gets even trickier when execution is conditional. What if one route is public, another needs auth, and another needs auth plus tenant? You can wire different middleware chains per route, but it is still hard to make the final handler type reflect the exact chain that ran.
You can manually create a custom request type, but that only describes the shape you hope to receive. It does not prove which middleware ran, or whether it ran in the correct order.
All those problems Awilixify pre-handlers try to solve.
Why Build Another Middleware Layer?
With Awilixify, the idea is not to remove or don’t use HTTP middleware. Keep it for things that are truly HTTP or infrastracture concerns: rate limiting, CORS, compression, body parsing, low-level logging.
The idea is to have a separate, more controlled middleware chain for business logic.
The goal is:
- more control over which pre-handlers run
- type-safe context produced by those runs
- business rules outside the HTTP framework, not bound to
req - one entry point for a use case through the mediator
- the same behaviour available from HTTP, queues, cron jobs, CLI scripts, or tests
Passing Request Data Without Passing Request
You may ask: if we stop passing req everywhere, where does request data go?
Awilixify has executionContext for that.
It is immutable runtime input passed to mediator execution. It can contain things like token, tenant header, request id, locale, or anything else extracted from the current transport.
class UsersController {
constructor(private readonly queryMediator: Deps["queryMediator"]) {}
@GET("/users")
async getUser(req, res) {
const result = await this.queryMediator.execute(
"users/get", // name of usecase
{ id: req.params.id }, // payload of usecase
{
executionContext: {
token: req.headers.authorization,
tenantName: req.headers["x-tenant"],
},
},
);
return res.json(result);
}
}
The important rule is simple: Only pre-handlers read executionContext
Creating an Auth Pre-Handler
Now we can build the first pre-handler. It reads runtime data from executionContext and returns typed handlercontext :
import { Result, type Middleware, type MiddlewareContract } from "awilixify";
class UnauthorizedError extends Error {
readonly code = "unauthorized"
}
type ReturnType = Result<{ userId: string }, UnauthorizedError>;
type Contract = MiddlewareContract<typeof AuthMiddleware.key, ReturnType>;
class AuthMiddleware implements Middleware<Contract> {
static key = "auth";
declare readonly contract: Contract;
async execute(
_payload: unknown,
_context: Contract["context"],
executionContext: Contract["executionContext"],
): Promise<ReturnType> {
if (!executionContext.token) {
return Result.error(new UnauthorizedError());
}
// some usual jwt check can be here
return Result.ok({ userId: "u-1"});
}
}
The successful output of middleware is userId: string
Later will be shown how handler can use this data
Why Result Type?
You may ask: why return Result ? Why not just throw an error from a pre-handler?
Throwing works, but it hides important information from types.
If AuthMiddleware throws UnauthorizedError , TypeScript does not know that executing users/get may fail with UnauthorizedError . The error path exists, but it is invisible in the function signature.
Result makes that path explicit:
Result<{ userId: string }, UnauthorizedError>;
This says two things:
- success adds
{ userId: string }to the handler context - failure returns
UnauthorizedError
Awilixify can use both parts. The success type becomes handler context. The error type becomes part of the final mediator result.
It also keeps business logic transport-agnostic. UnauthorizedError is an application error. It can later be mapped to HTTP 401 in a controller, to a queue failure reason, or to a CLI message in a script.
The Handler Gets Clean Context
The handler does not need to know where the token came from. It does not need to know about Express request objects. It only receives the context produced by successful pre-handlers.
import { type Handler, type QueryContract, Result } from "awilixify";
class UserNotFoundError extends Error {
readonly code = 'user.not_found'
}
type Response = Result<{ id: string; role: string }, UserNotFoundError>;
class GetUserHandler implements Handler<GetUserHandler["contract"]> {
static readonly key = "users/get";
declare readonly contract: QueryContract<
typeof GetUserHandler.key,
{ id: string },
Response
>;
async executor(
payload: this["contract"]["payload"],
context: this["contract"]["context"],
): Promise<Response> {
// typed context: { userId: string }
const { userId } = context;
if (!payload.id) {
return Result.error(new UserNotFoundError());
}
return Result.ok({ id: payload.id, role: "admin" });
}
}
This is the E2E type-safe flow I wanted.
If
AuthMiddlewareis active for handler and the handler itself returnsResult<Success, UserNotFoundError>, the final mediator execute result in controller is inferred as:
Result<Success, UnauthorizedError | UserNotFoundError>
Registering Pre-Handlers in a Module
Pre-handlers are declared in a module:
// users.module.ts
import { createModule, type ModuleDef } from "awilixify";
type UsersModuleDef = ModuleDef<{
queryHandlers: [typeof GetUserHandler];
queryPreHandlers: {
auth: AuthMiddleware;
};
}>;
export const UsersModule = createModule<UsersModuleDef>({
name: "UsersModule",
queryHandlers: [GetUserHandler],
queryPreHandlers: {
auth: AuthMiddleware,
},
});
In this form, pre-handlers are module-scoped. They belong to UsersModule and participate in the mediator pipeline for query handlers in that module.
If a pre-handler is a shared application behavior, it can be exported from one module and imported or registered globally where other modules need it.
Scenarios: Explicit Per-Call Control
Sometimes you do not want the full pipeline.
Maybe one query requires auth. Another is public. Another needs additional policy checks.
In many frameworks this becomes decorators on controller methods:
@UseGuards(AuthGuard, PolicyGuard)
Awilixify takes a more explicit approach at the mediator call level.
Handlers can declare scenarios:
declare readonly contract: QueryContract<
typeof GetUserHandler.key,
{ id: string },
Response,
// all available for module pre-handlers to include
| { name: "default" }
// exclude "auth" pre-handler
| { name: "public"; exclulePreHandlerKeys: ["auth"] }
>;
Then the caller chooses:
// NotAuthorizedError will not be part of return type
// and auth pre-handler won't be executed
const result = await queryMediator.execute(
"users/get",
{ id: req.params.id },
{
scenario: "public",
excludePreHandlerKeys: ["auth"],
executionContext: {
token: req.headers.authorization,
},
},
);
The important part is that context and return type are recalculated for the scenario. That makes execution rules visible in code and checked by TypeScript.
To Sum Up
Awilixify pre-handlers give you a way to move important request-time logic out of framework middleware and into a type-safe application pipeline.
The controller passes raw runtime data through executionContext . Pre-handlers validate it, enrich it, and return typed context. Handlers receive that typed context without depending on an HTTP request object.
With Result and scenarios, the whole middleware flow becomes something you can trust: success context, failure types, and per-call pipeline changes are all visible to TypeScipt.
For me, that is the real value: the chain is no longer just runtime wiring. You can see what enters it, what each pre-handler adds, and what result controller recieves at the end.
Links
If this approach looks useful for you project, you can check Awilixify here:
- GitHub: https://github.com/wildstyles/awilixify
- Documentation: https://wildstyles.github.io/awilixify/
- Initial article about Awilixify: https://medium.com/@r.vanzhula/awilixify-nestjs-like-modular-di-for-legacy-applications-db2a1e29c7de
And if the article was helpful, please leave a clap. It helps more people find the project.
Thanks for reading!
메타데이터
- post_id
- b3f4dbfb6b42
- slug
- awilixify-making-middlewares-end-to-end-type-safe-b3f4dbfb6b42
- url
- https://medium.com/@r.vanzhula/awilixify-making-middlewares-end-to-end-type-safe-b3f4dbfb6b42
- canonical_url
- https://medium.com/@r.vanzhula/awilixify-making-middlewares-end-to-end-type-safe-b3f4dbfb6b42
- author_url
- https://medium.com/@r.vanzhula
- status
- ok
- fetched_at
- 2026-06-09 15:37:30