Compare commits

10 Commits

Author SHA1 Message Date
6c4420d32f docs(httpkernel): enhance class and interface documentation
- Improve JSDoc comments for `HttpKernel` and `IHttpKernel` to clarify
  purpose, usage, and type parameters.
- Add detailed descriptions for methods, parameters, and generics.
- Refine explanations of middleware pipeline, error handling, and
  contextual typing.
- Enhance readability and consistency in public API documentation.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 16:45:37 +02:00
b7410b44dd 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>
2025-05-07 16:45:10 +02:00
9059bdda62 refactor(httpkernel): introduce configuration object for flexibility
- Replace individual constructor arguments with a configuration object.
- Add IHttpKernelConfig interface to standardize configuration structure.
- Refactor route builder and response decorator usage to use config.
- Simplify code and improve extensibility by consolidating parameters.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 13:56:56 +02:00
0990cacb22 chore(settings): add exportall configuration for barrel name and message
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 13:56:39 +02:00
fd1c7f4170 chore(.gitignore): add git_log_diff.txt to ignore list
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:42:16 +02:00
ba7aa79f56 feat(http): add error handling for invalid HTTP methods
- Introduce `InvalidHttpMethodError` for unrecognized HTTP methods.
- Enhance type safety in `HttpKernel` by using generic contexts.
- Update `ResponseDecorator` to accept context for enriched responses.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:35:41 +02:00
a236fa7c97 feat(http): enhance type safety and extend route context
- Refactor HttpKernel and related interfaces to support generic contexts.
- Add typed query parameters, route params, and state to IContext.
- Introduce HttpMethod type for stricter HTTP method validation.
- Update RouteBuilder and middleware to handle generic contexts.
- Improve test cases to verify compatibility with new types.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:29:49 +02:00
82a6877485 test(utils): rename and update import paths in test file
- Rename test file for better alignment with its purpose.
- Update relative import paths to reflect the new file location.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:29:10 +02:00
94525fce52 test(utils): add unit tests for parseQuery function
- Introduce comprehensive tests for the parseQuery utility.
- Validate handling of single and multi-value query parameters.
- Ensure empty query strings return an empty object.
- Confirm repeated keys are grouped into arrays.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:28:51 +02:00
cc734fa7b1 Update settings.json to include folderListener configuration for exportall
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 12:28:09 +02:00
43 changed files with 1322 additions and 290 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ logs/
cache/ cache/
out.py out.py
output.txt output.txt
git_log_diff.txt

View File

@@ -10,4 +10,12 @@
"editor.defaultFormatter": "denoland.vscode-deno", "editor.defaultFormatter": "denoland.vscode-deno",
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.indentSize": "tabSize", "editor.indentSize": "tabSize",
"exportall.config.folderListener": [
"/src/Interfaces",
"/src/Utils",
"/src/Types",
"/src/Errors"
],
"exportall.config.barrelName": "mod.ts",
"exportall.config.message": "deno-coverage-ignore-file",
} }

View File

@@ -0,0 +1,25 @@
/**
* Represents an error thrown when an incoming HTTP method
* is not among the recognized set of valid HTTP methods.
*
* This is typically used in routers or request dispatchers
* to enforce allowed methods and produce 405-like behavior.
*/
export class InvalidHttpMethodError extends Error {
/**
* The invalid method that triggered this error.
*/
public readonly method: unknown;
/**
* A fixed HTTP status code representing "Method Not Allowed".
*/
public readonly status: number = 405;
constructor(method: unknown) {
const label = typeof method === 'string' ? method : '[non-string]';
super(`Unsupported HTTP method: ${label}`);
this.name = 'InvalidHttpMethodError';
this.method = method;
}
}

3
src/Errors/mod.ts Normal file
View File

@@ -0,0 +1,3 @@
// deno-coverage-ignore-file
export { InvalidHttpMethodError } from './InvalidHttpMethodError.ts';

View File

