Skip to content

Instantly share code, notes, and snippets.

@lxe
Last active November 8, 2025 23:04
Show Gist options
  • Save lxe/2f16634d84d1b28611f77fac574f732f to your computer and use it in GitHub Desktop.
Save lxe/2f16634d84d1b28611f77fac574f732f to your computer and use it in GitHub Desktop.
bun-middleware.ts
// TODO: somehow enforce ordering at compile time
type WriteOnly<T> = {
-readonly [K in keyof T]: T[K]; // TODO: Typescript can't block setters like this natively :)
};
type RouteHandler<T extends string> = ( // TODO: Extract from Bun... I think it has this built in
req: Bun.BunRequest<T>,
server: Bun.Server,
) => Response | Promise<Response>;
// Helper to extract the output context from a middleware
// Note: Handles Middleware<CtxOut>, Middleware<CtxOut, Route>, and Middleware<Deps, CtxOut, Route>
type MiddlewareOutput<M> =
M extends Middleware<infer First, infer Second, any>
? Second extends undefined
? First // Single param case: First is the output
: Second extends string
? First // Two param case with Route: First is the output, Second is the route
: Second // Three param case: Second is the output
: never;
// Helper to accumulate outputs from an array of middleware types
type AccumulateOutputs<T extends readonly any[]> = T extends readonly [infer First, ...infer Rest]
? MiddlewareOutput<First> & AccumulateOutputs<Rest>
: {};
// Middleware that mutates context in-place
// Single param: Middleware<CtxOut> - no dependencies, just output
// Two params: Middleware<CtxOut, Route> - output context with specific route
// Three params: Middleware<Deps, CtxOut, Route> - deps as array or type, plus output, with route
type Middleware<
DepsOrCtxOut extends readonly any[] | any = {},
CtxOutOrRoute extends Record<string, any> | string | undefined = undefined,
Route extends string = any
> = (
req: Bun.BunRequest<
CtxOutOrRoute extends undefined
? any // Single param case: any route
: CtxOutOrRoute extends string
? CtxOutOrRoute // Two param case: CtxOutOrRoute is the route
: Route // Three param case: Route is the route
>,
srv: Bun.Server,
ctx: (
CtxOutOrRoute extends undefined
? WriteOnly<DepsOrCtxOut> // Single param: DepsOrCtxOut is the output
: CtxOutOrRoute extends string
? WriteOnly<DepsOrCtxOut> // Two param case: DepsOrCtxOut is the output, CtxOutOrRoute is the route
: (DepsOrCtxOut extends readonly any[] ? AccumulateOutputs<DepsOrCtxOut> : DepsOrCtxOut) & WriteOnly<CtxOutOrRoute> // Three param case: DepsOrCtxOut is deps, CtxOutOrRoute is output
)
) => Response | Promise<Response> | void | Promise<void>;
// Helper to accumulate context types through middleware chain
// Uses MiddlewareOutput to handle both single and two-param forms
type AccumulateContext <
T extends readonly any[],
Acc = {}
> = T extends readonly [infer First, ...infer Rest]
? AccumulateContext<Rest, Acc & MiddlewareOutput<First>>
: Acc;
function withMiddleware <
Route extends string,
M extends readonly Middleware<any, any, Route>[]
>(
...middlewares: M
) {
return <R extends Route = Route>(
handler: (ctx: AccumulateContext<M>) => RouteHandler<R>
): RouteHandler<R> => {
return async (req, srv) => {
const ctx: any = {};
// Run middleware in order, short-circuit if any returns a Response
for (const middleware of middlewares) {
const result = await middleware(req, srv, ctx);
if (result instanceof Response) {
return result;
}
}
return handler(ctx)(req, srv);
};
};
}
@lxe
Copy link
Author

lxe commented Oct 2, 2025

I asked AI and it SOLVED the dependency ordering. Behold:

type WriteOnly<T> = {
  -readonly [K in keyof T]: T[K];
};

type RouteHandler<T extends string = any> = (
  req: Bun.BunRequest<T>,
  server: Bun.Server,
) => Response | Promise<Response>;

// Helper to convert union to intersection
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

// Helper to extract output type from middleware
type ExtractOutput<M> = M extends Middleware<any, infer Output, any> ? Output : never;

