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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = {
'error',
{
ImportDeclaration: {
minProperties: 2,
minProperties: 1,
multiline: true,
},
},
Expand Down
107 changes: 99 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ pnpm add @bessonovs/node-http-router

## Documentation and examples

### Usage with native node http server
### 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.

#### Usage with native node http server

```typescript
const router = new Router((req, res) => {
Expand All @@ -50,7 +54,7 @@ router.addRoute({

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

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

Expand All @@ -68,7 +72,11 @@ router.addRoute({

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

### MethodMatcher
### Matchers

In the core, matchers are responsible to decide if particular handler should be called or not. There is no magic: matchers are interated on every request and first positive "match" calls defined handler.

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

Method matcher is the simplest matcher and matches any of the passed http methods:

Expand All @@ -80,7 +88,7 @@ router.addRoute({
})
```

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

Matches given pathnames (but ignores query parameters):

Expand All @@ -92,7 +100,7 @@ router.addRoute({
})
```

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

Defines expectations on query parameters:

Expand All @@ -115,7 +123,7 @@ router.addRoute({
})
```

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

Allows powerful expressions:

Expand All @@ -127,7 +135,7 @@ router.addRoute({
```
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`.

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

EndpointMatcher is a combination of Method and RegExpUrl matcher for convenient usage:

Expand All @@ -138,9 +146,92 @@ router.addRoute({
})
```

### Middleware

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!

But well, handler can be wrapped like:

```typescript
// example of a generic middleware, not a cors middleware!
function corsMiddleware(origin: string) {
return function corsWrapper<T extends MatchResult>(
wrappedHandler: Handler<T>,
): Handler<T> {
return async function corsHandler(req, res, ...args) {
// -> executed before handler
// it's even possible to skip the handler at all
const result = await wrappedHandler(req, res, ...args)
// -> executed after handler, like:
res.setHeader('Access-Control-Allow-Origin', origin)
return result
}
}
}

// create a configured instance of middleware
const cors = corsMiddleware('http://0.0.0.0:8080')

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

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> {
return function middlewaresHandler(...args) {
// @ts-expect-error
return cors(session(handler(...args)))
}
}

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

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}`),
})
```

## License

The MIT License (MIT)
MIT License

Copyright (c) 2019 - today, Anton Bessonov

Expand Down
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bessonovs/node-http-router",
"version": "0.0.8",
"version": "0.0.9",
"description": "Extensible http router for node and micro",
"keywords": [
"router",
Expand Down Expand Up @@ -30,7 +30,8 @@
"build": "tsc",
"example-node-start": "tsc && node dist/examples/node.js",
"example-micro-start": "tsc && node dist/examples/micro.js",
"precommit": "$_ run test && $_ run lint && $_ run build"
"precommit": "$_ run test && $_ run lint && $_ run build",
"update": "pnpm update --interactive --recursive --latest"
},
"dependencies": {
"urlite": "3.0.0"
Expand All @@ -40,19 +41,19 @@
"@types/express": "4.17.13",
"@types/jest": "27.4.1",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "5.13.0",
"@typescript-eslint/parser": "5.13.0",
"eslint": "8.10.0",
"@typescript-eslint/eslint-plugin": "5.17.0",
"@typescript-eslint/parser": "5.17.0",
"eslint": "8.12.0",
"eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-react": "7.29.3",
"eslint-plugin-react": "7.29.4",
"jest": "27.5.1",
"micro": "9.3.5-canary.3",
"node-mocks-http": "1.11.0",
"path-to-regexp": "6.2.0",
"ts-jest": "27.1.3",
"typescript": "4.6.2"
"ts-jest": "27.1.4",
"typescript": "4.6.3"
},
"publishConfig": {
"access": "public"
Expand Down
16 changes: 11 additions & 5 deletions src/__tests__/router.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import {
IncomingMessage, ServerResponse,
IncomingMessage,
ServerResponse,
} from 'http'
import {
createRequest, createResponse,
createRequest,
createResponse,
} from 'node-mocks-http'
import {
compile, pathToRegexp,
compile,
pathToRegexp,
} from 'path-to-regexp'
import {
MatchedHandler, Router,
MatchedHandler,
Router,
} from '../router'
import {
AndMatcher, EndpointMatcher, ExactUrlPathnameMatcher,
AndMatcher,
EndpointMatcher,
ExactUrlPathnameMatcher,
MethodMatcher,
} from '../matchers'

Expand Down
10 changes: 7 additions & 3 deletions src/examples/micro.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import http from 'http'
import micro, { send } from 'micro'
import { Router } from '../router'
import micro, {
send,
} from 'micro'
import {
Router,
} from '../router'
import {
EndpointMatcher,
ExactUrlPathnameMatcher,
Expand Down Expand Up @@ -30,7 +34,7 @@ server.once('listening', () => {

router.addRoute({
// it's not necessary to type the matcher, but it give you a confidence
matcher: new EndpointMatcher<{name: string}>('GET', /^\/hello\/(?<name>[^/]+)$/),
matcher: new EndpointMatcher<{ name: string }>('GET', /^\/hello\/(?<name>[^/]+)$/),
handler: (req, res, match) => {
return `Hello ${match.match.groups.name}!`
},
Expand Down
4 changes: 3 additions & 1 deletion src/examples/node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import http from 'http'
import { Router } from '../router'
import {
Router,
} from '../router'
import {
EndpointMatcher,
ExactUrlPathnameMatcher,
Expand Down
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export * from './matchers'
export type { Route, MatchedHandler } from './router'
export { Router } from './router'
export type {
Handler,
Route,
MatchedHandler,
} from './router'
export {
Router,
} from './router'
8 changes: 6 additions & 2 deletions src/matchers/AndMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import {
IncomingMessage, ServerResponse,
IncomingMessage,
ServerResponse,
} from 'http'
import {
MatchResult, Matched, Matcher, isMatched,
MatchResult,
Matched,
Matcher,
isMatched,
} from '.'

export type AndMatcherResult<MR1 extends MatchResult, MR2 extends MatchResult> = MatchResult<{
Expand Down
12 changes: 9 additions & 3 deletions src/matchers/EndpointMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import {
IncomingMessage,
ServerResponse,
} from 'http'
import { Matcher } from './Matcher'
import { MatchResult } from './MatchResult'
import {
Matcher,
} from './Matcher'
import {
MatchResult,
} from './MatchResult'
import {
Method,
MethodMatchResult,
MethodMatcher,
} from './MethodMatcher'
import { AndMatcher } from './AndMatcher'
import {
AndMatcher,
} from './AndMatcher'
import {
RegExpExecGroupArray,
RegExpUrlMatchResult,
Expand Down
12 changes: 9 additions & 3 deletions src/matchers/ExactQueryMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { IncomingMessage } from 'http'
import {
IncomingMessage,
} from 'http'
import Url from 'urlite'
import { Matcher } from './Matcher'
import { MatchResult } from './MatchResult'
import {
Matcher,
} from './Matcher'
import {
MatchResult,
} from './MatchResult'

type QueryMatch = {[key: string]: string | true | false | undefined}

Expand Down
12 changes: 9 additions & 3 deletions src/matchers/ExactUrlPathnameMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { IncomingMessage } from 'http'
import {
IncomingMessage,
} from 'http'
import Url from 'urlite'
import { Matcher } from './Matcher'
import { MatchResult } from './MatchResult'
import {
Matcher,
} from './Matcher'
import {
MatchResult,
} from './MatchResult'

export type ExactUrlPathnameMatchResult<U extends [string, ...string[]]> = MatchResult<{
pathname: U[number]
Expand Down
7 changes: 5 additions & 2 deletions src/matchers/Matcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
IncomingMessage, ServerResponse,
IncomingMessage,
ServerResponse,
} from 'http'
import { MatchResult } from './MatchResult'
import {
MatchResult,
} from './MatchResult'

export type ExtractMatchResult<M> = M extends Matcher<infer MR> ? MR : never

Expand Down
12 changes: 9 additions & 3 deletions src/matchers/MethodMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { IncomingMessage } from 'http'
import { Matcher } from './Matcher'
import { MatchResult } from './MatchResult'
import {
IncomingMessage,
} from 'http'
import {
Matcher,
} from './Matcher'
import {
MatchResult,
} from './MatchResult'

const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] as const

Expand Down
Loading