Provides HTTP routing middleware.
For further insights, read on.
To install this library use your favorite package manager. No additional steps are required to start using the library.
npm install @n7e/http-routing
This library is implemented in TypeScript but can be used with JavaScript without any additional steps.
The provided action router middleware delegates incoming requests to a matching routable action. To create an action router middleware use the provided action router middleware builder.
To create an action router middleware builder instance, simply import it and create a new instance:
import { ActionRouterMiddlewareBuilder } from "@n7e/http-routing";
const actionRouterMiddlewareBuilder = new ActionRouterMiddlewareBuilder();
If we already have or want to create a routable action we can add it to the builder as is:
actionRouterMiddlewareBuilder.addRoutableAction(routableAction);
Otherwise, the builder provides a method to create a routable action from provided parameters:
import { RequestMethod } from "@n7e/http";
actionRouterMiddlewareBuilder.add(
RequestMethod.GET,
"some/path",
(request, parameters, signal) => { /* Produce an appropriate response. */ }
);
Once configured the builder will produce a middleware that will match incoming requests to the registered actions if possible. If no matching action is found the middleware will delegate to the provided request handler.
const actionRouterMiddleware = actionRouterMiddlewareBuilder.build();
How requests are matched and actions invoked is decided by the builders configured routing predicate and routable action invokation respectively.
Routable actions are the destination of incoming requests and are responsible for producing appropriate HTTP responses. To match routable actions against incoming requests routable actions has the following properties (besides the action itself of course):
- requestMethod
- HTTP request method to match against.
- pathExpression
- A regular expression to match against the HTTP URI path component.
- metadata
- Arbitrary metadata. This can be used to match requests or any other purpose the implementor feel is appropriate.
The logic behind how requests are matched against routable actions can be configured by providing a predicate function to the builder:
actionRouterMiddlewareBuilder.useRoutingPredicate();
The predicate function receives the incoming request and a routable action and should determine whether the request should be routed to the given routable action.
The default behaviour is to check if the request method matches the routable action request method and to test that the routable action path expression matches the request URI path.
Here is an example of a routing predicate that matches request methods defined in method override request headers.
actionRouterMiddlewareBuilder.useRoutingPredicate(
(request, routableAction) => (
[
request.method,
request.headerFieldValueFor("X-Http-Method-Override")
].includes(routableAction.requestMethod) &&
routableAction.pathExpression.test(request.uri.path)
)
);
How routable actions are invoked once an incoming requests has been routed can be configured by providing a routable action invokation function to the builder:
actionRouterMiddlewareBuilder.useActionInvocation(invokationFunction);
The function receives the incoming request, routable action and an abort signal and should invoke the routable action's action to produce an appropriate response.
The default behaviour is to extract parameters from the incoming request URI path using the action router's path expression and invoke the action with the request, parameters and abort signal.
See path pattern for how to extract parameters from route expressions.
When creating routable actions using the action router middleware builder or using decorators you can use a special syntax to define which URI path components a routable action should match against.
It looks really similar to a URI path component but with an additional leading
:
for parameters, a trailing ?
for optional parameters and an optional
regular expression enclosed in parentheses.
Here are some examples:
Description | Path Pattern |
---|---|
Match path verbatim | some/path |
Extract parameter | some/:parameter |
Extract optional parameter | some/:parameter? |
Extract parameter matching expression | some/:parameter(test-.+?) |
Extract optional parameter matching expression | some/:parameter(test-.+?)? |
Regular expressions used in path patterns are quite limited, most notably anchors and capture groups are not supported.
Path patterns can be converted to regular expressions using a provided function:
import { pathExpressionFor } from "@n7e/http-routing";
const regularExpression = pathExpressionFor("/some/:parameter");
This will produce a regular expression that matches URI path components appropriately and creates named groups for matched parameters.
Parameters can be extracted from path expressions using a provided function:
import { extractRouteParametersFrom } from "@n7e/http-routing";
const parameters = extractRouteParametersFrom("some/path", pathExpression);
Optional parameters that are not present will be omitted from the parameters object. Here's a concrete example:
import { extractRouteParametersFrom, pathExpressionFor } from "@n7e/http-routing";
console.log(extractRouteParametersFrom("some/path", pathExpressionFor("/some/:parameter")));
// {parameter: "path"}
If you prefer an object oriented approach there are some decorators available to easily create routable controllers. The decorators adds metadata to the controller class that can be retrieved with a provided function:
import { getDecoratorMetadataFor } from "@n7e/http-routing";
const metadata = getDecoratorMetadataFor(SomeController);
The metadata provided by the decorators has the following properties:
- routePrefix
- Controller wide base path.
- routeMetadata
- Controller wide metadata.
- routableActions
- Routable decorator actions mapped to controller methods.
Here's a simple example of a controller with a single routable decorator action:
import { Request, RequestMethod, Response } from "@n7e/http";
import { request } from "@n7e/http-routing";
class SomeController {
@request(RequestMethod.GET, "some/path")
public someMethod(request: Request): Promise<Response> {
// ...
}
}
You can use the @route()
decorator to provide a controller wide path prefix
like so:
import { Request, RequestMethod, Response } from "@n7e/http";
import { request, route } from "@n7e/http-routing";
@route("some/route")
class SomeController {
@request(RequestMethod.GET, "some/path")
public someMethod(request: Request): Promise<Response> {
// ...
}
}
Ultimately it's up to an implementation to add the registered decorator routes to a routing mechanism. But the idea is that the controller route prefix should prefix all route paths in the controller.
For convenience there are dedicated decorators provided for common request methods. The provided decorators are:
- CONNECT
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
- TRACE
The functionality of these decorators are identical to that of @request()
but
the request method is omitted:
import { GET } from "@n7e/http-routing";
import type { Request, Response } from "@n7e/http";
class SomeController {
@GET("some/path")
public someMethod(request: Request): Promise<Response> {
// ...
}
}
The optional last argument to all decorators allows us to provide arbitrary metadata to the routable decorator action.
import { Request, RequestMethod, Response } from "@n7e/http";
import { request, route } from "@n7e/http-routing";
@route("some/route", {key: "value"})
class SomeController {
@request(RequestMethod.GET, "some/path", {key: "value"})
public someMethod(request: Request): Promise<Response> {
// ...
}
}
The significance of this metadata is up to the implementations of the routing mechanism.
Routable decorator actions are almost identical to routable actions with two key differences:
- pathPattern
- Instead of a regular expression routable decorator actions uses the path pattern syntax to match requests against.
- action
- To enable creative freedom in how to structure the application routable decorator actions only have one restriction, and that's to return a promise resolving with an HTTP response. Whatever parameters (if any) the action accepts or requires is up to the implementor.