@@ -2,55 +2,99 @@ import {
IContext, IContext,
IHandler, IHandler,
IHttpKernel, IHttpKernel,
IHttpKernelConfig,
IInternalRoute, IInternalRoute,
IMiddleware, IMiddleware,
IRouteBuilder, IRouteBuilder,
IRouteBuilderFactory,
IRouteDefinition, IRouteDefinition,
ResponseDecorator, isHandler,
isMiddleware,
} from './Interfaces/mod.ts'; } from './Interfaces/mod.ts';
import {
DeepPartial,
HTTP_404_NOT_FOUND,
HTTP_500_INTERNAL_SERVER_ERROR,
HttpStatusTextMap,
} from './Types/mod.ts';
import { RouteBuilder } from './RouteBuilder.ts'; import { RouteBuilder } from './RouteBuilder.ts';
import { createEmptyContext, normalizeError } from './Utils/mod.ts';
/** /**
* The central HTTP kernel responsible for managing route definitions, * The `HttpKernel` is the central routing engine that manages the full HTTP request lifecycle.
* executing middleware chains, and dispatching HTTP requests to their handlers.
* *
* This class supports a fluent API for route registration and allows the injection * It enables:
* of custom response decorators and route builder factories for maximum flexibility and testability. * - Dynamic and static route registration via a fluent API
* - Execution of typed middleware chains and final route handlers
* - Injection of response decorators and factory overrides
* - Fine-grained error handling via typed status-code-based handlers
*
* The kernel is designed with generics for flexible context typing, strong type safety,
* and a clear extension point for advanced routing, DI, or tracing logic.
*
* @typeParam TContext - The global context type used for all requests handled by this kernel.
*/ */
export class HttpKernel implements IHttpKernel { export class HttpKernel<TContext extends IContext = IContext>
/** implements IHttpKernel<TContext> {
* The list of internally registered routes, each with method, matcher, middleware, and handler. private cfg: IHttpKernelConfig<TContext>;
*/
private routes: IInternalRoute[] = [];
/** /**
* Creates a new instance of the `HttpKernel`. * The list of registered route definitions, including method, matcher,
* middleware pipeline, and final handler.
*/
private routes: IInternalRoute<TContext>[] = [];
/**
* Initializes the `HttpKernel` with optional configuration overrides.
* *
* @param decorateResponse - An optional response decorator function that is applied to all responses * Default components such as the route builder factory, response decorator,
* after the middleware/handler pipeline. Defaults to identity (no modification). * and 404/500 error handlers can be replaced by injecting a partial config.
* @param routeBuilderFactory - Optional factory for creating route builders. Defaults to using `RouteBuilder`. * Any omitted values fall back to sensible defaults.
*
* @param config - Partial kernel configuration. Missing fields are filled with defaults.
*/ */
public constructor( public constructor(
private readonly decorateResponse: ResponseDecorator = (res) => res, config?: DeepPartial<IHttpKernelConfig<TContext>>,
private readonly routeBuilderFactory: IRouteBuilderFactory = ) {
RouteBuilder, this.cfg = {
) {} decorateResponse: (res) => res,
routeBuilderFactory: RouteBuilder,
httpErrorHandlers: {
[HTTP_404_NOT_FOUND]: () =>
new Response(HttpStatusTextMap[HTTP_404_NOT_FOUND], {
status: HTTP_404_NOT_FOUND,
}),
[HTTP_500_INTERNAL_SERVER_ERROR]: () =>
new Response(
HttpStatusTextMap[HTTP_500_INTERNAL_SERVER_ERROR],
{
status: HTTP_500_INTERNAL_SERVER_ERROR,
},
),
...(config?.httpErrorHandlers ?? {}),
},
...config,
} as IHttpKernelConfig<TContext>;
/** this.handle = this.handle.bind(this);
* @inheritdoc this.registerRoute = this.registerRoute.bind(this);
*/
public route(definition: IRouteDefinition): IRouteBuilder {
return new this.routeBuilderFactory(
this.registerRoute.bind(this),
definition,
);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
public handle = async (request: Request): Promise<Response> => { public route<_TContext extends IContext = TContext>(
definition: IRouteDefinition,
): IRouteBuilder<_TContext> {
return new this.cfg.routeBuilderFactory(
this.registerRoute,
definition,
) as IRouteBuilder<_TContext>;
}
/**
* @inheritdoc
*/
public async handle(request: Request): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
const method = request.method.toUpperCase(); const method = request.method.toUpperCase();
@@ -58,11 +102,12 @@ export class HttpKernel implements IHttpKernel {
if (route.method !== method) continue; if (route.method !== method) continue;
const match = route.matcher(url, request); const match = route.matcher(url, request);
if (match) { if (match) {
const ctx: IContext = { const ctx: TContext = {
req: request, req: request,
params: match.params, params: match.params,
query: match.query,
state: {}, state: {},
}; } as TContext;
return await this.executePipeline( return await this.executePipeline(
ctx, ctx,
route.middlewares, route.middlewares,
@@ -71,51 +116,84 @@ export class HttpKernel implements IHttpKernel {
} }
} }
return new Response('Not Found', { status: 404 }); return this.cfg.httpErrorHandlers[HTTP_404_NOT_FOUND](
}; createEmptyContext<TContext>(request),
);
/**
* Registers a finalized route by pushing it into the internal route list.
*
* This method is typically called by the route builder after `.handle()` is invoked.
*
* @param route - The fully constructed route including matcher, middlewares, and handler.
*/
private registerRoute(route: IInternalRoute): void {
this.routes.push(route);
} }
/** /**
* Executes the middleware pipeline and final handler for a given request context. * Finalizes and registers a route within the kernel.
* *
* This function recursively invokes middleware in the order they were registered, * This method is invoked internally by the route builder once
* ending with the route's final handler. If a middleware returns a response directly * `.handle()` is called. It appends the route to the internal list.
* without calling `next()`, the pipeline is short-circuited.
* *
* The final response is passed through the `decorateResponse` function before being returned. * @param route - A fully constructed internal route object.
*/
private registerRoute<_TContext extends IContext = TContext>(
route: IInternalRoute<_TContext>,
): void {
this.routes.push(route as unknown as IInternalRoute<TContext>);
}
/**
* Executes the middleware and handler pipeline for a matched route.
* *
* @param ctx - The request context containing the request, parameters, and shared state. * This function:
* @param middleware - The ordered list of middleware to apply before the handler. * - Enforces linear middleware execution with `next()` tracking
* @param handler - The final request handler to invoke at the end of the pipeline. * - Validates middleware and handler types at runtime
* @returns The final HTTP response after middleware and decoration. * - Applies the optional response decorator post-processing
* - Handles all runtime errors via the configured 500 handler
*
* @param ctx - The active request context passed to middleware and handler.
* @param middleware - Ordered middleware functions for this route.
* @param handler - The final handler responsible for generating a response.
* @returns The final HTTP `Response`, possibly decorated.
*/ */
private async executePipeline( private async executePipeline(
ctx: IContext, ctx: TContext,
middleware: IMiddleware[], middleware: IMiddleware<TContext>[],
handler: IHandler, handler: IHandler<TContext>,
): Promise<Response> { ): Promise<Response> {
let i = -1; const handleInternalError = (ctx: TContext, err?: unknown) =>
const dispatch = async (index: number): Promise<Response> => { this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
if (index <= i) throw new Error('next() called multiple times'); ctx,
i = index; normalizeError(err),
const fn: IMiddleware | IHandler = index < middleware.length );
? middleware[index]
: handler; let lastIndex = -1;
if (!fn) return new Response('Internal error', { status: 500 });
return index < middleware.length const dispatch = async (currentIndex: number): Promise<Response> => {
? await fn(ctx, () => dispatch(index + 1)) if (currentIndex <= lastIndex) {
: await (fn as IHandler)(ctx); throw new Error('Middleware called `next()` multiple times');
}
lastIndex = currentIndex;
const isWithinMiddleware = currentIndex < middleware.length;
const fn = isWithinMiddleware ? middleware[currentIndex] : handler;
if (isWithinMiddleware) {
if (!isMiddleware(fn)) {
throw new Error(
'Expected middleware function, but received invalid value',
);
}
return await fn(ctx, () => dispatch(currentIndex + 1));
}
if (!isHandler(fn)) {
throw new Error(
'Expected request handler, but received invalid value',
);
}
return await fn(ctx);
}; };
return this.decorateResponse(await dispatch(0));
try {
const response = await dispatch(0);
return this.cfg.decorateResponse(response, ctx);
} catch (e) {
return handleInternalError(ctx, e);
}
} }
} }

View File

@@ -1,11 +1,22 @@
import { Params, Query, State } from '../Types/mod.ts';
/** /**
* Represents the per-request context passed through the middleware pipeline and to the final handler. * Represents the complete context for a single HTTP request,
* passed through the middleware pipeline and to the final route handler.
* *
* This context object encapsulates the original HTTP request, * This context object encapsulates all relevant runtime data for a request,
* the path parameters extracted from the matched route, * including the original request, path parameters, query parameters,
* and a mutable state object for sharing information across middlewares and handlers. * and a shared, mutable application state.
*
* @template TState Structured per-request state shared across middlewares and handlers.
* @template TParams Parsed URL path parameters, typically derived from route templates.
* @template TQuery Parsed query string parameters, preserving multi-value semantics.
*/ */
export interface IContext { export interface IContext<
TState extends State = State,
TParams extends Params = Params,
TQuery extends Query = Query,
> {
/** /**
* The original HTTP request object as received by Deno. * The original HTTP request object as received by Deno.
* Contains all standard fields like headers, method, body, etc. * Contains all standard fields like headers, method, body, etc.
@@ -18,14 +29,25 @@ export interface IContext {
* *
* These parameters are considered read-only and are set by the router. * These parameters are considered read-only and are set by the router.
*/ */
params: Record<string, string>; params: TParams;
/** /**
* A shared, mutable object used to pass arbitrary data between middlewares and handlers. * Query parameters extracted from the request URL's search string.
* *
* Use this field to attach validated user info, auth state, logging context, etc. * Values may occur multiple times (e.g., `?tag=ts&tag=deno`), and are therefore
* represented as either a string or an array of strings, depending on occurrence.
* *
* Each key should be well-named to avoid collisions across layers. * Use this field to access filters, flags, pagination info, or similar modifiers.
*/ */
state: Record<string, unknown>; query: TQuery;
/**
* A typed, mutable object used to pass structured data between middlewares and handlers.
*
* This object is ideal for sharing validated input, user identity, trace information,
* or other contextual state throughout the request lifecycle.
*
* Type-safe access to fields is ensured by the generic `TState` type.
*/
state: TState;
} }

View File

@@ -1,16 +1,50 @@
import { IContext } from './IContext.ts'; import { IContext } from './IContext.ts';
/** /**
* Represents a final request handler responsible for generating a response. * Represents a final request handler responsible for producing an HTTP response.
* *
* The handler is the last step in the middleware pipeline and must return * The handler is the terminal stage of the middleware pipeline and is responsible
* a valid HTTP `Response`. It has access to all data injected into the * for processing the incoming request and generating the final `Response`.
* request context, including path parameters and any state added by middleware. *
* It receives the fully-typed request context, which includes the original request,
* parsed route parameters, query parameters, and any shared state populated by prior middleware.
*
* @template TContext The specific context type for this handler, including typed `state`, `params`, and `query`.
*/ */
export interface IHandler { export interface IHandler<TContext extends IContext = IContext> {
/** /**
* @param ctx - The complete request context, including parameters and middleware state. * Handles the request and generates a response.
* @returns A promise resolving to an HTTP `Response`. *
* @param ctx - The complete request context, including request metadata, route and query parameters,
* and mutable state populated during the middleware phase.
* @returns A `Promise` resolving to an HTTP `Response` to be sent to the client.
*/ */
(ctx: IContext): Promise<Response>; (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
);
} }

View 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>;
}

View File

