Skip to content

Commit

Permalink
Merge pull request #7 from Bessonov/nested-routes
Browse files Browse the repository at this point in the history
allow nested routes
  • Loading branch information
Bessonov committed Jun 24, 2022
2 parents 419d201 + b8bda0a commit d74d81d
Show file tree
Hide file tree
Showing 40 changed files with 1,368 additions and 654 deletions.
6 changes: 3 additions & 3 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
coverage/
.eslintignore
.eslintrc.js
.git
.github
.npmignore
.nvmrc
.travis/
.travis.yml
jest.config.js
tsconfig.json
src/
**/__tests__/**
**/examples/**
**/examples/**
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v16.14.0
v18.4.0
226 changes: 158 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Router for Node.js, micro and others
====================================
Router for Node.js, micro and other use cases
=============================================

[![Project is](https://img.shields.io/badge/Project%20is-fantastic-ff69b4.svg)](https://github.com/Bessonov/node-http-router)
[![Build Status](https://api.travis-ci.org/Bessonov/node-http-router.svg?branch=master)](https://travis-ci.org/Bessonov/node-http-router)
Expand All @@ -18,7 +18,18 @@ This router is intended to be used with native node http interface. Features:
- Convenient [`EndpointMatcher`](#endpointmatcher)
- `AndMatcher` and `OrMatcher`
- Can be used with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
- Work with another servers? Tell it me!
- Work with other servers? Tell it me!

From 2.0.0 the router isn't tied to node or even http anymore! Although the primary use case is still node's request routing, you can use it for use cases like event processing.

## Sponsoring

Contact me if you want to become a sponsor or need paid support.

Sponsored by Superlative GmbH

![Superlative GmbH](./sponsors/superlative.gmbh.png)


## Installation

Expand All @@ -30,48 +41,110 @@ yarn add @bessonovs/node-http-router
pnpm add @bessonovs/node-http-router
```

## Changelog

See [releases](https://github.com/Bessonov/node-http-router/releases).

## Documentation and examples

### Binding

The router works with native http interfaces like `IncomingMessage` and `ServerResponse`. Therefore it should be possible to use it with most of existing servers.
The router doesn't depends on the native http interfaces like `IncomingMessage` and `ServerResponse`. Therefore, you can use it for everything. Below are some use cases.

#### Usage with native node http server

```typescript
const router = new Router((req, res) => {
res.statusCode = 404
res.end()
})
const router = new NodeHttpRouter()

const server = http.createServer(router.serve).listen(8080, 'localhost')

router.addRoute({
matcher: new ExactUrlPathnameMatcher(['/hello']),
handler: () => 'Hello kitty!',
})

// 404 handler
router.addRoute({
matcher: new BooleanMatcher(true),
handler: ({ data: { res } }) => send(res, 404)
})
```

See [full example](src/examples/node.ts) and [native node http server](https://nodejs.org/api/http.html#http_class_http_server) documentation.

#### Usage with micro
#### Usage with micro server

[micro](https://github.com/vercel/micro) is a very lightweight layer around the native node http server with some convenience methods.

```typescript
// specify default handler
const router = new Router((req, res) => send(res, 404))
const router = new NodeHttpRouter()

http.createServer(micro(router.serve)).listen(8080, 'localhost')

router.addRoute({
matcher: new ExactUrlPathnameMatcher(['/hello']),
handler: () => 'Hello kitty!',
})

// 404 handler
router.addRoute({
matcher: new BooleanMatcher(true),
handler: ({ data: { res } }) => send(res, 404)
})
```

See [full example](src/examples/micro.ts).

#### Usage for event processing or generic use case

```typescript
// Custom type
type MyEvent = {
name: 'test1',
} | {
name: 'test2',
} | {
name: 'invalid',
}

const eventRouter = new Router<MyEvent>()

eventRouter.addRoute({
// define matchers for event processing
matcher: ({
match(params: MyEvent): MatchResult<number> {
const result = /^test(?<num>\d+)$/.exec(params.name)
if (result?.groups?.num) {
return {
matched: true,
result: parseInt(result.groups.num)
}
}
return {
matched: false,
}
},
}),
// define event handler for matched events
handler({ data, match: { result } }) {
return `the event ${data.name} has number ${result}`
}
})

// add default handler
eventRouter.addRoute({
matcher: new BooleanMatcher(true),
handler({ data }) {
return `the event '${data.name}' is unknown`
}
})

// execute and get processing result
const result = eventRouter.exec({
name: 'test1',
})
```

### Matchers

In the core, matchers are responsible to decide if particular handler should be called or not. There is no magic: matchers are iterated on every request and first positive "match" calls defined handler.
Expand All @@ -84,7 +157,7 @@ Method matcher is the simplest matcher and matches any of the passed http method
router.addRoute({
matcher: new MethodMatcher(['OPTIONS', 'POST']),
// method is either OPTIONS or POST
handler: (req, res, { method }) => `Method: ${method}`,
handler: ({ match: { result: { method } } }) => `Method: ${method}`,
})
```

Expand All @@ -96,7 +169,7 @@ Matches given pathnames (but ignores query parameters):
router.addRoute({
matcher: new ExactUrlPathnameMatcher(['/v1/graphql', '/v2/graphql']),
// pathname is /v1/graphql or /v2/graphql
handler: (req, res, { pathname }) => `Path is ${pathname}`,
handler: ({ match: { result: { pathname } } }) => `Path is ${pathname}`,
})
```

Expand All @@ -115,11 +188,11 @@ router.addRoute({
// undefined defines optional parameters. They
// aren't used for matching, but available as type
isOptional: undefined,
// a string defines expected parameter name and value
mustExact: 'exactValue',
// array of strings defines expected parameter name and value
mustExact: ['exactValue'] as const,
}),
// query parameter isOptional has type string | undefined
handler: (req, res, { query }) => query.isOptional,
handler: ({ match: { result: { query } } }) => query.isOptional,
})
```

Expand All @@ -130,10 +203,10 @@ Allows powerful expressions:
```typescript
router.addRoute({
matcher: new RegExpUrlMatcher<{ userId: string }>([/^\/group\/(?<userId>[^/]+)$/]),
handler: (req, res, { match }) => `User id is: ${match.groups.userId}`,
handler: ({ match: { result: { match } } }) => `User id is: ${match.groups.userId}`,
})
```
Ordinal parameters can be used too. Be aware that regular expression must match the whole base url (also with query parameters) and not only `pathname`.
Be aware that regular expression must match the whole base url (also with query parameters) and not only `pathname`. Ordinal parameters can be used too.

#### EndpointMatcher ([source](./src/matchers/EndpointMatcher.ts))

Expand All @@ -142,13 +215,13 @@ EndpointMatcher is a combination of Method and RegExpUrl matcher for convenient
```typescript
router.addRoute({
matcher: new EndpointMatcher<{ userId: string }>('GET', /^\/group\/(?<userId>[^/]+)$/),
handler: (req, res, { match }) => `Group id is: ${match.groups.userId}`,
handler: ({ match: { result: { method, match } } }) => `Group id ${match.groups.userId} matched with ${method} method`,
})
```

### Middlewares

**This section is highly experimental!**
**This whole section is highly experimental!**

Currently, there is no built-in API for middlewares. It seems like there is no aproach to provide centralized and typesafe way for middlewares. And it need some conceptual work, before it will be added. Open an issue, if you have a great idea!

Expand All @@ -157,8 +230,17 @@ Currently, there is no built-in API for middlewares. It seems like there is no a
Example of CorsMiddleware usage:

```typescript
const corsMiddleware = CorsMiddleware({
origins: corsOrigins,
const cors = CorsMiddleware(async () => {
return {
origins: ['https://my-cool.site'],
}
})

const router = new NodeHttpRouter()
router.addRoute({
matcher: new MethodMatcher(['OPTIONS', 'POST']),
// use it
handler: cors(({ match: { result: { method } } }) => `Method: ${method}.`),
})
```

Expand All @@ -179,22 +261,42 @@ interface CorsMiddlewareOptions {
}
```

See source file for defaults.
See ([source](./src/middlewares/CorsMiddleware.ts)) file for defaults.

#### Create own middleware

```typescript
// example of a generic middleware, not a cors middleware!
// example of a generic middleware, not a real cors middleware!
function CorsMiddleware(origin: string) {
return function corsWrapper<T extends MatchResult, D extends Matched<T>>(
wrappedHandler: Handler<T, D>,
return function corsWrapper<
T extends MatchResult<any>,
D extends {
// add requirements of middleware
req: ServerRequest,
res: ServerResponse,
}
>(
wrappedHandler: Handler<T, D & {
// new attributes can be used in the handler
isCors: boolean
}>,
): Handler<T, D> {
return async function corsHandler(req, res, ...args) {
return async function corsHandler(params) {
const { req, res } = params.data
const isCors = !!req.headers.origin
// -> executed before handler
// it's even possible to skip the handler at all
const result = await wrappedHandler(req, res, ...args)
const result = await wrappedHandler({
...params,
data: {
...params.data,
isCors,
}
})
// -> executed after handler, like:
res.setHeader('Access-Control-Allow-Origin', origin)
if (isCors) {
res.setHeader('Access-Control-Allow-Origin', origin)
}
return result
}
}
Expand All @@ -203,64 +305,52 @@ function CorsMiddleware(origin: string) {
// create a configured instance of middleware
const cors = CorsMiddleware('http://0.0.0.0:8080')

const router = new NodeHttpRouter()

router.addRoute({
matcher: new MethodMatcher(['OPTIONS', 'POST']),
// use it
handler: cors((req, res, { method }) => `Method: ${method}`),
})
```

Apropos typesafety. You can modify types in middleware:

```typescript
function ValueMiddleware(myValue: string) {
return function valueWrapper<T extends MatchResult>(
handler: Handler<T, Matched<T> & {
// add additional type
myValue: string
}>,
): Handler<T> {
return function valueHandler(req, res, match) {
return handler(req, res, {
...match,
// add additional property
myValue,
})
}
}
}

const value = ValueMiddleware('world')

router.addRoute({
matcher: new MethodMatcher(['GET']),
handler: value((req, res, { myValue }) => `Hello ${myValue}`),
handler: cors(({ match: { result: { method } }, data: { isCors } }) => `Method: ${method}. Cors: ${isCors}`),
})
```

#### DRY approach
#### Combine middlewares

Of course you can create a `middlewares` wrapper and put all middlewares inside it:
```typescript
type Middleware<T extends (handler: Handler<MatchResult>) => Handler<MatchResult>> = Parameters<Parameters<T>[0]>[2]

function middlewares<T extends MatchResult>(
handler: Handler<T, Matched<T>
& Middleware<typeof session>
& Middleware<typeof cors>>,
): Handler<T> {
function middlewares<
T extends MatchResultAny,
D extends {
req: ServerRequest
res: ServerResponse
}
>(
handler: Handler<T, D
& MiddlewareData<typeof corsMiddleware>
& MiddlewareData<typeof sessionMiddleware>
>,
): Handler<T, any> {
return function middlewaresHandler(...args) {
return cors(session(handler))(...args)
return corsMiddleware(sessionMiddleware(handler))(...args)
}
}

router.addRoute({
matcher,
// use it
handler: middlewares((req, res, { csrftoken }) => `Token: ${csrftoken}`),
handler: middlewares(({ data: { csrftoken } }) => `Token: ${csrftoken}`),
})
```

### Nested routers

There are some use cases for nested routers:
- Add features like multi-tenancy
- Implement modularity
- Apply middlewares globally

See [example](./src/__tests__/Router.test.ts#216).

## License

MIT License
Expand Down
Loading

0 comments on commit d74d81d

Please sign in to comment.