@vyriy/router 0.2.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ With Yarn:
28
28
  yarn add @vyriy/router
29
29
  ```
30
30
 
31
- ## Usage
31
+ ## Basic Router
32
32
 
33
33
  ```ts
34
34
  import { createRouter } from '@vyriy/router';
@@ -36,7 +36,6 @@ import { createRouter } from '@vyriy/router';
36
36
  const router = createRouter();
37
37
 
38
38
  router.get('/health', async ({ event, query, headers, pathParameters, body }) => ({
39
- statusCode: 200,
40
39
  body: JSON.stringify({
41
40
  ok: true,
42
41
  method: event.httpMethod,
@@ -46,6 +45,106 @@ router.get('/health', async ({ event, query, headers, pathParameters, body }) =>
46
45
  body,
47
46
  }),
48
47
  }));
48
+
49
+ router.fallback(async ({ event }) => ({
50
+ statusCode: 404,
51
+ body: JSON.stringify({
52
+ message: 'Not Found',
53
+ path: event.path,
54
+ }),
55
+ }));
56
+
57
+ export const handler = (event: Parameters<typeof router.route>[0]) => router.route(event);
58
+ ```
59
+
60
+ ## Stream Router
61
+
62
+ Use `createStreamRouter()` when handlers write directly to a Lambda response stream. The stream is passed as the second handler argument, and stream handlers do not return a response object.
63
+
64
+ ```ts
65
+ import { createStreamRouter } from '@vyriy/router';
66
+
67
+ const streamRouter = createStreamRouter();
68
+
69
+ streamRouter.get('/events', ({ event, query }, responseStream) => {
70
+ responseStream.setContentType?.('text/plain');
71
+ responseStream.write(`path: ${event.path}\n`);
72
+ responseStream.write(`cursor: ${query?.cursor ?? 'start'}\n`);
73
+ responseStream.end('done');
74
+ });
75
+
76
+ streamRouter.fallback(({ event }, responseStream) => {
77
+ responseStream.setContentType?.('application/json');
78
+ responseStream.end(
79
+ JSON.stringify({
80
+ message: 'Not Found',
81
+ path: event.path,
82
+ }),
83
+ );
84
+ });
85
+
86
+ export const handler = (
87
+ event: Parameters<typeof streamRouter.route>[0],
88
+ responseStream: Parameters<typeof streamRouter.route>[1],
89
+ ) => streamRouter.route(event, responseStream);
90
+ ```
91
+
92
+ ## Calm Composition
93
+
94
+ The router keeps request matching separate from handler wrappers and local server adapters. A small API can stay as a plain composition of focused packages:
95
+
96
+ ```ts
97
+ import { api } from '@vyriy/handler';
98
+ import { createRouter } from '@vyriy/router';
99
+ import { server } from '@vyriy/server';
100
+
101
+ const router = createRouter();
102
+
103
+ router.get('/health', () => ({
104
+ body: JSON.stringify({
105
+ ok: true,
106
+ }),
107
+ }));
108
+
109
+ const handler = api((event) => router.route(event));
110
+
111
+ server(handler);
112
+ ```
113
+
114
+ The same shape works for Lambda response streaming:
115
+
116
+ ```ts
117
+ import { streamApi } from '@vyriy/handler';
118
+ import { createStreamRouter } from '@vyriy/router';
119
+ import { streamServer } from '@vyriy/server';
120
+
121
+ const router = createStreamRouter();
122
+
123
+ router.get('/events', (_params, responseStream) => {
124
+ responseStream.setContentType?.('text/plain');
125
+ responseStream.end('ok');
126
+ });
127
+
128
+ const handler = streamApi((event, responseStream) => router.route(event, responseStream));
129
+
130
+ streamServer(handler);
131
+ ```
132
+
133
+ For a Lambda-only entrypoint, keep the same composition and export the handler:
134
+
135
+ ```ts
136
+ import { api } from '@vyriy/handler';
137
+ import { createRouter } from '@vyriy/router';
138
+
139
+ const router = createRouter();
140
+
141
+ router.get('/health', () => ({
142
+ body: JSON.stringify({
143
+ ok: true,
144
+ }),
145
+ }));
146
+
147
+ export const handler = api((event) => router.route(event));
49
148
  ```
50
149
 
51
150
  ## Exports
@@ -53,24 +152,33 @@ router.get('/health', async ({ event, query, headers, pathParameters, body }) =>
53
152
  The package exposes both the root entry and the direct module entry:
54
153
 
55
154
  ```ts
56
- import { createRouter } from '@vyriy/router';
57
- import { Router } from '@vyriy/router/router';
155
+ import { createRouter, createStreamRouter } from '@vyriy/router';
156
+ import { Router, StreamRouter } from '@vyriy/router/router';
58
157
  ```
59
158
 
60
159
  ## API
61
160
 
62
161
  - `createRouter()` returns a chainable router API.
162
+ - `createStreamRouter()` returns a chainable response streaming router API.
63
163
  - `router.get(path, handler)` registers a `GET` handler.
64
164
  - `router.post(path, handler)` registers a `POST` handler.
65
165
  - `router.put(path, handler)` registers a `PUT` handler.
66
166
  - `router.delete(path, handler)` registers a `DELETE` handler.
67
167
  - `router.patch(path, handler)` registers a `PATCH` handler.
168
+ - `router.fallback(handler)` registers a handler for unmatched requests.
68
169
  - `router.route(event)` resolves the matching route and returns an API Gateway response.
170
+ - `streamRouter.route(event, responseStream)` resolves the matching route and writes to the stream.
171
+
172
+ Route handlers may omit `statusCode`; the router normalizes missing status codes to `200` before returning from `router.route(event)`.
69
173
 
70
- The low-level `Router` class is also available from `@vyriy/router/router` and exposes only:
174
+ The low-level `Router` and `StreamRouter` classes are also available from `@vyriy/router/router`:
71
175
 
72
176
  - `router.on(method, path, handler)`
177
+ - `router.fallback(handler)`
73
178
  - `router.route(event)`
179
+ - `streamRouter.on(method, path, handler)`
180
+ - `streamRouter.fallback(handler)`
181
+ - `streamRouter.route(event, responseStream)`
74
182
 
75
183
  Route handlers receive:
76
184
 
@@ -83,3 +191,9 @@ type HandlerParams = {
83
191
  event: APIGatewayProxyEvent;
84
192
  };
85
193
  ```
194
+
195
+ Stream route handlers receive the same `HandlerParams` as the first argument and `ResponseStream` as the second argument:
196
+
197
+ ```ts
198
+ type StreamHandler = (params: HandlerParams, responseStream: ResponseStream) => void | Promise<void>;
199
+ ```
package/create.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { RouterApi } from './types.js';
1
+ import type { RouterApi, StreamRouterApi } from './types.js';
2
2
  export declare const createRouter: () => RouterApi;
3
+ export declare const createStreamRouter: () => StreamRouterApi;
package/create.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Router } from './router.js';
1
+ import { Router, StreamRouter } from './router.js';
2
2
  export const createRouter = () => {
3
3
  const router = new Router();
4
4
  const api = {
@@ -18,6 +18,10 @@ export const createRouter = () => {
18
18
  router.on('DELETE', path, handler);
19
19
  return api;
20
20
  },
21
+ fallback(handler) {
22
+ router.fallback(handler);
23
+ return api;
24
+ },
21
25
  patch(path, handler) {
22
26
  router.on('PATCH', path, handler);
23
27
  return api;
@@ -28,3 +32,36 @@ export const createRouter = () => {
28
32
  };
29
33
  return api;
30
34
  };
35
+ export const createStreamRouter = () => {
36
+ const router = new StreamRouter();
37
+ const api = {
38
+ get(path, handler) {
39
+ router.on('GET', path, handler);
40
+ return api;
41
+ },
42
+ post(path, handler) {
43
+ router.on('POST', path, handler);
44
+ return api;
45
+ },
46
+ put(path, handler) {
47
+ router.on('PUT', path, handler);
48
+ return api;
49
+ },
50
+ delete(path, handler) {
51
+ router.on('DELETE', path, handler);
52
+ return api;
53
+ },
54
+ fallback(handler) {
55
+ router.fallback(handler);
56
+ return api;
57
+ },
58
+ patch(path, handler) {
59
+ router.on('PATCH', path, handler);
60
+ return api;
61
+ },
62
+ route(event, responseStream) {
63
+ return router.route(event, responseStream);
64
+ },
65
+ };
66
+ return api;
67
+ };
package/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './create.js';
2
+ export * from './router.js';
2
3
  export type * from './types.js';
package/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from './create.js';
2
+ export * from './router.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vyriy/router",
3
- "version": "0.2.1",
3
+ "version": "0.3.2",
4
4
  "description": "Router utility for Vyriy projects",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/router.d.ts CHANGED
@@ -1,6 +1,14 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyResult, Handler } from './types.js';
2
- export declare class Router {
3
- private readonly routes;
4
- on(method: string, path: string, handler: Handler): this;
5
- route(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult>;
1
+ import type { APIGatewayProxyEvent, Handler, ResponseStream, RouteResult, StreamHandler } from './types.js';
2
+ declare class BaseRouter<CurrentHandler> {
3
+ protected fallbackHandler?: CurrentHandler;
4
+ protected readonly routes: Record<string, Record<string, CurrentHandler>>;
5
+ on(method: string, path: string, handler: CurrentHandler): this;
6
+ fallback(handler: CurrentHandler): this;
6
7
  }
8
+ export declare class Router extends BaseRouter<Handler> {
9
+ route(event: APIGatewayProxyEvent): Promise<RouteResult>;
10
+ }
11
+ export declare class StreamRouter extends BaseRouter<StreamHandler> {
12
+ route(event: APIGatewayProxyEvent, responseStream: ResponseStream): Promise<void>;
13
+ }
14
+ export {};
package/router.js CHANGED
@@ -1,39 +1,94 @@
1
1
  import { METHODS, STATUS_CODES } from 'node:http';
2
2
  const SUPPORTED_METHODS = new Set(METHODS.map((method) => method.toUpperCase()));
3
- export class Router {
3
+ const normalizeResult = (result) => ({
4
+ statusCode: result.statusCode ?? 200,
5
+ body: result.body,
6
+ headers: result.headers,
7
+ isBase64Encoded: result.isBase64Encoded,
8
+ multiValueHeaders: result.multiValueHeaders,
9
+ });
10
+ const registerRoute = (routes, method, path, handler) => {
11
+ const normalizedMethod = method.toUpperCase();
12
+ if (!SUPPORTED_METHODS.has(normalizedMethod)) {
13
+ throw new Error(`Unsupported HTTP method: ${normalizedMethod}`);
14
+ }
15
+ const routeGroup = (routes[normalizedMethod] ??= {});
16
+ if (routeGroup[path]) {
17
+ throw new Error(`${normalizedMethod} ${path} already exists!`);
18
+ }
19
+ routeGroup[path] = handler;
20
+ };
21
+ class BaseRouter {
22
+ fallbackHandler;
4
23
  routes = {};
5
24
  on(method, path, handler) {
6
- const normalizedMethod = method.toUpperCase();
7
- if (!SUPPORTED_METHODS.has(normalizedMethod)) {
8
- throw new Error(`Unsupported HTTP method: ${normalizedMethod}`);
9
- }
10
- const routeGroup = (this.routes[normalizedMethod] ??= {});
11
- if (routeGroup[path]) {
12
- throw new Error(`${normalizedMethod} ${path} already exists!`);
13
- }
14
- routeGroup[path] = handler;
25
+ registerRoute(this.routes, method, path, handler);
15
26
  return this;
16
27
  }
28
+ fallback(handler) {
29
+ this.fallbackHandler = handler;
30
+ return this;
31
+ }
32
+ }
33
+ export class Router extends BaseRouter {
17
34
  async route(event) {
18
35
  const { httpMethod, path, queryStringParameters, body, headers, pathParameters } = event;
19
- const result = await this.routes[httpMethod]?.[path]?.({
20
- query: queryStringParameters ?? undefined,
21
- body: body ?? undefined,
22
- headers,
23
- pathParameters: pathParameters ?? undefined,
24
- event,
25
- });
26
- return result
27
- ? {
28
- statusCode: result.statusCode ?? 200,
29
- body: result.body,
30
- headers: result.headers,
31
- }
32
- : {
33
- statusCode: 404,
34
- body: JSON.stringify({
35
- message: STATUS_CODES[404],
36
- }),
37
- };
36
+ const exactHandler = this.routes[httpMethod]?.[path];
37
+ if (exactHandler) {
38
+ const result = await exactHandler({
39
+ query: queryStringParameters ?? undefined,
40
+ body: body ?? undefined,
41
+ headers,
42
+ pathParameters: pathParameters ?? undefined,
43
+ event,
44
+ });
45
+ return normalizeResult(result);
46
+ }
47
+ if (this.fallbackHandler) {
48
+ const fallbackResult = await this.fallbackHandler({
49
+ query: queryStringParameters ?? undefined,
50
+ body: body ?? undefined,
51
+ headers,
52
+ pathParameters: pathParameters ?? undefined,
53
+ event,
54
+ });
55
+ return normalizeResult(fallbackResult);
56
+ }
57
+ return {
58
+ statusCode: 404,
59
+ body: JSON.stringify({
60
+ message: STATUS_CODES[404],
61
+ }),
62
+ };
63
+ }
64
+ }
65
+ export class StreamRouter extends BaseRouter {
66
+ async route(event, responseStream) {
67
+ const { httpMethod, path, queryStringParameters, body, headers, pathParameters } = event;
68
+ const exactHandler = this.routes[httpMethod]?.[path];
69
+ if (exactHandler) {
70
+ await exactHandler({
71
+ query: queryStringParameters ?? undefined,
72
+ body: body ?? undefined,
73
+ headers,
74
+ pathParameters: pathParameters ?? undefined,
75
+ event,
76
+ }, responseStream);
77
+ return;
78
+ }
79
+ if (this.fallbackHandler) {
80
+ await this.fallbackHandler({
81
+ query: queryStringParameters ?? undefined,
82
+ body: body ?? undefined,
83
+ headers,
84
+ pathParameters: pathParameters ?? undefined,
85
+ event,
86
+ }, responseStream);
87
+ return;
88
+ }
89
+ responseStream.setContentType?.('application/json');
90
+ responseStream.end(JSON.stringify({
91
+ message: STATUS_CODES[404],
92
+ }));
38
93
  }
39
94
  }
package/types.d.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  import type { APIGatewayProxyEvent, APIGatewayProxyEventQueryStringParameters, APIGatewayProxyResult } from 'aws-lambda';
2
2
  export type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
3
- export type Result = {
3
+ type MaybePromise<Result> = Result | Promise<Result>;
4
+ export type RouteResult = APIGatewayProxyResult;
5
+ export type RouteHandlerResult = {
4
6
  body: APIGatewayProxyResult['body'];
5
7
  headers?: APIGatewayProxyResult['headers'];
8
+ isBase64Encoded?: APIGatewayProxyResult['isBase64Encoded'];
9
+ multiValueHeaders?: APIGatewayProxyResult['multiValueHeaders'];
6
10
  statusCode?: APIGatewayProxyResult['statusCode'];
7
11
  };
12
+ export type ResponseStream = {
13
+ end: {
14
+ (): unknown;
15
+ (chunk: string | Buffer | Uint8Array): unknown;
16
+ };
17
+ setContentType?: (contentType: string) => unknown;
18
+ writableEnded?: boolean;
19
+ write(chunk: string | Buffer | Uint8Array): unknown;
20
+ };
8
21
  export type HandlerParams = {
9
22
  query?: APIGatewayProxyEventQueryStringParameters;
10
23
  body?: string;
@@ -12,12 +25,23 @@ export type HandlerParams = {
12
25
  pathParameters?: APIGatewayProxyEvent['pathParameters'];
13
26
  event: APIGatewayProxyEvent;
14
27
  };
15
- export type Handler = (params: HandlerParams) => Promise<Result | undefined>;
28
+ export type Handler = (params: HandlerParams) => MaybePromise<RouteHandlerResult>;
29
+ export type StreamHandler = (params: HandlerParams, responseStream: ResponseStream) => MaybePromise<void>;
16
30
  export type RouterApi = {
17
31
  get(path: string, handler: Handler): RouterApi;
18
32
  post(path: string, handler: Handler): RouterApi;
19
33
  put(path: string, handler: Handler): RouterApi;
20
34
  delete(path: string, handler: Handler): RouterApi;
35
+ fallback(handler: Handler): RouterApi;
21
36
  patch(path: string, handler: Handler): RouterApi;
22
- route(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult>;
37
+ route(event: APIGatewayProxyEvent): Promise<RouteResult>;
38
+ };
39
+ export type StreamRouterApi = {
40
+ get(path: string, handler: StreamHandler): StreamRouterApi;
41
+ post(path: string, handler: StreamHandler): StreamRouterApi;
42
+ put(path: string, handler: StreamHandler): StreamRouterApi;
43
+ delete(path: string, handler: StreamHandler): StreamRouterApi;
44
+ fallback(handler: StreamHandler): StreamRouterApi;
45
+ patch(path: string, handler: StreamHandler): StreamRouterApi;
46
+ route(event: APIGatewayProxyEvent, responseStream: ResponseStream): Promise<void>;
23
47
  };