@@ -1,33 +1,49 @@
import { IContext } from './IContext.ts';
import { IRouteBuilder } from './IRouteBuilder.ts'; import { IRouteBuilder } from './IRouteBuilder.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import { IRouteDefinition } from './IRouteDefinition.ts';
/** /**
* Defines the core interface for the HTTP kernel, responsible for route registration, * The `IHttpKernel` interface defines the public API for a type-safe, middleware-driven HTTP dispatching system.
* middleware orchestration, and request dispatching. *
* Implementations of this interface are responsible for:
* - Registering routes with optional per-route context typing
* - Handling incoming requests by matching and dispatching to appropriate handlers
* - Managing the complete middleware pipeline and final response generation
*
* The kernel operates on a customizable `IContext` type to support strongly typed request parameters, state,
* and query values across the entire routing lifecycle.
*
* @typeParam TContext - The default context type used for all routes unless overridden per-route.
*/ */
export interface IHttpKernel { export interface IHttpKernel<TContext extends IContext = IContext> {
/** /**
* Registers a new route with a static path pattern or a dynamic matcher. * Registers a new HTTP route (static or dynamic) and returns a route builder for middleware/handler chaining.
* *
* This method accepts both conventional route definitions (with path templates) * This method supports contextual polymorphism via the `_TContext` type parameter, enabling fine-grained
* and advanced matcher-based routes for flexible URL structures. * typing of route-specific `params`, `query`, and `state` values. The route is not registered until
* `.handle()` is called on the returned builder.
* *
* Returns a route builder that allows chaining middleware and assigning a handler. * @typeParam _TContext - An optional override for the context type specific to this route.
* Falls back to the global `TContext` of the kernel if omitted.
* *
* @param definition - A static or dynamic route definition, including the HTTP method * @param definition - A route definition specifying the HTTP method and path or custom matcher.
* and either a path pattern or custom matcher function. * @returns A fluent builder interface to define middleware and attach a final handler.
* @returns A builder interface to attach middleware and define the handler.
*/ */
route(definition: IRouteDefinition): IRouteBuilder; route<_TContext extends IContext = TContext>(
definition: IRouteDefinition,
): IRouteBuilder<_TContext>;
/** /**
* Handles an incoming HTTP request by matching it against registered routes, * Handles an incoming HTTP request and produces a `Response`.
* executing any associated middleware in order, and invoking the final route handler.
* *
* This method serves as the main entry point to integrate with `Deno.serve`. * The kernel matches the request against all registered routes by method and matcher,
* constructs a typed context, and executes the middleware/handler pipeline.
* If no route matches, a 404 error handler is invoked.
*
* This method is designed to be passed directly to `Deno.serve()` or similar server frameworks.
* *
* @param request - The incoming HTTP request object. * @param request - The incoming HTTP request object.
* @returns A promise resolving to the final HTTP response. * @returns A `Promise` resolving to a complete HTTP response.
*/ */
handle(request: Request): Promise<Response>; handle(request: Request): Promise<Response>;
} }

View File

@@ -0,0 +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>;
}

View File

@@ -1,5 +1,7 @@
import { HttpMethod } from '../Types/mod.ts';
import { IHandler } from './IHandler.ts'; import { IHandler } from './IHandler.ts';
import { IMiddleware } from './IMiddleware.ts'; import { IMiddleware } from './IMiddleware.ts';
import { IContext, IRouteMatcher } from './mod.ts';
/** /**
* Represents an internally registered route within the HttpKernel. * Represents an internally registered route within the HttpKernel.
@@ -7,12 +9,12 @@ import { IMiddleware } from './IMiddleware.ts';
* Contains all data required to match an incoming request and dispatch it * Contains all data required to match an incoming request and dispatch it
* through the associated middleware chain and final handler. * through the associated middleware chain and final handler.
*/ */
export interface IInternalRoute { export interface IInternalRoute<TContext extends IContext = IContext> {
/** /**
* The HTTP method (e.g. 'GET', 'POST') that this route responds to. * The HTTP method (e.g. 'GET', 'POST') that this route responds to.
* The method should always be in uppercase. * The method should always be in uppercase.
*/ */
method: string; method: HttpMethod;
/** /**
* A matcher function used to determine whether this route matches a given request. * A matcher function used to determine whether this route matches a given request.
@@ -25,18 +27,15 @@ export interface IInternalRoute {
* @param req - The original Request object. * @param req - The original Request object.
* @returns An object with extracted path parameters, or `null` if not matched. * @returns An object with extracted path parameters, or `null` if not matched.
*/ */
matcher: ( matcher: IRouteMatcher;
url: URL,
req: Request,
) => null | { params: Record<string, string> };
/** /**
* An ordered list of middleware functions to be executed before the handler. * An ordered list of middleware functions to be executed before the handler.
*/ */
middlewares: IMiddleware[]; middlewares: IMiddleware<TContext>[];
/** /**
* The final handler that generates the HTTP response after all middleware has run. * The final handler that generates the HTTP response after all middleware has run.
*/ */
handler: IHandler; handler: IHandler<TContext>;
} }

View File

@@ -3,18 +3,41 @@ import { IContext } from './IContext.ts';
/** /**
* Represents a middleware function in the HTTP request pipeline. * Represents a middleware function in the HTTP request pipeline.
* *
* Middleware can perform tasks such as logging, authentication, validation, * Middleware is a core mechanism to intercept, observe, or modify the request lifecycle.
* or response transformation. It receives the current request context and * It can be used for tasks such as logging, authentication, input validation,
* a `next()` function to delegate control to the next middleware or final handler. * metrics collection, or response transformation.
* *
* To stop the request pipeline, a middleware can return a `Response` directly * Each middleware receives a fully-typed request context and a `next()` function
* without calling `next()`. * to invoke the next stage of the pipeline. Middleware may choose to short-circuit
* the pipeline by returning a `Response` early.
*
* @template TContext The specific context type for this middleware, including state, params, and query information.
*/ */
export interface IMiddleware { export interface IMiddleware<TContext extends IContext = IContext> {
/** /**
* @param ctx - The request context, containing the request, path parameters, and shared state. * Handles the request processing at this middleware stage.
* @param next - A function that continues the middleware pipeline. Returns the final `Response`. *
* @returns A promise resolving to an HTTP `Response`. * @param ctx - The full request context, containing request, params, query, and typed state.
* @param next - A continuation function that executes the next middleware or handler in the pipeline.
* @returns A `Promise` resolving to an HTTP `Response`, either from this middleware or downstream.
*/ */
(ctx: IContext, next: () => Promise<Response>): Promise<Response>; (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
);
} }

View File

@@ -2,20 +2,21 @@ import { IHandler } from './IHandler.ts';
import { IInternalRoute } from './IInternalRoute.ts'; import { IInternalRoute } from './IInternalRoute.ts';
import { IMiddleware } from './IMiddleware.ts'; import { IMiddleware } from './IMiddleware.ts';
import { IRouteDefinition } from './IRouteDefinition.ts'; import { IRouteDefinition } from './IRouteDefinition.ts';
import { IContext } from './mod.ts';
export interface IRouteBuilderFactory { export interface IRouteBuilderFactory<TContext extends IContext = IContext> {
new ( new (
registerRoute: (route: IInternalRoute) => void, registerRoute: (route: IInternalRoute<TContext>) => void,
def: IRouteDefinition, def: IRouteDefinition,
mws?: IMiddleware[], mws?: IMiddleware<TContext>[],
): IRouteBuilder; ): IRouteBuilder<TContext>;
} }
/** /**
* Provides a fluent API to build a single route configuration by chaining * Provides a fluent API to build a single route configuration by chaining
* middleware and setting the final request handler. * middleware and setting the final request handler.
*/ */
export interface IRouteBuilder { export interface IRouteBuilder<TContext extends IContext = IContext> {
/** /**
* Adds a middleware to the current route. * Adds a middleware to the current route.
* Middleware will be executed in the order of registration. * Middleware will be executed in the order of registration.
@@ -23,7 +24,9 @@ export interface IRouteBuilder {
* @param mw - A middleware function. * @param mw - A middleware function.
* @returns The route builder for further chaining. * @returns The route builder for further chaining.
*/ */
middleware(mw: IMiddleware): IRouteBuilder; middleware(
mw: IMiddleware<TContext>,
): IRouteBuilder<TContext>;
/** /**
* Sets the final request handler for the route. * Sets the final request handler for the route.
@@ -31,5 +34,7 @@ export interface IRouteBuilder {
* *
* @param handler - The function to execute when this route is matched. * @param handler - The function to execute when this route is matched.
*/ */
handle(handler: IHandler): void; handle(
handler: IHandler<TContext>,
): void;
} }

View File

