Skip to content

Commit d74d81d

Browse files
authored
Merge pull request #7 from Bessonov/nested-routes
allow nested routes
2 parents 419d201 + b8bda0a commit d74d81d

40 files changed

+1368
-654
lines changed

.npmignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
coverage/
22
.eslintignore
33
.eslintrc.js
4+
.git
5+
.github
46
.npmignore
57
.nvmrc
6-
.travis/
7-
.travis.yml
88
jest.config.js
99
tsconfig.json
1010
src/
1111
**/__tests__/**
12-
**/examples/**
12+
**/examples/**

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v16.14.0
1+
v18.4.0

README.md

Lines changed: 158 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Router for Node.js, micro and others
2-
====================================
1+
Router for Node.js, micro and other use cases
2+
=============================================
33

44
[![Project is](https://img.shields.io/badge/Project%20is-fantastic-ff69b4.svg)](https://github.com/Bessonov/node-http-router)
55
[![Build Status](https://api.travis-ci.org/Bessonov/node-http-router.svg?branch=master)](https://travis-ci.org/Bessonov/node-http-router)
@@ -18,7 +18,18 @@ This router is intended to be used with native node http interface. Features:
1818
- Convenient [`EndpointMatcher`](#endpointmatcher)
1919
- `AndMatcher` and `OrMatcher`
2020
- Can be used with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
21-
- Work with another servers? Tell it me!
21+
- Work with other servers? Tell it me!
22+
23+
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.
24+
25+
## Sponsoring
26+
27+
Contact me if you want to become a sponsor or need paid support.
28+
29+
Sponsored by Superlative GmbH
30+
31+
![Superlative GmbH](./sponsors/superlative.gmbh.png)
32+
2233

2334
## Installation
2435

@@ -30,48 +41,110 @@ yarn add @bessonovs/node-http-router
3041
pnpm add @bessonovs/node-http-router
3142
```
3243

44+
## Changelog
45+
46+
See [releases](https://github.com/Bessonov/node-http-router/releases).
47+
3348
## Documentation and examples
3449

3550
### Binding
3651

37-
The router works with native http interfaces like `IncomingMessage` and `ServerResponse`. Therefore it should be possible to use it with most of existing servers.
52+
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.
3853

3954
#### Usage with native node http server
4055

4156
```typescript
42-
const router = new Router((req, res) => {
43-
res.statusCode = 404
44-
res.end()
45-
})
57+
const router = new NodeHttpRouter()
4658

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

4961
router.addRoute({
5062
matcher: new ExactUrlPathnameMatcher(['/hello']),
5163
handler: () => 'Hello kitty!',
5264
})
65+
66+
// 404 handler
67+
router.addRoute({
68+
matcher: new BooleanMatcher(true),
69+
handler: ({ data: { res } }) => send(res, 404)
70+
})
5371
```
5472

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

57-
#### Usage with micro
75+
#### Usage with micro server
5876

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

6179
```typescript
62-
// specify default handler
63-
const router = new Router((req, res) => send(res, 404))
80+
const router = new NodeHttpRouter()
6481

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

6784
router.addRoute({
6885
matcher: new ExactUrlPathnameMatcher(['/hello']),
6986
handler: () => 'Hello kitty!',
7087
})
88+
89+
// 404 handler
90+
router.addRoute({
91+
matcher: new BooleanMatcher(true),
92+
handler: ({ data: { res } }) => send(res, 404)
93+
})
7194
```
7295

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

98+
#### Usage for event processing or generic use case
99+
100+
```typescript
101+
// Custom type
102+
type MyEvent = {
103+
name: 'test1',
104+
} | {
105+
name: 'test2',
106+
} | {
107+
name: 'invalid',
108+
}
109+
110+
const eventRouter = new Router<MyEvent>()
111+
112+
eventRouter.addRoute({
113+
// define matchers for event processing
114+
matcher: ({
115+
match(params: MyEvent): MatchResult<number> {
116+
const result = /^test(?<num>\d+)$/.exec(params.name)
117+
if (result?.groups?.num) {
118+
return {
119+
matched: true,
120+
result: parseInt(result.groups.num)
121+
}
122+
}
123+
return {
124+
matched: false,
125+
}
126+
},
127+
}),
128+
// define event handler for matched events
129+
handler({ data, match: { result } }) {
130+
return `the event ${data.name} has number ${result}`
131+
}
132+
})
133+
134+
// add default handler
135+
eventRouter.addRoute({
136+
matcher: new BooleanMatcher(true),
137+
handler({ data }) {
138+
return `the event '${data.name}' is unknown`
139+
}
140+
})
141+
142+
// execute and get processing result
143+
const result = eventRouter.exec({
144+
name: 'test1',
145+
})
146+
```
147+
75148
### Matchers
76149

77150
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.
@@ -84,7 +157,7 @@ Method matcher is the simplest matcher and matches any of the passed http method
84157
router.addRoute({
85158
matcher: new MethodMatcher(['OPTIONS', 'POST']),
86159
// method is either OPTIONS or POST
87-
handler: (req, res, { method }) => `Method: ${method}`,
160+
handler: ({ match: { result: { method } } }) => `Method: ${method}`,
88161
})
89162
```
90163

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

@@ -115,11 +188,11 @@ router.addRoute({
115188
// undefined defines optional parameters. They
116189
// aren't used for matching, but available as type
117190
isOptional: undefined,
118-
// a string defines expected parameter name and value
119-
mustExact: 'exactValue',
191+
// array of strings defines expected parameter name and value
192+
mustExact: ['exactValue'] as const,
120193
}),
121194
// query parameter isOptional has type string | undefined
122-
handler: (req, res, { query }) => query.isOptional,
195+
handler: ({ match: { result: { query } } }) => query.isOptional,
123196
})
124197
```
125198

@@ -130,10 +203,10 @@ Allows powerful expressions:
130203
```typescript
131204
router.addRoute({
132205
matcher: new RegExpUrlMatcher<{ userId: string }>([/^\/group\/(?<userId>[^/]+)$/]),
133-
handler: (req, res, { match }) => `User id is: ${match.groups.userId}`,
206+
handler: ({ match: { result: { match } } }) => `User id is: ${match.groups.userId}`,
134207
})
135208
```
136-
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`.
209+
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.
137210

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

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

149222
### Middlewares
150223

151-
**This section is highly experimental!**
224+
**This whole section is highly experimental!**
152225

153226
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!
154227

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

159232
```typescript
160-
const corsMiddleware = CorsMiddleware({
161-
origins: corsOrigins,
233+
const cors = CorsMiddleware(async () => {
234+
return {
235+
origins: ['https://my-cool.site'],
236+
}
237+
})
238+
239+
const router = new NodeHttpRouter()
240+
router.addRoute({
241+
matcher: new MethodMatcher(['OPTIONS', 'POST']),
242+
// use it
243+
handler: cors(({ match: { result: { method } } }) => `Method: ${method}.`),
162244
})
163245
```
164246

@@ -179,22 +261,42 @@ interface CorsMiddlewareOptions {
179261
}
180262
```
181263

182-
See source file for defaults.
264+
See ([source](./src/middlewares/CorsMiddleware.ts)) file for defaults.
183265

184266
#### Create own middleware
185267

186268
```typescript
187-
// example of a generic middleware, not a cors middleware!
269+
// example of a generic middleware, not a real cors middleware!
188270
function CorsMiddleware(origin: string) {
189-
return function corsWrapper<T extends MatchResult, D extends Matched<T>>(
190-
wrappedHandler: Handler<T, D>,
271+
return function corsWrapper<
272+
T extends MatchResult<any>,
273+
D extends {
274+
// add requirements of middleware
275+
req: ServerRequest,
276+
res: ServerResponse,
277+
}
278+
>(
279+
wrappedHandler: Handler<T, D & {
280+
// new attributes can be used in the handler
281+
isCors: boolean
282+
}>,
191283
): Handler<T, D> {
192-
return async function corsHandler(req, res, ...args) {
284+
return async function corsHandler(params) {
285+
const { req, res } = params.data
286+
const isCors = !!req.headers.origin
193287
// -> executed before handler
194288
// it's even possible to skip the handler at all
195-
const result = await wrappedHandler(req, res, ...args)
289+
const result = await wrappedHandler({
290+
...params,
291+
data: {
292+
...params.data,
293+
isCors,
294+
}
295+
})
196296
// -> executed after handler, like:
197-
res.setHeader('Access-Control-Allow-Origin', origin)
297+
if (isCors) {
298+
res.setHeader('Access-Control-Allow-Origin', origin)
299+
}
198300
return result
199301
}
200302
}
@@ -203,64 +305,52 @@ function CorsMiddleware(origin: string) {
203305
// create a configured instance of middleware
204306
const cors = CorsMiddleware('http://0.0.0.0:8080')
205307

308+
const router = new NodeHttpRouter()
309+
206310
router.addRoute({
207311
matcher: new MethodMatcher(['OPTIONS', 'POST']),
208312
// use it
209-
handler: cors((req, res, { method }) => `Method: ${method}`),
210-
})
211-
```
212-
213-
Apropos typesafety. You can modify types in middleware:
214-
215-
```typescript
216-
function ValueMiddleware(myValue: string) {
217-
return function valueWrapper<T extends MatchResult>(
218-
handler: Handler<T, Matched<T> & {
219-
// add additional type
220-
myValue: string
221-
}>,
222-
): Handler<T> {
223-
return function valueHandler(req, res, match) {
224-
return handler(req, res, {
225-
...match,
226-
// add additional property
227-
myValue,
228-
})
229-
}
230-
}
231-
}
232-
233-
const value = ValueMiddleware('world')
234-
235-
router.addRoute({
236-
matcher: new MethodMatcher(['GET']),
237-
handler: value((req, res, { myValue }) => `Hello ${myValue}`),
313+
handler: cors(({ match: { result: { method } }, data: { isCors } }) => `Method: ${method}. Cors: ${isCors}`),
238314
})
239315
```
240316

241-
#### DRY approach
317+
#### Combine middlewares
242318

243319
Of course you can create a `middlewares` wrapper and put all middlewares inside it:
244320
```typescript
245-
type Middleware<T extends (handler: Handler<MatchResult>) => Handler<MatchResult>> = Parameters<Parameters<T>[0]>[2]
246-
247-
function middlewares<T extends MatchResult>(
248-
handler: Handler<T, Matched<T>
249-
& Middleware<typeof session>
250-
& Middleware<typeof cors>>,
251-
): Handler<T> {
321+
function middlewares<
322+
T extends MatchResultAny,
323+
D extends {
324+
req: ServerRequest
325+
res: ServerResponse
326+
}
327+
>(
328+
handler: Handler<T, D
329+
& MiddlewareData<typeof corsMiddleware>
330+
& MiddlewareData<typeof sessionMiddleware>
331+
>,
332+
): Handler<T, any> {
252333
return function middlewaresHandler(...args) {
253-
return cors(session(handler))(...args)
334+
return corsMiddleware(sessionMiddleware(handler))(...args)
254335
}
255336
}
256337

257338
router.addRoute({
258339
matcher,
259340
// use it
260-
handler: middlewares((req, res, { csrftoken }) => `Token: ${csrftoken}`),
341+
handler: middlewares(({ data: { csrftoken } }) => `Token: ${csrftoken}`),
261342
})
262343
```
263344

345+
### Nested routers
346+
347+
There are some use cases for nested routers:
348+
- Add features like multi-tenancy
349+
- Implement modularity
350+
- Apply middlewares globally
351+
352+
See [example](./src/__tests__/Router.test.ts#216).
353+
264354
## License
265355

266356
MIT License

0 commit comments

Comments
 (0)