refactor(core): enhance HttpKernel pipeline and matcher system with full context and error handling
BREAKING CHANGE: `parseQuery` utility removed; `IRouteMatcher` now includes query parsing; `RouteBuilder.middleware` and `handle` are now strictly typed per builder instance.
- Add `isHandler` and `isMiddleware` runtime type guards for validation in `HttpKernel`.
- Introduce `createEmptyContext` for constructing default context objects.
- Support custom HTTP error handlers (`404`, `500`) via `IHttpKernelConfig.httpErrorHandlers`.
- Default error handlers return meaningful HTTP status text (e.g., "Not Found").
- Replace legacy `parseQuery` logic with integrated query extraction via `createRouteMatcher`.
- Strongly type `RouteBuilder.middleware()` and `.handle()` methods without generic overrides.
- Simplify `HttpKernel.handle()` and `executePipeline()` through precise control flow and validation.
- Remove deprecated `registerRoute.ts` and `HttpKernelConfig.ts` in favor of colocated type exports.
- Add tests for integrated query parsing in `createRouteMatcher`.
- Improve error handling tests: middleware/handler validation, double `next()` call, thrown exceptions.
- Replace `assertRejects` with plain response code checks (via updated error handling).
- Removed `parseQuery.ts` and all related tests — query parsing is now built into route matching.
- `IRouteMatcher` signature changed to return `{ params, query }` instead of only `params`.
- `HttpKernelConfig` now uses `DeepPartial` and includes `httpErrorHandlers`.
- `RouteBuilder`'s generics are simplified for better DX and improved type safety.
This refactor improves clarity, test coverage, and runtime safety of the request lifecycle while reducing boilerplate and eliminating duplicated query handling logic.
Signed-off-by: Max P. <Mail@MPassarello.de>
This commit is contained in:
@@ -21,3 +21,30 @@ export interface IHandler<TContext extends IContext = IContext> {
|
||||
*/
|
||||
(ctx: TContext): Promise<Response>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to determine whether a given value is a valid `IHandler` function.
|
||||
*
|
||||
* This function checks whether the input is a function and whether it returns
|
||||
* a `Promise<Response>` when called. Due to TypeScript's structural typing and
|
||||
* the lack of runtime type information, only minimal runtime validation is possible.
|
||||
*
|
||||
* @param value - The value to test.
|
||||
* @returns `true` if the value is a function that appears to conform to `IHandler`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const candidate = async (ctx: IContext) => new Response("ok");
|
||||
* if (isHandler(candidate)) {
|
||||
* // candidate is now typed as IHandler<IContext>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function isHandler<TContext extends IContext = IContext>(
|
||||
value: unknown,
|
||||
): value is IHandler<TContext> {
|
||||
return (
|
||||
typeof value === 'function' &&
|
||||
value.length === 1 // ctx
|
||||
);
|
||||
}
|
||||
|
||||
40
src/Interfaces/IHttpErrorHandlers.ts
Normal file
40
src/Interfaces/IHttpErrorHandlers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { IContext } from '../Interfaces/mod.ts';
|
||||
import { HttpErrorHandler, validHttpErrorCodes } from '../Types/mod.ts';
|
||||
|
||||
/**
|
||||
* A mapping of HTTP status codes to their corresponding error handlers.
|
||||
*
|
||||
* This interface defines required handlers for common critical status codes (404 and 500)
|
||||
* and allows optional handlers for all other known error codes defined in `validHttpErrorCodes`.
|
||||
*
|
||||
* This hybrid approach ensures predictable handling for key failure cases,
|
||||
* while remaining flexible for less common codes.
|
||||
*
|
||||
* @template TContext - The context type used in all error handlers.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const errorHandlers: IHttpErrorHandlers = {
|
||||
* 404: (ctx) => new Response("Not Found", { status: 404 }),
|
||||
* 500: (ctx, err) => {
|
||||
* console.error(err);
|
||||
* return new Response("Internal Server Error", { status: 500 });
|
||||
* },
|
||||
* 429: (ctx) => new Response("Too Many Requests", { status: 429 }),
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface IHttpErrorHandlers<TContext extends IContext = IContext>
|
||||
extends
|
||||
Partial<
|
||||
Record<
|
||||
Exclude<typeof validHttpErrorCodes[number], 404 | 500>,
|
||||
HttpErrorHandler<TContext>
|
||||
>
|
||||
> {
|
||||
/** Required error handler for HTTP 404 (Not Found). */
|
||||
404: HttpErrorHandler<TContext>;
|
||||
|
||||
/** Required error handler for HTTP 500 (Internal Server Error). */
|
||||
500: HttpErrorHandler<TContext>;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export interface IHttpKernel<TContext extends IContext = IContext> {
|
||||
* @param request - The incoming HTTP request to dispatch.
|
||||
* @returns A promise resolving to the final HTTP response.
|
||||
*/
|
||||
handle<_TContext extends IContext = TContext>(
|
||||
handle(
|
||||
request: Request,
|
||||
): Promise<Response>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ResponseDecorator } from '../Types/mod.ts';
|
||||
import { IContext } from './IContext.ts';
|
||||
import { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
|
||||
import { IRouteBuilderFactory } from './IRouteBuilder.ts';
|
||||
|
||||
export interface IHttpKernelConfig<TContext extends IContext = IContext> {
|
||||
decorateResponse: ResponseDecorator<TContext>;
|
||||
routeBuilderFactory: IRouteBuilderFactory;
|
||||
httpErrorHandlers: IHttpErrorHandlers<TContext>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpMethod } from '../Types/mod.ts';
|
||||
import { IHandler } from './IHandler.ts';
|
||||
import { IMiddleware } from './IMiddleware.ts';
|
||||
import { IContext } from './mod.ts';
|
||||
import { IContext, IRouteMatcher } from './mod.ts';
|
||||
|
||||
/**
|
||||
* Represents an internally registered route within the HttpKernel.
|
||||
@@ -27,10 +27,7 @@ export interface IInternalRoute<TContext extends IContext = IContext> {
|
||||
* @param req - The original Request object.
|
||||
* @returns An object with extracted path parameters, or `null` if not matched.
|
||||
*/
|
||||
matcher: (
|
||||
url: URL,
|
||||
req: Request,
|
||||
) => null | { params: Record<string, string> };
|
||||
matcher: IRouteMatcher;
|
||||
|
||||
/**
|
||||
* An ordered list of middleware functions to be executed before the handler.
|
||||
|
||||
@@ -23,3 +23,21 @@ export interface IMiddleware<TContext extends IContext = IContext> {
|
||||
*/
|
||||
(ctx: TContext, next: () => Promise<Response>): Promise<Response>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to verify whether a given value is a valid `IMiddleware` function.
|
||||
*
|
||||
* This guard checks whether the input is a function that accepts exactly two arguments.
|
||||
* Note: This is a structural check and cannot fully guarantee the semantics of a middleware.
|
||||
*
|
||||
* @param value - The value to test.
|
||||
* @returns `true` if the value is structurally a valid middleware function.
|
||||
*/
|
||||
export function isMiddleware<TContext extends IContext = IContext>(
|
||||
value: unknown,
|
||||
): value is IMiddleware<TContext> {
|
||||
return (
|
||||
typeof value === 'function' &&
|
||||
value.length === 2 // ctx, next
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ export interface IRouteBuilder<TContext extends IContext = IContext> {
|
||||
* @param mw - A middleware function.
|
||||
* @returns The route builder for further chaining.
|
||||
*/
|
||||
middleware<_TContext extends IContext = TContext>(
|
||||
mw: IMiddleware<_TContext>,
|
||||
): IRouteBuilder<_TContext>;
|
||||
middleware(
|
||||
mw: IMiddleware<TContext>,
|
||||
): IRouteBuilder<TContext>;
|
||||
|
||||
/**
|
||||
* Sets the final request handler for the route.
|
||||
@@ -34,7 +34,7 @@ export interface IRouteBuilder<TContext extends IContext = IContext> {
|
||||
*
|
||||
* @param handler - The function to execute when this route is matched.
|
||||
*/
|
||||
handle<_TContext extends IContext = TContext>(
|
||||
handler: IHandler<_TContext>,
|
||||
handle(
|
||||
handler: IHandler<TContext>,
|
||||
): void;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpMethod } from '../Types/mod.ts';
|
||||
import { HttpMethod, isHttpMethod } from '../Types/mod.ts';
|
||||
import { IRouteMatcher } from './IRouteMatcher.ts';
|
||||
|
||||
/**
|
||||
@@ -45,3 +45,47 @@ export interface IDynamicRouteDefinition {
|
||||
* or a dynamic route with a custom matcher function for advanced matching logic.
|
||||
*/
|
||||
export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition;
|
||||
|
||||
/**
|
||||
* Type guard to check whether a route definition is a valid static route definition.
|
||||
*
|
||||
* Ensures that the object:
|
||||
* - has a `method` property of type `HttpMethod`
|
||||
* - has a `path` property of type `string`
|
||||
* - does NOT have a `matcher` function (to avoid ambiguous mixed types)
|
||||
*/
|
||||
export function isStaticRouteDefinition(
|
||||
def: IRouteDefinition,
|
||||
): def is IStaticRouteDefinition {
|
||||
return (
|
||||
def &&
|
||||
typeof def === 'object' &&
|
||||
'method' in def &&
|
||||
isHttpMethod(def.method) &&
|
||||
'path' in def &&
|
||||
typeof (def as { path?: unknown }).path === 'string' &&
|
||||
!('matcher' in def)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check whether a route definition is a valid dynamic route definition.
|
||||
*
|
||||
* Ensures that the object:
|
||||
* - has a `method` property of type `HttpMethod`
|
||||
* - has a `matcher` property of type `function`
|
||||
* - does NOT have a `path` property (to avoid ambiguous mixed types)
|
||||
*/
|
||||
export function isDynamicRouteDefinition(
|
||||
def: IRouteDefinition,
|
||||
): def is IDynamicRouteDefinition {
|
||||
return (
|
||||
def &&
|
||||
typeof def === 'object' &&
|
||||
'method' in def &&
|
||||
isHttpMethod(def.method) &&
|
||||
'matcher' in def &&
|
||||
typeof (def as { matcher?: unknown }).matcher === 'function' &&
|
||||
!('path' in def)
|
||||
);
|
||||
}
|
||||
|
||||
6
src/Interfaces/IRouteMatch.ts
Normal file
6
src/Interfaces/IRouteMatch.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Params, Query } from '../Types/mod.ts';
|
||||
|
||||
export interface IRouteMatch {
|
||||
params?: Params;
|
||||
query?: Query;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Params } from '../Types/mod.ts';
|
||||
import { IRouteDefinition } from './IRouteDefinition.ts';
|
||||
import { IRouteMatch } from './IRouteMatch.ts';
|
||||
|
||||
/**
|
||||
* Defines a route matcher function that evaluates whether a route applies to a given request.
|
||||
@@ -14,7 +16,7 @@ export interface IRouteMatcher {
|
||||
* @param req - The raw Request object (may be used for context or headers).
|
||||
* @returns An object containing path parameters if matched, or `null` if not matched.
|
||||
*/
|
||||
(url: URL, req: Request): null | { params: Record<string, string> };
|
||||
(url: URL, req: Request): null | IRouteMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
43
src/Interfaces/__tests__/routeDefinitionGuards.test.ts
Normal file
43
src/Interfaces/__tests__/routeDefinitionGuards.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
|
||||
import {
|
||||
IRouteDefinition,
|
||||
isDynamicRouteDefinition,
|
||||
isStaticRouteDefinition,
|
||||
} from '../IRouteDefinition.ts';
|
||||
|
||||
Deno.test('isStaticRouteDefinition returns true for static route', () => {
|
||||
const staticDef: IRouteDefinition = {
|
||||
method: 'GET',
|
||||
path: '/users/:id',
|
||||
};
|
||||
|
||||
assertEquals(isStaticRouteDefinition(staticDef), true);
|
||||
assertEquals(isDynamicRouteDefinition(staticDef), false);
|
||||
});
|
||||
|
||||
Deno.test('isDynamicRouteDefinition returns true for dynamic route', () => {
|
||||
const dynamicDef: IRouteDefinition = {
|
||||
method: 'POST',
|
||||
matcher: (_url, _req) => ({ params: {} }),
|
||||
};
|
||||
|
||||
assertEquals(isDynamicRouteDefinition(dynamicDef), true);
|
||||
assertEquals(isStaticRouteDefinition(dynamicDef), false);
|
||||
});
|
||||
|
||||
Deno.test('isStaticRouteDefinition returns false for invalid object', () => {
|
||||
const invalidDef = {
|
||||
method: 'GET',
|
||||
} as unknown as IRouteDefinition;
|
||||
|
||||
assertEquals(isStaticRouteDefinition(invalidDef), false);
|
||||
});
|
||||
|
||||
Deno.test('isDynamicRouteDefinition returns false for object with no matcher', () => {
|
||||
const def = {
|
||||
method: 'DELETE',
|
||||
path: '/something',
|
||||
};
|
||||
|
||||
assertEquals(isDynamicRouteDefinition(def as IRouteDefinition), false);
|
||||
});
|
||||
@@ -1,15 +1,23 @@
|
||||
// deno-coverage-ignore-file
|
||||
|
||||
export type { IHttpKernelConfig } from './HttpKernelConfig.ts';
|
||||
export type { IContext } from './IContext.ts';
|
||||
export { isHandler } from './IHandler.ts';
|
||||
export type { IHandler } from './IHandler.ts';
|
||||
export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
|
||||
export type { IHttpKernel } from './IHttpKernel.ts';
|
||||
export type { IHttpKernelConfig } from './IHttpKernelConfig.ts';
|
||||
export type { IInternalRoute } from './IInternalRoute.ts';
|
||||
export { isMiddleware } from './IMiddleware.ts';
|
||||
export type { IMiddleware } from './IMiddleware.ts';
|
||||
export type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
|
||||
export {
|
||||
isDynamicRouteDefinition,
|
||||
isStaticRouteDefinition,
|
||||
} from './IRouteDefinition.ts';
|
||||
export type {
|
||||
IDynamicRouteDefinition,
|
||||
IRouteDefinition,
|
||||
IStaticRouteDefinition,
|
||||
} from './IRouteDefinition.ts';
|
||||
export type { IRouteMatch } from './IRouteMatch.ts';
|
||||
export type { IRouteMatcher, IRouteMatcherFactory } from './IRouteMatcher.ts';
|
||||
|
||||
Reference in New Issue
Block a user