@@ -1,3 +1,4 @@
import { HttpMethod, isHttpMethod } from '../Types/mod.ts';
import { IRouteMatcher } from './IRouteMatcher.ts'; import { IRouteMatcher } from './IRouteMatcher.ts';
/** /**
@@ -10,7 +11,7 @@ export interface IStaticRouteDefinition {
/** /**
* The HTTP method this route should match (e.g. "GET", "POST"). * The HTTP method this route should match (e.g. "GET", "POST").
*/ */
method: string; method: HttpMethod;
/** /**
* A static path pattern for the route, which may include named parameters * A static path pattern for the route, which may include named parameters
@@ -29,7 +30,7 @@ export interface IDynamicRouteDefinition {
/** /**
* The HTTP method this route should match (e.g. "GET", "POST"). * The HTTP method this route should match (e.g. "GET", "POST").
*/ */
method: string; method: HttpMethod;
/** /**
* A custom matcher function that receives the parsed URL and raw request. * A custom matcher function that receives the parsed URL and raw request.
@@ -44,3 +45,47 @@ export interface IDynamicRouteDefinition {
* or a dynamic route with a custom matcher function for advanced matching logic. * or a dynamic route with a custom matcher function for advanced matching logic.
*/ */
export type IRouteDefinition = IStaticRouteDefinition | IDynamicRouteDefinition; 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)
);
}

View File

@@ -0,0 +1,6 @@
import { Params, Query } from '../Types/mod.ts';
export interface IRouteMatch {
params?: Params;
query?: Query;
}

View File

@@ -1,4 +1,6 @@
import { Params } from '../Types/mod.ts';
import { IRouteDefinition } from './IRouteDefinition.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. * 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). * @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. * @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;
} }
/** /**

View 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);
});

View File

@@ -1,13 +1,23 @@
// deno-coverage-ignore-file
export type { IContext } from './IContext.ts'; export type { IContext } from './IContext.ts';
export type { IMiddleware } from './IMiddleware.ts'; export { isHandler } from './IHandler.ts';
export type { IHandler } from './IHandler.ts'; export type { IHandler } from './IHandler.ts';
export type { IHttpErrorHandlers } from './IHttpErrorHandlers.ts';
export type { IHttpKernel } from './IHttpKernel.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 type { IRouteBuilder, IRouteBuilderFactory } from './IRouteBuilder.ts';
export {
isDynamicRouteDefinition,
isStaticRouteDefinition,
} from './IRouteDefinition.ts';
export type { export type {
IDynamicRouteDefinition, IDynamicRouteDefinition,
IRouteDefinition, IRouteDefinition,
IStaticRouteDefinition, IStaticRouteDefinition,
} from './IRouteDefinition.ts'; } from './IRouteDefinition.ts';
export type { IInternalRoute } from './IInternalRoute.ts'; export type { IRouteMatch } from './IRouteMatch.ts';
export type { IRouteMatcher } from './IRouteMatcher.ts'; export type { IRouteMatcher, IRouteMatcherFactory } from './IRouteMatcher.ts';
export type { ResponseDecorator } from './ResponseDecorator.ts';

View File

@@ -1,12 +1,13 @@
import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts'; import { IRouteMatcherFactory } from './Interfaces/IRouteMatcher.ts';
import { import {
IContext,
IHandler, IHandler,
IInternalRoute,
IMiddleware, IMiddleware,
IRouteBuilder, IRouteBuilder,
IRouteDefinition, IRouteDefinition,
} from './Interfaces/mod.ts'; } from './Interfaces/mod.ts';
import { createRouteMatcher } from './Utils.ts'; import { RegisterRoute } from './Types/mod.ts';
import { createRouteMatcher } from './Utils/createRouteMatcher.ts';
/** /**
* Provides a fluent builder interface for defining a single route, * Provides a fluent builder interface for defining a single route,
@@ -14,7 +15,8 @@ import { createRouteMatcher } from './Utils.ts';
* *
* This builder is stateless and immutable; each chained call returns a new instance. * This builder is stateless and immutable; each chained call returns a new instance.
*/ */
export class RouteBuilder implements IRouteBuilder { export class RouteBuilder<TContext extends IContext = IContext>
implements IRouteBuilder<TContext> {
/** /**
* Constructs a new instance of the route builder. * Constructs a new instance of the route builder.
* *
@@ -23,9 +25,9 @@ export class RouteBuilder implements IRouteBuilder {
* @param mws - The list of middleware functions collected so far (default: empty). * @param mws - The list of middleware functions collected so far (default: empty).
*/ */
constructor( constructor(
private readonly registerRoute: (route: IInternalRoute) => void, private readonly registerRoute: RegisterRoute<TContext>,
private readonly def: IRouteDefinition, private readonly def: IRouteDefinition,
private readonly mws: IMiddleware[] = [], private readonly mws: IMiddleware<TContext>[] = [],
private readonly matcherFactory: IRouteMatcherFactory = private readonly matcherFactory: IRouteMatcherFactory =
createRouteMatcher, createRouteMatcher,
) {} ) {}
@@ -39,11 +41,14 @@ export class RouteBuilder implements IRouteBuilder {
* @param mw - A middleware function to be executed before the handler. * @param mw - A middleware function to be executed before the handler.
* @returns A new `RouteBuilder` instance for continued chaining. * @returns A new `RouteBuilder` instance for continued chaining.
*/ */
middleware(mw: IMiddleware): IRouteBuilder { middleware(
return new RouteBuilder(this.registerRoute, this.def, [ mw: IMiddleware<TContext>,
...this.mws, ): IRouteBuilder<TContext> {
mw, return new RouteBuilder<TContext>(
]); this.registerRoute,
this.def,
[...this.mws, mw],
);
} }
/** /**
@@ -54,13 +59,15 @@ export class RouteBuilder implements IRouteBuilder {
* *
* @param handler - The final request handler for this route. * @param handler - The final request handler for this route.
*/ */
handle(handler: IHandler): void { handle(
handler: IHandler<TContext>,
): void {
const matcher = this.matcherFactory(this.def); const matcher = this.matcherFactory(this.def);
this.registerRoute({ this.registerRoute({
method: this.def.method.toUpperCase(), method: this.def.method,
matcher, matcher,
middlewares: this.mws, middlewares: this.mws,
handler, handler: handler,
}); });
} }
} }

4
src/Types/DeepPartial.ts Normal file
View File

@@ -0,0 +1,4 @@
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]>
: T[P];
};

View File

@@ -0,0 +1,28 @@
import { IContext } from '../Interfaces/mod.ts';
/**
* Defines a handler function for errors that occur during the execution
* of middleware or route handlers within the HTTP kernel.
*
* This function receives both the request context and the thrown error,
* and is responsible for producing an appropriate HTTP `Response`.
*
* Typical use cases include:
* - Mapping known error types to specific HTTP status codes.
* - Generating structured error responses (e.g. JSON error payloads).
* - Logging errors centrally with request metadata.
*
* The handler may return the response synchronously or asynchronously.
*
* @template TContext - The specific request context type, allowing typed access to route parameters,
* query parameters, and per-request state when formatting error responses.
*
* @param context - The active request context at the time the error occurred.
* @param error - The exception or error that was thrown during request processing.
*
* @returns A `Response` object or a `Promise` resolving to one, to be sent to the client.
*/
export type HttpErrorHandler<TContext extends IContext = IContext> = (
context?: Partial<TContext>,
error?: Error,
) => Promise<Response> | Response;

52
src/Types/HttpMethod.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* A constant list of all supported HTTP methods according to RFC 7231 and RFC 5789.
*
* This array serves both as a runtime value list for validation
* and as the basis for deriving the `HttpMethod` union type.
*
* Note: The list is immutable and should not be modified at runtime.
*/
export const validHttpMethods = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
'OPTIONS',
] as const;
/**
* A union type representing all valid HTTP methods recognized by this application.
*
* This type is derived directly from the `validHttpMethods` constant,
* ensuring type safety and consistency between type system and runtime checks.
*
* Example:
* ```ts
* const method: HttpMethod = 'POST'; // ✅ valid
* const method: HttpMethod = 'FOO'; // ❌ Type error
* ```
*/
export type HttpMethod = typeof validHttpMethods[number];
/**
* Type guard to verify whether a given value is a valid HTTP method.
*
* This function checks both the type and content of the value
* and is suitable for runtime validation of inputs (e.g., from HTTP requests).
*
* Example:
* ```ts
* if (isHttpMethod(input)) {
* // input is now typed as HttpMethod
* }
* ```
*
* @param value - The value to test (typically a string from a request).
* @returns `true` if the value is a valid `HttpMethod`, otherwise `false`.
*/
export function isHttpMethod(value: unknown): value is HttpMethod {
return typeof value === 'string' &&
validHttpMethods.includes(value as HttpMethod);
}

189
src/Types/HttpStatusCode.ts Normal file
View File

@@ -0,0 +1,189 @@
// Informational responses
/** Indicates that the request was received and the client can continue. */
export const HTTP_100_CONTINUE = 100;
/** The server is switching protocols as requested by the client. */
export const HTTP_101_SWITCHING_PROTOCOLS = 101;
/** The server has received and is processing the request, but no response is available yet. */
export const HTTP_102_PROCESSING = 102;
// Successful responses
/** The request has succeeded. */
export const HTTP_200_OK = 200;
/** The request has succeeded and a new resource has been created as a result. */
export const HTTP_201_CREATED = 201;
/** The request has been accepted for processing, but the processing is not complete. */
export const HTTP_202_ACCEPTED = 202;
/** The server has successfully fulfilled the request and there is no content to send. */
export const HTTP_204_NO_CONTENT = 204;
// Redirection messages
/** The resource has been moved permanently to a new URI. */
export const HTTP_301_MOVED_PERMANENTLY = 301;
/** The resource resides temporarily under a different URI. */
export const HTTP_302_FOUND = 302;
/** Indicates that the resource has not been modified since the last request. */
export const HTTP_304_NOT_MODIFIED = 304;
// Client error responses
/** The server could not understand the request due to invalid syntax. */
export const HTTP_400_BAD_REQUEST = 400;
/** The request requires user authentication. */
export const HTTP_401_UNAUTHORIZED = 401;
/** The server understood the request but refuses to authorize it. */
export const HTTP_403_FORBIDDEN = 403;
/** The server cannot find the requested resource. */
export const HTTP_404_NOT_FOUND = 404;
/** The request method is known by the server but is not supported by the target resource. */
export const HTTP_405_METHOD_NOT_ALLOWED = 405;
/** The request could not be completed due to a conflict with the current state of the resource. */
export const HTTP_409_CONFLICT = 409;
/** The server understands the content type but was unable to process the contained instructions. */
export const HTTP_422_UNPROCESSABLE_ENTITY = 422;
/** The user has sent too many requests in a given amount of time. */
export const HTTP_429_TOO_MANY_REQUESTS = 429;
// Server error responses
/** The server encountered an unexpected condition that prevented it from fulfilling the request. */
export const HTTP_500_INTERNAL_SERVER_ERROR = 500;
/** The server does not support the functionality required to fulfill the request. */
export const HTTP_501_NOT_IMPLEMENTED = 501;
/** The server, while acting as a gateway or proxy, received an invalid response from the upstream server. */
export const HTTP_502_BAD_GATEWAY = 502;
/** The server is not ready to handle the request, often due to maintenance or overload. */
export const HTTP_503_SERVICE_UNAVAILABLE = 503;
/** The server is acting as a gateway and cannot get a response in time. */
export const HTTP_504_GATEWAY_TIMEOUT = 504;
/**
* A constant list of supported HTTP status codes used by this application.
*
* These constants are grouped by category and used to construct the union type `HttpStatusCode`.
*/
export const validHttpStatusCodes = [
// Informational
HTTP_100_CONTINUE,
HTTP_101_SWITCHING_PROTOCOLS,
HTTP_102_PROCESSING,
// Successful
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_202_ACCEPTED,
HTTP_204_NO_CONTENT,
// Redirection
HTTP_301_MOVED_PERMANENTLY,
HTTP_302_FOUND,
HTTP_304_NOT_MODIFIED,
// Client Errors
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_405_METHOD_NOT_ALLOWED,
HTTP_409_CONFLICT,
HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_429_TOO_MANY_REQUESTS,
// Server Errors
HTTP_500_INTERNAL_SERVER_ERROR,
HTTP_501_NOT_IMPLEMENTED,
HTTP_502_BAD_GATEWAY,
HTTP_503_SERVICE_UNAVAILABLE,
HTTP_504_GATEWAY_TIMEOUT,
] as const;
/**
* A constant list of HTTP error codes that are commonly used in the application.
*/
export const validHttpErrorCodes = [
// Client Errors
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_405_METHOD_NOT_ALLOWED,
HTTP_409_CONFLICT,
HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_429_TOO_MANY_REQUESTS,
// Server Errors
HTTP_500_INTERNAL_SERVER_ERROR,
HTTP_501_NOT_IMPLEMENTED,
HTTP_502_BAD_GATEWAY,
HTTP_503_SERVICE_UNAVAILABLE,
HTTP_504_GATEWAY_TIMEOUT,
] as const;
/**
* Maps each supported HTTP status code to its standard status message.
*
* Useful for logging, diagnostics, or building custom error responses.
*/
export const HttpStatusTextMap: Record<
typeof validHttpStatusCodes[number],
string
> = {
[HTTP_100_CONTINUE]: 'Continue',
[HTTP_101_SWITCHING_PROTOCOLS]: 'Switching Protocols',
[HTTP_102_PROCESSING]: 'Processing',
[HTTP_200_OK]: 'OK',
[HTTP_201_CREATED]: 'Created',
[HTTP_202_ACCEPTED]: 'Accepted',
[HTTP_204_NO_CONTENT]: 'No Content',
[HTTP_301_MOVED_PERMANENTLY]: 'Moved Permanently',
[HTTP_302_FOUND]: 'Found',
[HTTP_304_NOT_MODIFIED]: 'Not Modified',
[HTTP_400_BAD_REQUEST]: 'Bad Request',
[HTTP_401_UNAUTHORIZED]: 'Unauthorized',
[HTTP_403_FORBIDDEN]: 'Forbidden',
[HTTP_404_NOT_FOUND]: 'Not Found',
[HTTP_405_METHOD_NOT_ALLOWED]: 'Method Not Allowed',
[HTTP_409_CONFLICT]: 'Conflict',
[HTTP_422_UNPROCESSABLE_ENTITY]: 'Unprocessable Entity',
[HTTP_429_TOO_MANY_REQUESTS]: 'Too Many Requests',
[HTTP_500_INTERNAL_SERVER_ERROR]: 'Internal Server Error',
[HTTP_501_NOT_IMPLEMENTED]: 'Not Implemented',
[HTTP_502_BAD_GATEWAY]: 'Bad Gateway',
[HTTP_503_SERVICE_UNAVAILABLE]: 'Service Unavailable',
[HTTP_504_GATEWAY_TIMEOUT]: 'Gateway Timeout',
};
/**
* A union type representing commonly used HTTP status codes.
*
* This type ensures consistency between runtime and type-level status code handling.
*
* Example:
* ```ts
* const status: HttpStatusCode = 404; // ✅ valid
* const status: HttpStatusCode = 418; // ❌ Type error (unless added to list)
* ```
*/
export type HttpStatusCode = typeof validHttpStatusCodes[number];
/**
* Type guard to check whether a given value is a valid HTTP status code.
*
* This is useful for validating numeric values received from external input,
* ensuring they conform to known HTTP semantics.
*
* Example:
* ```ts
* if (isHttpStatusCode(value)) {
* // value is now typed as HttpStatusCode
* }
* ```
*
* @param value - The numeric value to check.
* @returns `true` if the value is a recognized HTTP status code, otherwise `false`.
*/
export function isHttpStatusCode(value: unknown): value is HttpStatusCode {
return typeof value === 'number' &&
validHttpStatusCodes.includes(value as HttpStatusCode);
}

10
src/Types/Params.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Represents route parameters parsed from dynamic segments in the URL path.
*
* This type is typically derived from route definitions with placeholders,
* such as `/users/:id`, which would yield `{ id: "123" }`.
*
* All values are strings and should be considered read-only, as they are
* extracted by the router and should not be modified by application code.
*/
export type Params = Record<string, string | undefined>;

12
src/Types/Query.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Represents the parsed query parameters from the request URL.
*
* Query parameters originate from the URL search string (e.g. `?filter=active&tags=ts&tags=deno`)
* and may contain single or multiple values per key.
*
* All values are expressed as strings or arrays of strings, depending on how often
* the key occurs. This structure preserves the raw semantics of the query.
*
* For normalized single-value access, prefer custom DTOs or wrapper utilities.
*/
export type Query = Record<string, string | string[]>;

View File

@@ -0,0 +1,16 @@
import { IContext } from '../Interfaces/IContext.ts';
import { IInternalRoute } from '../Interfaces/mod.ts';
/**
* A type alias for the internal route registration function used by the `HttpKernel`.
*
* This function accepts a fully constructed internal route, including method, matcher,
* middleware chain, and final handler, and registers it for dispatching.
*
* Typically passed into `RouteBuilder` instances to enable fluent API chaining.
*
* @template TContext The context type associated with the route being registered.
*/
export type RegisterRoute<TContext extends IContext = IContext> = (
route: IInternalRoute<TContext>,
) => void;

View File

@@ -1,3 +1,5 @@
import { IContext } from '../Interfaces/mod.ts';
/** /**
* A function that modifies or enriches an outgoing HTTP response before it is returned to the client. * A function that modifies or enriches an outgoing HTTP response before it is returned to the client.
* *
@@ -22,4 +24,7 @@
* }; * };
* ``` * ```
*/ */
export type ResponseDecorator = (res: Response) => Response; export type ResponseDecorator<TContext extends IContext = IContext> = (
res: Response,
ctx: TContext,
) => Response;

9
src/Types/State.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Represents the per-request state object shared across the middleware pipeline.
*
* This type defines the base structure for custom state definitions,
* which can be extended with concrete fields like user data, request metadata, etc.
*
* Custom `TState` types must extend this base to ensure compatibility.
*/
export type State = Record<string, unknown>;

View File

@@ -0,0 +1,40 @@
import { assertEquals } from 'https://deno.land/std/assert/mod.ts';
import { isHttpMethod, validHttpMethods } from '../HttpMethod.ts';
Deno.test('isHttpMethod: returns true for all valid methods', () => {
for (const method of validHttpMethods) {
const result = isHttpMethod(method);
assertEquals(result, true, `Expected "${method}" to be valid`);
}
});
Deno.test('isHttpMethod: returns false for lowercase or unknown strings', () => {
const invalid = [
'get',
'post',
'FETCH',
'TRACE',
'CONNECT',
'INVALID',
'',
' ',
];
for (const method of invalid) {
const result = isHttpMethod(method);
assertEquals(result, false, `Expected "${method}" to be invalid`);
}
});
Deno.test('isHttpMethod: returns false for non-string inputs', () => {
const invalidInputs = [null, undefined, 123, {}, [], true, Symbol('GET')];
for (const input of invalidInputs) {
const result = isHttpMethod(input);
assertEquals(
result,
false,
`Expected non-string input to be invalid: ${String(input)}`,
);
}
});

View File

@@ -0,0 +1,35 @@
// src/Types/__tests__/HttpStatusCode.test.ts
import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { isHttpStatusCode, validHttpStatusCodes } from '../HttpStatusCode.ts';
Deno.test('isHttpStatusCode: returns true for all valid status codes', () => {
for (const code of validHttpStatusCodes) {
assertEquals(
isHttpStatusCode(code),
true,
`Expected ${code} to be valid`,
);
}
});
Deno.test('isHttpStatusCode: returns false for invalid status codes', () => {
const invalidInputs = [99, 600, 1234, -1, 0, 999];
for (const val of invalidInputs) {
assertEquals(
isHttpStatusCode(val),
false,
`Expected ${val} to be invalid`,
);
}
});
Deno.test('isHttpStatusCode: returns false for non-numeric values', () => {
const invalid = ['200', null, undefined, {}, [], true];
for (const val of invalid) {
assertEquals(
isHttpStatusCode(val),
false,
`Expected ${val} to be invalid`,
);
}
});

41
src/Types/mod.ts Normal file
View File

@@ -0,0 +1,41 @@
// deno-coverage-ignore-file
export type { DeepPartial } from './DeepPartial.ts';
export type { HttpErrorHandler } from './HttpErrorHandler.ts';
export { isHttpMethod, validHttpMethods } from './HttpMethod.ts';
export type { HttpMethod } from './HttpMethod.ts';
export {
HTTP_100_CONTINUE,
HTTP_101_SWITCHING_PROTOCOLS,
HTTP_102_PROCESSING,
HTTP_200_OK,
HTTP_201_CREATED,
HTTP_202_ACCEPTED,
HTTP_204_NO_CONTENT,
HTTP_301_MOVED_PERMANENTLY,
HTTP_302_FOUND,
HTTP_304_NOT_MODIFIED,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
HTTP_405_METHOD_NOT_ALLOWED,
HTTP_409_CONFLICT,
HTTP_422_UNPROCESSABLE_ENTITY,
HTTP_429_TOO_MANY_REQUESTS,
HTTP_500_INTERNAL_SERVER_ERROR,
HTTP_501_NOT_IMPLEMENTED,
HTTP_502_BAD_GATEWAY,
HTTP_503_SERVICE_UNAVAILABLE,
HTTP_504_GATEWAY_TIMEOUT,
HttpStatusTextMap,
isHttpStatusCode,
validHttpErrorCodes,
validHttpStatusCodes,
} from './HttpStatusCode.ts';
export type { HttpStatusCode } from './HttpStatusCode.ts';
export type { Params } from './Params.ts';
export type { Query } from './Query.ts';
export type { RegisterRoute } from './RegisterRoute.ts';
export type { ResponseDecorator } from './ResponseDecorator.ts';
export type { State } from './State.ts';

View File

@@ -1,61 +0,0 @@
import { IRouteDefinition, IRouteMatcher } from './Interfaces/mod.ts';
/**
* Creates a matcher function from a given route definition.
*
* This utility supports both static path-based route definitions (e.g. `/users/:id`)
* and custom matcher functions for dynamic routing scenarios.
*
* ### Static Path Example
* For a definition like:
* ```ts
* { method: "GET", path: "/users/:id" }
* ```
* the returned matcher function will:
* - match requests to `/users/123`
* - extract `{ id: "123" }` as `params`
*
* ### Dynamic Matcher Example
* If the `IRouteDefinition` includes a `matcher` function, it will be used as-is.
*
* @param def - The route definition to convert into a matcher function.
* Can be static (`path`) or dynamic (`matcher`).
*
* @returns A matcher function that receives a `URL` and `Request` and returns:
* - `{ params: Record<string, string> }` if the route matches
* - `null` if the route does not match the request
*
* @example
* ```ts
* const matcher = createRouteMatcher({ method: "GET", path: "/repo/:owner/:name" });
* const result = matcher(new URL("http://localhost/repo/foo/bar"), req);
* // result: { params: { owner: "foo", name: "bar" } }
* ```
*/
export function createRouteMatcher(
def: IRouteDefinition,
): IRouteMatcher {
if ('matcher' in def) {
return def.matcher;
} else {
const pattern = def.path;
const keys: string[] = [];
const regex = new RegExp(
'^' +
pattern.replace(/:[^\/]+/g, (m) => {
keys.push(m.substring(1));
return '([^/]+)';
}) +
'$',
);
return (url: URL) => {
const match = url.pathname.match(regex);
if (!match) return null;
const params: Record<string, string> = {};
for (let i = 0; i < keys.length; i++) {
params[keys[i]] = decodeURIComponent(match[i + 1]);
}
return { params };
};
}
}

View File

