Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/event-handler/src/rest/BaseRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ErrorHandler,
ErrorResolveOptions,
HttpMethod,
Middleware,
Path,
RouteHandler,
RouteOptions,
Expand All @@ -30,13 +31,18 @@ import {
} from './errors.js';
import { Route } from './Route.js';
import { RouteHandlerRegistry } from './RouteHandlerRegistry.js';
import { isAPIGatewayProxyEvent, isHttpMethod } from './utils.js';
import {
composeMiddleware,
isAPIGatewayProxyEvent,
isHttpMethod,
} from './utils.js';

abstract class BaseRouter {
protected context: Record<string, unknown>;

protected readonly routeRegistry: RouteHandlerRegistry;
protected readonly errorHandlerRegistry: ErrorHandlerRegistry;
protected readonly middlwares: Middleware[] = [];

/**
* A logger instance to be used for logging debug, warning, and error messages.
Expand Down Expand Up @@ -140,6 +146,33 @@ abstract class BaseRouter {
};
}

/**
* Registers a global middleware function that will be executed for all routes.
*
* Global middleware executes before route-specific middleware and follows the onion model
* where middleware executes in registration order before `next()` and in reverse order after `next()`.
*
* @param middleware - The middleware function to register globally
*
* @example
* ```typescript
* const authMiddleware: Middleware = async (params, options, next) => {
* // Authentication logic
* if (!isAuthenticated(options.request)) {
* return new Response('Unauthorized', { status: 401 });
* }
* await next();
* // Cleanup or logging after request completion
* console.log('Request completed');
* };
*
* router.use(authMiddleware);
* ```
*/
public use(middleware: Middleware): void {
this.middlwares.push(middleware);
}

/**
* Resolves an API Gateway event by routing it to the appropriate handler
* and converting the result to an API Gateway proxy result. Handles errors
Expand Down Expand Up @@ -185,14 +218,28 @@ abstract class BaseRouter {
throw new NotFoundError(`Route ${path} for method ${method} not found`);
}

const result = await route.handler.apply(options?.scope ?? this, [
const handler =
options?.scope != null
? route.handler.bind(options.scope)
: route.handler;

const middleware = composeMiddleware([...this.middlwares]);

const result = await middleware(
route.params,
{
event,
context,
request,
},
]);
() => handler(route.params, { event, context, request })
);

// In practice this we never happen because the final 'middleware' is
// the handler function that allways returns HandlerResponse. However, the
// type signature of of NextFunction includes undefined so we need this for
// the TS compiler
if (result === undefined) throw new InternalServerError();

return await handlerResultToProxyResult(result);
} catch (error) {
Expand Down
73 changes: 73 additions & 0 deletions packages/event-handler/src/rest/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import type {
CompiledRoute,
HandlerResponse,
HttpMethod,
Middleware,
Path,
RequestOptions,
ValidationResult,
} from '../types/rest.js';
import {
Expand Down Expand Up @@ -105,3 +108,73 @@ export const isAPIGatewayProxyResult = (
typeof result.isBase64Encoded === 'boolean')
);
};

/**
* Composes multiple middleware functions into a single middleware function.
*
* Middleware functions are executed in order, with each middleware having the ability
* to call `next()` to proceed to the next middleware in the chain. The composed middleware
* follows the onion model where middleware executes in order before `next()` and in
* reverse order after `next()`.
*
* @param middlewares - Array of middleware functions to compose
* @returns A single middleware function that executes all provided middlewares in sequence
*
* @example
* ```typescript
* const middleware1: Middleware = async (params, options, next) => {
* console.log('middleware1 start');
* await next();
* console.log('middleware1 end');
* };
*
* const middleware2: Middleware = async (params, options, next) => {
* console.log('middleware2 start');
* await next();
* console.log('middleware2 end');
* };
*
* const composed: Middleware = composeMiddleware([middleware1, middleware2]);
* // Execution order:
* // middleware1 start
* // -> middleware2 start
* // -> handler
* // -> middleware2 end
* // -> middleware1 end
* ```
*/
export const composeMiddleware = (middlewares: Middleware[]): Middleware => {
return async (
params: Record<string, string>,
options: RequestOptions,
next: () => Promise<HandlerResponse | void>
): Promise<HandlerResponse | void> => {
let index = -1;
let result: HandlerResponse | undefined;

const dispatch = async (i: number): Promise<void> => {
if (i <= index) throw new Error('next() called multiple times');
index = i;

if (i === middlewares.length) {
const nextResult = await next();
if (nextResult !== undefined) {
result = nextResult;
}
return;
}

const middleware = middlewares[i];
const middlewareResult = await middleware(params, options, () =>
dispatch(i + 1)
);

if (middlewareResult !== undefined) {
result = middlewareResult;
}
};

await dispatch(0);
return result;
};
};
9 changes: 9 additions & 0 deletions packages/event-handler/src/types/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ type RouteOptions = {
path: Path;
};

type NextFunction = () => Promise<HandlerResponse | void>;

type Middleware = (
params: Record<string, string>,
options: RequestOptions,
next: NextFunction
) => Promise<void | HandlerResponse>;

type RouteRegistryOptions = {
/**
* A logger instance to be used for logging debug, warning, and error messages.
Expand Down Expand Up @@ -111,6 +119,7 @@ export type {
HandlerResponse,
HttpStatusCode,
HttpMethod,
Middleware,
Path,
RequestOptions,
RouterOptions,
Expand Down
Loading