Holiday Router
Advanced URI Routing library for matching URI to controller
- ### Open Source under the MIT License
- ### Written in Typescript. Strong typing and interface based design makes it very flexible for developers to implement own controller classes.
- ### Ideal for being used as the URI and/or Http routing module in custom frameworks written in TypeScript
- ### Over 150 unit tests covering 100% of codebase and many extra real-life use-cases are covered
- ### Using industry standard eslint rules for typescript makes use of best practices for writing clean tyescript code
Features
- Named uri parameters /catalog/{category}/{subcategory/
- Catchall routes with support for named catchall parameter /images/**imagepath or just /images/** for anonymous catchall param
- Support for prefix and postfix in uri segments /catalog/category-{category}.html In this example the category- is a prefix and .html and postfix and category will be extrafted from url
- Support for regex matches in uri segments /cagegory/{categoryid:[0-9]+}}/info In this case the named parameter categoryid must be all numeric or it will not produce a match
- Regex segments in match are also extracted and added to RouteMatch (return object from uri match)
- Named routes for reverse route generation.
- Multiple routes per uri match. This way a developer may have their own custom logic in addition to just matching the url. For example there could be a requirement to pick different controller for the same URI based on value of the request header, or presense of query parameters, or time of day, or anything else. When route is matched it always returns array of objects that implemnt IControllr interface If this feature is not required then just add a single object per URI ane a match will return array with one element Also there is a convenience classes for creating instances of IControllerContainer.
- Compact tree structure for storing routes makes it very memory-efficient and fast.
- Convenience class HttpRouter is a wrapper class than added support for adding routes specific to http request methods. Basically HttpRouter holds a Map<httpMethod, Router> and matches the http method first and if found delegates uri resolution to a router object for that method.
How it works
Here is a break-down of how the routing information is stored when we add 5 routes to the router. Router breaks up the URI into uri segments. Segment is a part of the URI that ends with path separator '/'
- /catalog/category/{categoryID}/item-{widget:[0-9]+}/info
- /catalog/category/shoes/{brand}
- /catalog/category/shoes/{brand}/{size}
- /customers/orders/{orderID:[0-9]+}
- /customers/customer-{customerID:[0-9]+}/info
Path Node NodeType
--------------------------------------------------------------------------------------
|- / ExactMatchNode
| |- catalog/ ExactMatchNode
| |- category/ ExactMatchNode
| |- shoes/ ExactMatchNode
| |- {brand} PathParamNode
| |- {brand}/ PathParamNode
| |- {size} PathParamNode
| |- {categoryID} PathParamNode
| |- item-{widget:[0-9]+}/ RegexNode
| |- info ExactMatchNode
| |- customers/ ExactMatchNode
| |- customer-{customerID:[0-9]+}/ RegexNode
| |- info ExactMatchNode
| |- orders/ ExactMatchNode
| |- {orderID:[0-9]+} RegexNode
Installation
Install using npm:
npm install holiday-router
API Reference
-
Errors
-
Enums
-
Classes
- Router
- new Router<T extends IControllerContainer>()
- instance methods
- .addRoute(uri: string, controller: T) :
Node<T>
- .getRouteMatch(uri: string):
IRouteMatchResult<T extends IControllerContainer>
- .makeUri(controllerId: string, params: IStringMap = {}):
string
- .getAllRoutes():
Array<IRouteInfo>
- .addRoute(uri: string, controller: T) :
- HttpRouter
- new HttpRouter<T extends IControllerContainer>()
- instance methods
- .addRoute(httpMethod: HTTPMethod, uri: string, controller: T) :
Node<T>
- .getRouteMatch(httpMethod: HTTPMethod, uri: string):
undefined | IRouteMatch<T>
- .makeUri(httpMethod: HTTPMethod, controllerId: string, params?: IStringMap):
string
- .getAllRoutes():
Array<IHttpRouteInfo>
- .addRoute(httpMethod: HTTPMethod, uri: string, controller: T) :
- Router
Interfaces
IControllerContainer
Developer must implement own class that implements an IControllerContainer interface or use one of 2 helper Classes: BasicController or UniqueController
IRouteMatch
IRouteMatchResult
;
IUriParams
IExtractedPathParam
IRegexParams
IStringMap
IRouteInfo
IHttpRouteInfo
Node<T extends IControllerContainer>
Errors
RouterError
new RouterError(message: string, code: RouterErrorCode)
RouterErrorCode
Classes
Router
new Router()
Creates a new instance of Router.
Example
; const router = ;
Node<T>
.addRoute(uri: string, controller: T): Adds route to router.
param | type | description |
---|---|---|
uri | string |
uri with supported uri template syntax |
controller | IControllerContainer |
Controller is an object that must implement IControllerContainer interface |
Example
In this example we adding uri template
that will match any uri that looks like
/catalog/category/somecategory/widget-34/info
; ;router.addRoute'/catalog/category/{categoryID}/item-{widget:[0-9]+}/info', new BasicController'somecontroller', 'ctrl1';
Notice that
- First 2 uri segments must be matched exactly but third and fourth uri segments are placeholder segments.
- Third segment can match any string and that string will then be available in the RouteMatch object when .getRouteMatch() is called with the uri
- Fourth segment has a prefix widget- and the placeholder is a Regular Expression based param it must match the regex [0-9]+ (must be numeric value)
IRouteMatchResult<T>
.getRouteMatch(uri: string): Matches the URI and returns RouteMatch or undefined in no match found.
param | type | description |
---|---|---|
uri | string |
a full uri path. uri is case-sensitive |
Example
In this example we going to add a route
and then will get the matching object for
the url: /catalog/category/toys/widget-34/info
; ;router.addRoute'/catalog/category/{categoryID}/widget-{widget:([0-9]+)-(blue|red)}/info', new BasicController'somecontroller', 'ctrl1';;
We will get back the result object RouteMatch (it implements IRouteMatchResult) The object will have the following structure:
Notice the RouteMatch has 2 properties:
- params which contains extracted pathParam and regexParams
- node which contains .controllers array with our controller
Notice that regexParams contains array of values extracted from regex route match. The first element in array of regex matches is always the entire match, in this case it's "34-blue", second element is specific match of capturing groups in our regex: "34" from capturing group ([0-9]+) and "blue" from capturing group (blue|red)
"regexParams": [
{
"paramName": "widget",
"params": [
"34-blue",
"34",
"blue"
]
}
]
IStringMap = {}): string
.makeUri(controllerId: string, params: Generates URI for route. Replaces placeholders in URI template with values provided in params argument.
param | type | description |
---|---|---|
controllerId | string |
value of .id of Controller (implements IControllerContainer ) for the route |
params | IStringMap |
Object with keys matching placeholders in URI template for the route and value to be used in place of placeholders |
Throws RouterError with RouterErrorCode = RouterErrorCode.CONTROLLER_NOT_FOUND
if controller not found by controllerId.
Throws RouterError with RouterErrorCode = RouterErrorCode.MAKE_URI_MISSING_PARAM
if
params object does not have a key matching any of paramNames in URI template for the route.
Throws RouterError with RouterErrorCode = RouterErrorCode.MAKE_URI_REGEX_FAIL
if
value of param in params object does not match Regex in regex segment in uri template.
Example In this example we going to add a route and then call makeUri method to generate URI for the route:
; ;router.addRoute'/catalog/category/{categoryID}/widget-{widget:([0-9]+)-(blue|red)}/info', new BasicController'somecontroller', 'ctrl1';;
The value of uri in this example will be /catalog/category/toys/widget-24-blue/info
IRouteInfo>
.getAllRoutes(): Array<Example:
;;;;;; ;;;;;;;router.addRouteuri1, ctrl1;router.addRouteuri2, ctrl2;router.addRouteuri3, ctrl3;router.addRouteuri4, ctrl4;router.addRouteuri5, ctrl5;router.addRouteuri2, ctrl6; ;
The value of res in this example will be
HttpRouter
HttpRouter is a convenience wrapper class that internally holds map of httpMethod -> Router each method has own instance of Router object. Only methods supported by Node.js (included in array or Node.js http.METHODS) or by 'methods' npm module are supported Only methods that were added to the instance of HttpRouter with addRoute are added to the map of method -> router In other words if addRoute was used to only add GET and POST methods then the internal map method -> router will have only 2 elements.
IMPORTANT - when adding route with addRoute method the first parameter httpMethod is converted to upper case and used as key in map of method -> router as upper case string but the .getRouteMatch method does not convert the first parameter 'httpMethod' to upper case so you must make sure when you call .getRouteMatch that you pass the first argument in upper case. This is done for performance reasons since Node.js already give value of method in upper case, so we don't need to call .toUpperCase every time the .getRouteMatch is called
new Router()
Creates a new instance of Router.
Example
;; ;
Node<T>
.addRoute(httpMethod: HTTPMethod, uri: string, controller: T): Adds route to router.
param | type | description |
---|---|---|
httpMethod | HTTPMethod |
Uses HTTPMethod enum from http-method-enum npm package |
uri | string |
uri with supported uri template syntax |
controller | IControllerContainer |
Controller is an object that must implement IControllerContainer interface |
Throws RouterError with RouterErrorCode = RouterErrorCode.UNSUPPORTED_HTTP_METHOD
if httpMethod not supported by version of Node.js (if used with Node.js) or not in list of method from 'methods' npm module
IRouteMatchResult<T>
.getRouteMatch(httpMethod: HTTPMethod, uri: string): Matches the http request method and URI and returns RouteMatch or undefined in no match found.
param | type | description |
---|---|---|
httpMethod | HTTPMethod |
Http Request method. uses HTTPMethod enum from http-method-enum npm package |
uri | string |
a full uri path. uri is case-sensitive |
Example
In this example we going to add a route
for the http 'GET' method and then will get the matching object for
the 'GET' method and url: /catalog/category/toys/widget-34/info
; ; ;router.addRouteHTTPMethod.GET, '/catalog/category/{categoryID}/item-{widget:([0-9]+)-(blue|red)}/info', new BasicController'somecontroller', 'ctrl1';;
IStringMap = {}): string
.makeUri(httpMethod: string, controllerId: string, params: Generates URI for route. Replaces placeholders in URI template with values provided in params argument.
param | type | description |
---|---|---|
httpMethod | HTTPMethod |
uses emum from http-method-enum npm package |
controllerId | string |
value of .id of Controller (implements IControllerContainer ) for the route |
params | IStringMap |
Object with keys matching placeholders in URI template for the route and value to be used in place of placeholders |
Throws RouterError with RouterErrorCode = RouterErrorCode.UNSUPPORTED_HTTP_METHOD
if httpMethod not supported by version of Node.js (if used with Node.js) or not in list of method from 'methods' npm module
Throws RouterError with RouterErrorCode = RouterErrorCode.CONTROLLER_NOT_FOUND
if controller not found by controllerId.
Throws RouterError with RouterErrorCode = RouterErrorCode.MAKE_URI_MISSING_PARAM
if
params object does not have a key matching any of paramNames in URI template for the route.
Throws RouterError with RouterErrorCode = RouterErrorCode.MAKE_URI_REGEX_FAIL
if
value of param in params object does not match Regex in regex segment in uri template.
Example In this example we going to add a route and then call makeUri method to generate URI for the route:
; ; ;router.addRouteHTTPMethod.GET, '/catalog/category/{categoryID}/item-{widget:([0-9]+)-(blue|red)}/info', new BasicController'somecontroller', 'ctrl1';;
The value of uri in this example will be /catalog/category/toys/item-24-blue/info
IHttpRouteInfo>
.getAllRoutes(): Array<Example
;; ;; ;;;;;; ;httpRouter.addRouteHTTPMethod.GET, uri1, ctrl1;httpRouter.addRouteHTTPMethod.GET, uri2, ctrl2;httpRouter.addRouteHTTPMethod.POST, uri1, ctrl3;httpRouter.addRouteHTTPMethod.POST, uri2, ctrl4;httpRouter.addRouteHTTPMethod.POST, uri1, ctrl5;httpRouter.addRouteHTTPMethod.POST, uri2, ctrl6; ;
the value of allRoutes in this example will be