@@ -0,0 +1,28 @@
import { assertEquals } from 'https://deno.land/std/assert/mod.ts';
import { createEmptyContext } from '../createEmptyContext.ts';
import { IContext } from '../../Interfaces/mod.ts';
Deno.test('createEmptyContext: returns default-initialized context', () => {
const request = new Request('http://localhost');
const ctx = createEmptyContext(request);
assertEquals(ctx.req, request);
assertEquals(ctx.params, {});
assertEquals(ctx.query, {});
assertEquals(ctx.state, {});
});
Deno.test('createEmptyContext: preserves generic type compatibility', () => {
interface MyContext
extends
IContext<{ userId: string }, { id: string }, { verbose: string }> {}
const req = new Request('http://localhost');
const ctx = createEmptyContext<MyContext>(req);
// All properties exist and are empty
assertEquals(ctx.params, {} as MyContext['params']);
assertEquals(ctx.query, {} as MyContext['query']);
assertEquals(ctx.state, {} as MyContext['state']);
assertEquals(ctx.req, req);
});

View File

@@ -0,0 +1,118 @@
import {
assert,
assertEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../../Interfaces/mod.ts';
import { createRouteMatcher } from '../../mod.ts';
// Dummy request
const dummyRequest = new Request('http://localhost');
Deno.test('createRouteMatcher: static route matches and extracts params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/users/42'), dummyRequest);
assert(result);
assertEquals(result.params, { id: '42' });
});
Deno.test('createRouteMatcher: static route with multiple params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' };
const matcher = createRouteMatcher(def);
const result = matcher(
new URL('http://localhost/repo/max/wiki'),
dummyRequest,
);
assert(result);
assertEquals(result.params, { owner: 'max', name: 'wiki' });
});
Deno.test('createRouteMatcher: static route does not match wrong path', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/posts/42'), dummyRequest);
assertStrictEquals(result, null);
});
Deno.test('createRouteMatcher: uses custom matcher if provided', () => {
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/ping' ? { params: {} } : null,
};
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/ping'), dummyRequest);
assert(result);
assertEquals(result.params, {});
});
Deno.test('createRouteMatcher: extracts single query param', () => {
const def: IRouteDefinition = { method: 'GET', path: '/search' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/search?q=deno');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {}); // no path params
assertEquals(result.query, { q: 'deno' }); // single key → string
});
Deno.test('createRouteMatcher: duplicate query keys become array', () => {
const def: IRouteDefinition = { method: 'GET', path: '/tags' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/tags?tag=js&tag=ts&tag=deno');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {});
assertEquals(result.query, { tag: ['js', 'ts', 'deno'] }); // multi → string[]
});
Deno.test('createRouteMatcher: mix of single and duplicate keys', () => {
const def: IRouteDefinition = { method: 'GET', path: '/filter/:type' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/filter/repo?lang=ts&lang=js&page=2');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, { type: 'repo' });
assertEquals(result.query, {
lang: ['ts', 'js'], // duplicated
page: '2', // single
});
});
Deno.test('createRouteMatcher: no query parameters returns empty object', () => {
const def: IRouteDefinition = { method: 'GET', path: '/info' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/info');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {});
assertEquals(result.query, {}); // empty
});
Deno.test('createRouteMatcher: retains array order of duplicate keys', () => {
const def: IRouteDefinition = { method: 'GET', path: '/order' };
const matcher = createRouteMatcher(def);
const url = new URL(
'http://localhost/order?item=first&item=second&item=third',
);
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.query?.item, ['first', 'second', 'third']);
});

View File

@@ -0,0 +1,35 @@
import {
assertEquals,
assertInstanceOf,
} from 'https://deno.land/std/assert/mod.ts';
import { normalizeError } from '../normalizeError.ts';
Deno.test('normalizeError: preserves Error instances', () => {
const original = new Error('original');
const result = normalizeError(original);
assertInstanceOf(result, Error);
assertEquals(result, original);
});
Deno.test('normalizeError: converts string to Error', () => {
const result = normalizeError('something went wrong');
assertInstanceOf(result, Error);
assertEquals(result.message, 'something went wrong');
});
Deno.test('normalizeError: converts number to Error', () => {
const result = normalizeError(404);
assertInstanceOf(result, Error);
assertEquals(result.message, '404');
});
Deno.test('normalizeError: converts plain object to Error', () => {
const input = { error: true, msg: 'Invalid' };
const result = normalizeError(input);
assertInstanceOf(result, Error);
assertEquals(result.message, JSON.stringify(input));
});

View File

@@ -0,0 +1,30 @@
import { IContext } from '../Interfaces/mod.ts';
import { Params, Query, State } from '../Types/mod.ts';
/**
* Creates an empty request context suitable for fallback handlers (e.g., 404 or 500 errors).
*
* This function is primarily intended for cases where no route matched, but a context-compatible
* object is still needed to invoke a generic error handler. All context fields are initialized
* to their default empty values (`{}` for params, query, and state).
*
* @template TContext - The expected context type, typically extending `IContext`.
* @param req - The original HTTP request object from `Deno.serve()`.
* @returns A minimal context object compatible with `TContext`.
*
* @example
* ```ts
* const ctx = createEmptyContext<MyContext>(request);
* return httpErrorHandlers[404](ctx);
* ```
*/
export function createEmptyContext<TContext extends IContext = IContext>(
req: Request,
): TContext {
return {
req,
params: {} as Params,
query: {} as Query,
state: {} as State,
} as TContext;
}

View File

@@ -0,0 +1,52 @@
// createRouteMatcher.ts
import {
IRouteDefinition,
IRouteMatch,
IRouteMatcher,
isDynamicRouteDefinition,
} from '../Interfaces/mod.ts';
import { Params, Query } from '../Types/mod.ts';
/**
* Transforms a route definition into a matcher using Deno's URLPattern API.
*
* @param def - Static path pattern or custom matcher.
* @returns IRouteMatcher that returns `{ params, query }` or `null`.
*/
export function createRouteMatcher(
def: IRouteDefinition,
): IRouteMatcher {
// 1. Allow users to provide their own matcher
if (isDynamicRouteDefinition(def)) {
return def.matcher;
}
// 2. Build URLPattern; supports :id, *wildcards, regex groups, etc.
const pattern = new URLPattern({ pathname: def.path });
// 3. The actual matcher closure
return (url: URL): IRouteMatch | null => {
const result = pattern.exec(url);
// 3a. Path did not match
if (!result) return null;
// 3b. Extract route params
const params: Params = {};
for (const [key, value] of Object.entries(result.pathname.groups)) {
params[key] = value;
}
// 3c. Extract query parameters – keep duplicates as arrays
const query: Query = {};
for (const key of url.searchParams.keys()) {
const values = url.searchParams.getAll(key); // → string[]
query[key] = values.length === 1
? values[0] // single → "foo"
: values; // multi → ["foo","bar"]
}
return { params, query };
};
}

5
src/Utils/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
// deno-coverage-ignore-file
export { createEmptyContext } from './createEmptyContext.ts';
export { createRouteMatcher } from './createRouteMatcher.ts';
export { normalizeError } from './normalizeError.ts';

View File

@@ -0,0 +1,32 @@
/**
* Normalizes any thrown value to a proper `Error` instance.
*
* This is useful when handling unknown thrown values that may be:
* - strings (e.g. `throw "oops"`)
* - numbers (e.g. `throw 404`)
* - objects that are not instances of `Error`
*
* Ensures that downstream error handling logic always receives a consistent `Error` object.
*
* @param unknownError - Any value that might have been thrown.
* @returns A valid `Error` instance wrapping the original input.
*
* @example
* ```ts
* try {
* throw "something went wrong";
* } catch (e) {
* const err = normalizeError(e);
* console.error(err.message); // "something went wrong"
* }
* ```
*/
export function normalizeError(unknownError: unknown): Error {
return unknownError instanceof Error
? unknownError
: new Error(
typeof unknownError === 'string'
? unknownError
: JSON.stringify(unknownError),
);
}

View File

@@ -1,7 +1,4 @@
import { import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
assertEquals,
assertRejects,
} from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts'; import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts'; import { IRouteDefinition } from '../Interfaces/mod.ts';
@@ -11,7 +8,7 @@ Deno.test('HttpKernel: matches static route and executes handler', async () => {
const def: IRouteDefinition = { method: 'GET', path: '/hello' }; const def: IRouteDefinition = { method: 'GET', path: '/hello' };
let called = false; let called = false;
kernel.route(def).handle(() => { kernel.route(def).handle((_ctx) => {
called = true; called = true;
return Promise.resolve(new Response('OK', { status: 200 })); return Promise.resolve(new Response('OK', { status: 200 }));
}); });
@@ -31,7 +28,7 @@ Deno.test('HttpKernel: supports dynamic matcher', async () => {
matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null, matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null,
}; };
kernel.route(def).handle(() => kernel.route(def).handle((_ctx) =>
Promise.resolve(new Response('Dyn', { status: 200 })) Promise.resolve(new Response('Dyn', { status: 200 }))
); );
@@ -45,15 +42,15 @@ Deno.test('HttpKernel: calls middleware in order and passes to handler', async (
const calls: string[] = []; const calls: string[] = [];
kernel.route({ method: 'GET', path: '/test' }) kernel.route({ method: 'GET', path: '/test' })
.middleware(async (ctx, next) => { .middleware(async (_ctx, next) => {
calls.push('mw1'); calls.push('mw1');
return await next(); return await next();
}) })
.middleware(async (ctx, next) => { .middleware(async (_ctx, next) => {
calls.push('mw2'); calls.push('mw2');
return await next(); return await next();
}) })
.handle(() => { .handle((_ctx) => {
calls.push('handler'); calls.push('handler');
return Promise.resolve(new Response('done')); return Promise.resolve(new Response('done'));
}); });
@@ -70,15 +67,15 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
const calls: string[] = []; const calls: string[] = [];
kernel.route({ method: 'GET', path: '/stop' }) kernel.route({ method: 'GET', path: '/stop' })
.middleware(() => { .middleware((_ctx, _next) => {
calls.push('mw1'); calls.push('mw1');
return Promise.resolve(new Response('blocked', { status: 403 })); return Promise.resolve(new Response('blocked', { status: 403 }));
}) })
.middleware(() => { .middleware((_ctx, _next) => {
calls.push('mw2'); calls.push('mw2');
return Promise.resolve(new Response('should-not-call')); return Promise.resolve(new Response('should-not-call'));
}) })
.handle(() => { .handle((_ctx) => {
calls.push('handler'); calls.push('handler');
return Promise.resolve(new Response('ok')); return Promise.resolve(new Response('ok'));
}); });
@@ -91,6 +88,32 @@ Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
assertEquals(calls, ['mw1']); assertEquals(calls, ['mw1']);
}); });
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => {
const kernel = new HttpKernel();
// Middleware with wrong signature (missing ctx, next)
kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware
.middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok')));
const res1 = await kernel.handle(new Request('http://localhost/bad-mw'));
assertEquals(res1.status, 500);
assertEquals(await res1.text(), 'Internal Server Error');
// Handler with wrong signature (no ctx)
kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler
.handle(() => new Response('invalid'));
const res2 = await kernel.handle(
new Request('http://localhost/bad-handler'),
);
assertEquals(res2.status, 500);
assertEquals(await res2.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: 404 for unmatched route', async () => { Deno.test('HttpKernel: 404 for unmatched route', async () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
const res = await kernel.handle(new Request('http://localhost/nothing')); const res = await kernel.handle(new Request('http://localhost/nothing'));
@@ -113,18 +136,16 @@ Deno.test('HttpKernel: throws on next() called twice', async () => {
const kernel = new HttpKernel(); const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/bad' }) kernel.route({ method: 'GET', path: '/bad' })
.middleware(async (ctx, next) => { .middleware(async (_ctx, next) => {
await next(); await next();
await next(); // ❌ await next(); // ❌
return new Response('should never reach'); return new Response('should never reach');
}) })
.handle(() => Promise.resolve(new Response('OK'))); .handle((_ctx) => Promise.resolve(new Response('OK')));
await assertRejects( const res = await kernel.handle(new Request('http://localhost/bad'));
() => kernel.handle(new Request('http://localhost/bad')), assertEquals(res.status, 500);
Error, assertEquals(await res.text(), 'Internal Server Error');
'next() called multiple times',
);
}); });
Deno.test('HttpKernel: handler throws → error propagates', async () => { Deno.test('HttpKernel: handler throws → error propagates', async () => {
@@ -135,11 +156,9 @@ Deno.test('HttpKernel: handler throws → error propagates', async () => {
throw new Error('fail!'); throw new Error('fail!');
}); });
await assertRejects( const res = await kernel.handle(new Request('http://localhost/throw'));
() => kernel.handle(new Request('http://localhost/throw')), assertEquals(res.status, 500);
Error, assertEquals(await res.text(), 'Internal Server Error');
'fail!',
);
}); });
Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => { Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => {
@@ -157,5 +176,5 @@ Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async (
const res = await kernel.handle(new Request('http://localhost/fail')); const res = await kernel.handle(new Request('http://localhost/fail'));
assertEquals(res.status, 500); assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal error'); assertEquals(await res.text(), 'Internal Server Error');
}); });

View File

@@ -15,7 +15,7 @@ import { RouteBuilder } from '../mod.ts';
// Dummy objects // Dummy objects
const dummyHandler: IHandler = async () => new Response('ok'); const dummyHandler: IHandler = async () => new Response('ok');
const dummyMiddleware: IMiddleware = async (_, next) => await next(); const dummyMiddleware: IMiddleware = async (_, next) => await next();
const dummyDef: IRouteDefinition = { method: 'get', path: '/hello' }; const dummyDef: IRouteDefinition = { method: 'GET', path: '/hello' };
const dummyMatcher = () => ({ params: {} }); const dummyMatcher = () => ({ params: {} });
Deno.test('middleware: single middleware is registered correctly', () => { Deno.test('middleware: single middleware is registered correctly', () => {
@@ -57,7 +57,7 @@ Deno.test('middleware: preserves order of middleware', () => {
Deno.test('handle: uppercases method', () => { Deno.test('handle: uppercases method', () => {
let result: IInternalRoute | null = null as IInternalRoute | null; let result: IInternalRoute | null = null as IInternalRoute | null;
new RouteBuilder((r) => result = r, { method: 'post', path: '/x' }) new RouteBuilder((r) => result = r, { method: 'POST', path: '/x' })
.handle(dummyHandler); .handle(dummyHandler);
assertEquals(result?.method, 'POST'); assertEquals(result?.method, 'POST');
@@ -74,8 +74,18 @@ Deno.test('handle: works with no middleware', async () => {
const request = new Request('http://localhost'); const request = new Request('http://localhost');
const res1 = await route?.handler({ req: request, params: {}, state: {} }); const res1 = await route?.handler({
const res2 = await dummyHandler({ req: request, params: {}, state: {} }); req: request,
params: {},
state: {},
query: {},
});
const res2 = await dummyHandler({
req: request,
params: {},
state: {},
query: {},
});
assertEquals(res1?.status, res2?.status); assertEquals(res1?.status, res2?.status);
assertEquals(await res1?.text(), await res2?.text()); assertEquals(await res1?.text(), await res2?.text());

View File

@@ -1,54 +0,0 @@
import {
assert,
assertEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
import { createRouteMatcher } from '../mod.ts';
// Dummy request
const dummyRequest = new Request('http://localhost');
Deno.test('createRouteMatcher: static route matches and extracts params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/users/42'), dummyRequest);
assert(result);
assertEquals(result.params, { id: '42' });
});
Deno.test('createRouteMatcher: static route with multiple params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' };
const matcher = createRouteMatcher(def);
const result = matcher(
new URL('http://localhost/repo/max/wiki'),
dummyRequest,
);
assert(result);
assertEquals(result.params, { owner: 'max', name: 'wiki' });
});
Deno.test('createRouteMatcher: static route does not match wrong path', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/posts/42'), dummyRequest);
assertStrictEquals(result, null);
});
Deno.test('createRouteMatcher: uses custom matcher if provided', () => {
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/ping' ? { params: {} } : null,
};
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/ping'), dummyRequest);
assert(result);
assertEquals(result.params, {});
});

View File

@@ -1,4 +1,4 @@
// deno-coverage-ignore-file // deno-coverage-ignore-file
export { HttpKernel } from './HttpKernel.ts'; export { HttpKernel } from './HttpKernel.ts';
export { RouteBuilder } from './RouteBuilder.ts'; export { RouteBuilder } from './RouteBuilder.ts';
export { createRouteMatcher } from './Utils.ts'; export { createRouteMatcher } from './Utils/createRouteMatcher.ts';