// Helper to convert array of middleware types to intersection of their outputs
type ExtractDepsContext<T extends readonly any[]> = UnionToIntersection<ExtractOutput<T[number]>>;

// Simplified Middleware type that takes dependencies and output
// TDeps can be either:
//   - An array of output types: [{ ip: string }, { startTime: number }]
//   - An array of middleware types: [typeof withIP, typeof withDuration]
type Middleware<
  TDeps extends readonly any[] = [],
  TOutput extends Record<string, any> = {},
  Route extends string = any
> = (
  req: Bun.BunRequest<Route>,
  srv: Bun.Server,
  ctx: WriteOnly<
    (TDeps[number] extends Middleware<any, any, any>
      ? ExtractDepsContext<TDeps>  // Dependencies are middleware types
      : UnionToIntersection<TDeps[number]>)  // Dependencies are output types
    & TOutput
  >
) => Response | Promise<Response> | void | Promise<void>;

// Overloaded withMiddleware signatures for compile-time ordering enforcement
function withMiddleware<Route extends string, C1 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>
): <R extends Route = Route>(
  handler: (ctx: C1) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>, C6 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>,
  m6: Middleware<[C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C6, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5 & C6) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>, C6 extends Record<string, any>, C7 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>,
  m6: Middleware<[C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C6, Route>,
  m7: Middleware<[C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C7, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5 & C6 & C7) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>, C6 extends Record<string, any>, C7 extends Record<string, any>, C8 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>,
  m6: Middleware<[C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C6, Route>,
  m7: Middleware<[C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C7, Route>,
  m8: Middleware<[C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C8, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>, C6 extends Record<string, any>, C7 extends Record<string, any>, C8 extends Record<string, any>, C9 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>,
  m6: Middleware<[C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C6, Route>,
  m7: Middleware<[C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C7, Route>,
  m8: Middleware<[C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C8, Route>,
  m9: Middleware<[C1, C2, C3, C4, C5, C6, C7, C8] | [C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C9, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 & C9) => RouteHandler<R>
) => RouteHandler<R>;

function withMiddleware<Route extends string, C1 extends Record<string, any>, C2 extends Record<string, any>, C3 extends Record<string, any>, C4 extends Record<string, any>, C5 extends Record<string, any>, C6 extends Record<string, any>, C7 extends Record<string, any>, C8 extends Record<string, any>, C9 extends Record<string, any>, C10 extends Record<string, any>>(
  m1: Middleware<[], C1, Route>,
  m2: Middleware<[C1], C2, Route>,
  m3: Middleware<[C1, C2] | [C1] | [], C3, Route>,
  m4: Middleware<[C1, C2, C3] | [C1, C2] | [C1] | [], C4, Route>,
  m5: Middleware<[C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C5, Route>,
  m6: Middleware<[C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C6, Route>,
  m7: Middleware<[C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C7, Route>,
  m8: Middleware<[C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C8, Route>,
  m9: Middleware<[C1, C2, C3, C4, C5, C6, C7, C8] | [C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C9, Route>,
  m10: Middleware<[C1, C2, C3, C4, C5, C6, C7, C8, C9] | [C1, C2, C3, C4, C5, C6, C7, C8] | [C1, C2, C3, C4, C5, C6, C7] | [C1, C2, C3, C4, C5, C6] | [C1, C2, C3, C4, C5] | [C1, C2, C3, C4] | [C1, C2, C3] | [C1, C2] | [C1] | [], C10, Route>
): <R extends Route = Route>(
  handler: (ctx: C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 & C9 & C10) => RouteHandler<R>
) => RouteHandler<R>;

// Implementation
function withMiddleware(...middlewares: Middleware<any, any, any>[]) {
  return (handler: (ctx: any) => RouteHandler<any>): RouteHandler<any> => {
    return async (req, srv) => {
      const ctx: Record<string, any> = {};
      
      // Run middleware in order, short-circuit if any returns a Response
      for (const middleware of middlewares) {
        const result = await middleware(req, srv, ctx);
        if (result instanceof Response) {
          return result;
        }
      }
      
      return handler(ctx)(req, srv);
    };
  };
}

export type { Middleware, RouteHandler, WriteOnly };
export { withMiddleware };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment