astro-routify 1.1.0 → 1.2.1
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 +110 -59
- package/dist/core/defineHandler.d.ts +22 -0
- package/dist/core/defineHandler.js +25 -1
- package/dist/core/defineRoute.d.ts +32 -0
- package/dist/core/defineRoute.js +14 -1
- package/dist/core/defineRouter.d.ts +32 -1
- package/dist/core/defineRouter.js +25 -1
- package/dist/core/internal/createJsonStreamRoute.d.ts +64 -0
- package/dist/core/internal/createJsonStreamRoute.js +92 -0
- package/dist/core/responseHelpers.d.ts +62 -1
- package/dist/core/responseHelpers.js +54 -0
- package/dist/core/stream.d.ts +75 -0
- package/dist/core/stream.js +93 -0
- package/dist/core/streamJsonArray.d.ts +31 -0
- package/dist/core/streamJsonArray.js +30 -0
- package/dist/core/streamJsonND.d.ts +34 -0
- package/dist/core/streamJsonND.js +33 -0
- package/dist/index.d.ts +311 -3
- package/dist/index.js +3 -0
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -21,50 +21,52 @@ npm install astro-routify
|
|
|
21
21
|
```ts
|
|
22
22
|
// src/pages/api/index.ts
|
|
23
23
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
defineRoute,
|
|
25
|
+
defineRouter,
|
|
26
|
+
defineGroup,
|
|
27
|
+
HttpMethod,
|
|
28
|
+
ok,
|
|
29
29
|
} from 'astro-routify';
|
|
30
30
|
|
|
31
31
|
const userGroup = defineGroup('/users', (group) => {
|
|
32
|
-
|
|
32
|
+
group.addGet('/:id', ({params}) => ok({id: params.id}));
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
export const GET = defineRouter([
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
defineRoute(HttpMethod.GET, '/ping', () => ok('pong')),
|
|
37
|
+
...userGroup.getRoutes(),
|
|
38
38
|
]);
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Or to handle everything in a single place:
|
|
42
42
|
|
|
43
43
|
```ts
|
|
44
|
-
import {
|
|
44
|
+
import {RouterBuilder, ok} from 'astro-routify';
|
|
45
45
|
|
|
46
46
|
const builder = new RouterBuilder();
|
|
47
47
|
|
|
48
48
|
builder
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
.addGet('/ping', () => ok('pong'))
|
|
50
|
+
.addPost('/submit', async ({request}) => {
|
|
51
|
+
const body = await request.json();
|
|
52
|
+
return ok({received: body});
|
|
53
|
+
});
|
|
54
54
|
|
|
55
55
|
export const ALL = builder.build(); // catch-all
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## 💡 Full Example
|
|
59
59
|
|
|
60
|
-
You can find an implementation example in the [astro-routify-example](https://github.com/oamm/astro-routify-example)
|
|
60
|
+
You can find an implementation example in the [astro-routify-example](https://github.com/oamm/astro-routify-example)
|
|
61
|
+
repository.
|
|
61
62
|
It showcases a minimal Astro app with API endpoints configured under:
|
|
62
63
|
|
|
63
64
|
```text
|
|
64
65
|
/src/pages/api/[...path].ts
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
This setup demonstrates how to route requests dynamically using astro-routify, while still leveraging Astro's native
|
|
68
|
+
This setup demonstrates how to route requests dynamically using astro-routify, while still leveraging Astro's native
|
|
69
|
+
endpoint system.
|
|
68
70
|
|
|
69
71
|
---
|
|
70
72
|
|
|
@@ -78,9 +80,12 @@ This setup demonstrates how to route requests dynamically using astro-routify, w
|
|
|
78
80
|
- ✅ Built-in response helpers (`ok`, `created`, etc.)
|
|
79
81
|
- ✅ Trie-based matcher for fast route lookup
|
|
80
82
|
- ✅ Fully typed — no magic strings
|
|
83
|
+
- 🔁 **Streaming support**
|
|
84
|
+
- `stream()` — raw streaming with backpressure support (e.g. SSE, logs, custom protocols)
|
|
85
|
+
- `streamJsonND()` — newline-delimited JSON streaming (NDJSON)
|
|
86
|
+
- `streamJsonArray()` — server-side streamed JSON arrays
|
|
81
87
|
|
|
82
88
|
> 🔄 See [CHANGELOG.md](./CHANGELOG.md) for recent updates and improvements.
|
|
83
|
-
|
|
84
89
|
---
|
|
85
90
|
|
|
86
91
|
## 🧠 Core Concepts
|
|
@@ -91,7 +96,7 @@ Declare a single route:
|
|
|
91
96
|
|
|
92
97
|
```ts
|
|
93
98
|
defineRoute(HttpMethod.GET, "/users/:id", ({params}) => {
|
|
94
|
-
|
|
99
|
+
return ok({userId: params.id});
|
|
95
100
|
});
|
|
96
101
|
```
|
|
97
102
|
|
|
@@ -101,7 +106,7 @@ Group multiple routes under one HTTP method handler:
|
|
|
101
106
|
|
|
102
107
|
```ts
|
|
103
108
|
export const GET = defineRouter([
|
|
104
|
-
|
|
109
|
+
defineRoute(HttpMethod.GET, "/health", () => ok("ok"))
|
|
105
110
|
]);
|
|
106
111
|
```
|
|
107
112
|
|
|
@@ -109,17 +114,18 @@ export const GET = defineRouter([
|
|
|
109
114
|
|
|
110
115
|
### `RouterBuilder` (Catch-All & Fluent Builder)
|
|
111
116
|
|
|
112
|
-
Use `RouterBuilder` when you want to build routes dynamically, catch all HTTP methods via `ALL`, or organize routes more
|
|
117
|
+
Use `RouterBuilder` when you want to build routes dynamically, catch all HTTP methods via `ALL`, or organize routes more
|
|
118
|
+
fluently with helpers.
|
|
113
119
|
|
|
114
120
|
```ts
|
|
115
121
|
const builder = new RouterBuilder();
|
|
116
122
|
|
|
117
123
|
builder
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
.addGet("/ping", () => ok("pong"))
|
|
125
|
+
.addPost("/submit", async ({request}) => {
|
|
126
|
+
const body = await request.json();
|
|
127
|
+
return ok({received: body});
|
|
128
|
+
});
|
|
123
129
|
|
|
124
130
|
export const ALL = builder.build();
|
|
125
131
|
```
|
|
@@ -128,12 +134,13 @@ You can also group routes:
|
|
|
128
134
|
|
|
129
135
|
```ts
|
|
130
136
|
const users = defineGroup("/users")
|
|
131
|
-
|
|
137
|
+
.addGet("/:id", ({params}) => ok({id: params.id}));
|
|
132
138
|
|
|
133
139
|
builder.addGroup(users);
|
|
134
140
|
```
|
|
135
141
|
|
|
136
|
-
> 🔁 While `.register()` is still available, it's **deprecated** in favor of `.addGroup()` and `.addRoute()` for better
|
|
142
|
+
> 🔁 While `.register()` is still available, it's **deprecated** in favor of `.addGroup()` and `.addRoute()` for better
|
|
143
|
+
> structure and reusability.
|
|
137
144
|
|
|
138
145
|
---
|
|
139
146
|
|
|
@@ -151,12 +158,56 @@ notFound("Missing"); // 404
|
|
|
151
158
|
internalError(err); // 500
|
|
152
159
|
```
|
|
153
160
|
|
|
154
|
-
### File downloads
|
|
161
|
+
### 📄 File downloads
|
|
155
162
|
|
|
156
163
|
```ts
|
|
157
164
|
fileResponse(content, "application/pdf", "report.pdf"); // sets Content-Type and Content-Disposition
|
|
158
165
|
```
|
|
159
166
|
|
|
167
|
+
### 🔄 Streaming responses
|
|
168
|
+
|
|
169
|
+
#### Raw stream (e.g., Server-Sent Events)
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
stream('/clock', async ({response}) => {
|
|
173
|
+
const timer = setInterval(() => {
|
|
174
|
+
response.write(new Date().toISOString());
|
|
175
|
+
}, 1000);
|
|
176
|
+
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
clearInterval(timer);
|
|
179
|
+
response.close();
|
|
180
|
+
}, 5000);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### JSON NDStream (newline-delimited)
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
|
|
189
|
+
streamJsonND('/updates', async ({response}) => {
|
|
190
|
+
response.send({step: 1});
|
|
191
|
+
await delay(500);
|
|
192
|
+
response.send({step: 2});
|
|
193
|
+
response.close();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### JSON Array stream
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
|
|
202
|
+
streamJsonArray('/items', async ({response}) => {
|
|
203
|
+
for (let i = 0; i < 3; i++) {
|
|
204
|
+
response.send({id: i});
|
|
205
|
+
}
|
|
206
|
+
response.close();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
|
|
160
211
|
---
|
|
161
212
|
|
|
162
213
|
## 🔍 Param Matching
|
|
@@ -166,7 +217,7 @@ Any route param like `:id` is extracted into `ctx.params`:
|
|
|
166
217
|
```ts
|
|
167
218
|
const builder = new RouterBuilder();
|
|
168
219
|
|
|
169
|
-
builder.addGet("/users/:id", ({params}) => ok({userId:
|
|
220
|
+
builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
|
|
170
221
|
|
|
171
222
|
|
|
172
223
|
//OR
|
|
@@ -186,33 +237,33 @@ defineRoute(HttpMethod.GET, "/items/:id", ({params}) => {
|
|
|
186
237
|
```ts
|
|
187
238
|
// src/pages/api/[...slug].ts
|
|
188
239
|
export const GET = async ({request}) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
const url = new URL(request.url);
|
|
241
|
+
const path = url.pathname;
|
|
242
|
+
|
|
243
|
+
if (path.startsWith('/api/users/')) {
|
|
244
|
+
// Try to extract ID
|
|
245
|
+
const id = path.split('/').pop();
|
|
246
|
+
return new Response(JSON.stringify({id}), {
|
|
247
|
+
status: 200,
|
|
248
|
+
headers: {'Content-Type': 'application/json'},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (path === '/api/users') {
|
|
253
|
+
return new Response(JSON.stringify([{id: 1}, {id: 2}]), {
|
|
254
|
+
status: 200,
|
|
255
|
+
headers: {'Content-Type': 'application/json'},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (path === '/api/ping') {
|
|
260
|
+
return new Response(JSON.stringify({pong: true}), {
|
|
261
|
+
status: 200,
|
|
262
|
+
headers: {'Content-Type': 'application/json'}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return new Response('Not Found', {status: 404});
|
|
216
267
|
};
|
|
217
268
|
```
|
|
218
269
|
|
|
@@ -236,13 +287,13 @@ src/
|
|
|
236
287
|
|
|
237
288
|
const builder = new RouterBuilder();
|
|
238
289
|
builder.addGet("/ping", () => ok({pong: true}));
|
|
239
|
-
builder.addGet("/users/:id", ({params}) => ok({userId:
|
|
290
|
+
builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
|
|
240
291
|
|
|
241
292
|
// OR
|
|
242
293
|
|
|
243
294
|
export const ALL = defineRouter([
|
|
244
|
-
|
|
245
|
-
|
|
295
|
+
defineRoute(HttpMethod.GET, "/ping", () => ok({pong: true})),
|
|
296
|
+
defineRoute(HttpMethod.GET, "/users/:id", ({params}) => ok({id: params.id}))
|
|
246
297
|
]);
|
|
247
298
|
|
|
248
299
|
```
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import type { APIContext, APIRoute } from 'astro';
|
|
2
2
|
import { type ResultResponse } from './responseHelpers';
|
|
3
|
+
/**
|
|
4
|
+
* A flexible route handler that can return:
|
|
5
|
+
* - a native `Response` object,
|
|
6
|
+
* - a structured `ResultResponse` object,
|
|
7
|
+
* - or a file stream (Blob, ArrayBuffer, or ReadableStream).
|
|
8
|
+
*/
|
|
3
9
|
export type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> | ResultResponse | Response;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a `Handler` function into an `APIRoute` that:
|
|
12
|
+
* - logs requests and responses,
|
|
13
|
+
* - supports all valid `ResultResponse` return formats,
|
|
14
|
+
* - auto-converts structured responses into Astro `Response`s,
|
|
15
|
+
* - handles errors with standardized 500 output.
|
|
16
|
+
*
|
|
17
|
+
* @param handler - A handler function returning a `Response` or `ResultResponse`
|
|
18
|
+
* @returns An Astro-compatible `APIRoute` function
|
|
19
|
+
*/
|
|
4
20
|
export declare function defineHandler(handler: Handler): APIRoute;
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to detect ReadableStreams, used for streamed/binary responses.
|
|
23
|
+
*
|
|
24
|
+
* @param value - Any value to test
|
|
25
|
+
* @returns True if it looks like a ReadableStream
|
|
26
|
+
*/
|
|
5
27
|
export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import { internalError, toAstroResponse } from './responseHelpers';
|
|
2
|
+
/**
|
|
3
|
+
* Logs the incoming request method and path to the console.
|
|
4
|
+
*/
|
|
2
5
|
function logRequest(ctx) {
|
|
3
6
|
const { method, url } = ctx.request;
|
|
4
7
|
console.info(`[astro-routify] → ${method} ${new URL(url).pathname}`);
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Logs the response status and time taken.
|
|
11
|
+
*/
|
|
6
12
|
function logResponse(status, start) {
|
|
7
13
|
console.info(`[astro-routify] ← responded ${status} in ${Math.round(performance.now() - start)}ms`);
|
|
8
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a `Handler` function into an `APIRoute` that:
|
|
17
|
+
* - logs requests and responses,
|
|
18
|
+
* - supports all valid `ResultResponse` return formats,
|
|
19
|
+
* - auto-converts structured responses into Astro `Response`s,
|
|
20
|
+
* - handles errors with standardized 500 output.
|
|
21
|
+
*
|
|
22
|
+
* @param handler - A handler function returning a `Response` or `ResultResponse`
|
|
23
|
+
* @returns An Astro-compatible `APIRoute` function
|
|
24
|
+
*/
|
|
9
25
|
export function defineHandler(handler) {
|
|
10
26
|
return async (ctx) => {
|
|
11
27
|
const start = performance.now();
|
|
12
28
|
try {
|
|
13
29
|
logRequest(ctx);
|
|
14
30
|
const result = await handler(ctx);
|
|
31
|
+
// Native Response shortcut
|
|
15
32
|
if (result instanceof Response) {
|
|
16
33
|
logResponse(result.status, start);
|
|
17
34
|
return result;
|
|
18
35
|
}
|
|
19
|
-
//
|
|
36
|
+
// Direct binary or stream body (file download, etc.)
|
|
20
37
|
if (result?.body instanceof Blob ||
|
|
21
38
|
result?.body instanceof ArrayBuffer ||
|
|
22
39
|
isReadableStream(result?.body)) {
|
|
@@ -27,6 +44,7 @@ export function defineHandler(handler) {
|
|
|
27
44
|
logResponse(res.status, start);
|
|
28
45
|
return res;
|
|
29
46
|
}
|
|
47
|
+
// Structured ResultResponse → native Astro Response
|
|
30
48
|
const finalResponse = toAstroResponse(result);
|
|
31
49
|
logResponse(finalResponse.status, start);
|
|
32
50
|
return finalResponse;
|
|
@@ -39,6 +57,12 @@ export function defineHandler(handler) {
|
|
|
39
57
|
}
|
|
40
58
|
};
|
|
41
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Type guard to detect ReadableStreams, used for streamed/binary responses.
|
|
62
|
+
*
|
|
63
|
+
* @param value - Any value to test
|
|
64
|
+
* @returns True if it looks like a ReadableStream
|
|
65
|
+
*/
|
|
42
66
|
export function isReadableStream(value) {
|
|
43
67
|
return (typeof value === 'object' &&
|
|
44
68
|
value !== null &&
|
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { HttpMethod } from './HttpMethod';
|
|
2
2
|
import type { Handler } from './defineHandler';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single route definition.
|
|
5
|
+
*/
|
|
3
6
|
export interface Route {
|
|
7
|
+
/**
|
|
8
|
+
* HTTP method to match (GET, POST, PUT, etc.).
|
|
9
|
+
*/
|
|
4
10
|
method: HttpMethod;
|
|
11
|
+
/**
|
|
12
|
+
* Path pattern, starting with `/`, supporting static or param segments (e.g., `/users/:id`).
|
|
13
|
+
*/
|
|
5
14
|
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* The function that handles the request when matched.
|
|
17
|
+
*/
|
|
6
18
|
handler: Handler;
|
|
7
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Defines a route using a `Route` object.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* defineRoute({ method: 'GET', path: '/users', handler });
|
|
25
|
+
*
|
|
26
|
+
* @param route - A fully constructed route object
|
|
27
|
+
* @returns The validated Route object
|
|
28
|
+
*/
|
|
8
29
|
export declare function defineRoute(route: Route): Route;
|
|
30
|
+
/**
|
|
31
|
+
* Defines a route by specifying its method, path, and handler explicitly.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* defineRoute('POST', '/login', handler);
|
|
35
|
+
*
|
|
36
|
+
* @param method - HTTP method to match
|
|
37
|
+
* @param path - Route path (must start with `/`)
|
|
38
|
+
* @param handler - Function to handle the matched request
|
|
39
|
+
* @returns The validated Route object
|
|
40
|
+
*/
|
|
9
41
|
export declare function defineRoute(method: HttpMethod, path: string, handler: Handler): Route;
|
package/dist/core/defineRoute.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { ALLOWED_HTTP_METHODS } from './HttpMethod';
|
|
2
|
+
/**
|
|
3
|
+
* Internal route definition logic that supports both overloads.
|
|
4
|
+
*/
|
|
2
5
|
export function defineRoute(methodOrRoute, maybePath, maybeHandler) {
|
|
3
6
|
if (typeof methodOrRoute === 'object') {
|
|
4
7
|
validateRoute(methodOrRoute);
|
|
5
8
|
return methodOrRoute;
|
|
6
9
|
}
|
|
7
|
-
const route = {
|
|
10
|
+
const route = {
|
|
11
|
+
method: methodOrRoute,
|
|
12
|
+
path: maybePath,
|
|
13
|
+
handler: maybeHandler,
|
|
14
|
+
};
|
|
8
15
|
validateRoute(route);
|
|
9
16
|
return route;
|
|
10
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Ensures the route is properly formed and uses a valid method + path format.
|
|
20
|
+
*
|
|
21
|
+
* @param route - Route to validate
|
|
22
|
+
* @throws If method is unsupported or path doesn't start with `/`
|
|
23
|
+
*/
|
|
11
24
|
function validateRoute({ method, path }) {
|
|
12
25
|
if (!path.startsWith('/')) {
|
|
13
26
|
throw new Error(`Route path must start with '/': ${path}`);
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { notFound } from './responseHelpers';
|
|
3
3
|
import type { Route } from './defineRoute';
|
|
4
|
+
/**
|
|
5
|
+
* Optional configuration for the router instance.
|
|
6
|
+
*/
|
|
4
7
|
export interface RouterOptions {
|
|
8
|
+
/**
|
|
9
|
+
* A base path to strip from the incoming request path (default: `/api`).
|
|
10
|
+
* Only routes beneath this prefix will be matched.
|
|
11
|
+
*/
|
|
5
12
|
basePath?: string;
|
|
6
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Custom handler to return when no route is matched (404).
|
|
15
|
+
*/
|
|
7
16
|
onNotFound?: () => ReturnType<typeof notFound>;
|
|
8
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Defines a router that dynamically matches registered routes based on method and path.
|
|
20
|
+
*
|
|
21
|
+
* This allows building a clean, centralized API routing system with features like:
|
|
22
|
+
* - Trie-based fast route lookup
|
|
23
|
+
* - Per-method matching with 405 fallback
|
|
24
|
+
* - Parameter extraction (e.g. `/users/:id`)
|
|
25
|
+
* - Customizable basePath and 404 behavior
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* defineRouter([
|
|
29
|
+
* defineRoute('GET', '/users', handler),
|
|
30
|
+
* defineRoute('POST', '/login', loginHandler),
|
|
31
|
+
* ], {
|
|
32
|
+
* basePath: '/api',
|
|
33
|
+
* onNotFound: () => notFound('No such route')
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* @param routes - An array of route definitions (see `defineRoute`)
|
|
37
|
+
* @param options - Optional router config (basePath, custom 404)
|
|
38
|
+
* @returns An Astro-compatible APIRoute handler
|
|
39
|
+
*/
|
|
9
40
|
export declare function defineRouter(routes: Route[], options?: RouterOptions): APIRoute;
|
|
@@ -2,6 +2,28 @@ import { defineHandler } from './defineHandler';
|
|
|
2
2
|
import { methodNotAllowed, notFound, toAstroResponse } from './responseHelpers';
|
|
3
3
|
import { RouteTrie } from './RouteTrie';
|
|
4
4
|
import { normalizeMethod } from './HttpMethod';
|
|
5
|
+
/**
|
|
6
|
+
* Defines a router that dynamically matches registered routes based on method and path.
|
|
7
|
+
*
|
|
8
|
+
* This allows building a clean, centralized API routing system with features like:
|
|
9
|
+
* - Trie-based fast route lookup
|
|
10
|
+
* - Per-method matching with 405 fallback
|
|
11
|
+
* - Parameter extraction (e.g. `/users/:id`)
|
|
12
|
+
* - Customizable basePath and 404 behavior
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* defineRouter([
|
|
16
|
+
* defineRoute('GET', '/users', handler),
|
|
17
|
+
* defineRoute('POST', '/login', loginHandler),
|
|
18
|
+
* ], {
|
|
19
|
+
* basePath: '/api',
|
|
20
|
+
* onNotFound: () => notFound('No such route')
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @param routes - An array of route definitions (see `defineRoute`)
|
|
24
|
+
* @param options - Optional router config (basePath, custom 404)
|
|
25
|
+
* @returns An Astro-compatible APIRoute handler
|
|
26
|
+
*/
|
|
5
27
|
export function defineRouter(routes, options = {}) {
|
|
6
28
|
const trie = new RouteTrie();
|
|
7
29
|
for (const route of routes) {
|
|
@@ -18,15 +40,17 @@ export function defineRouter(routes, options = {}) {
|
|
|
18
40
|
const method = normalizeMethod(ctx.request.method);
|
|
19
41
|
const { handler, allowed, params } = trie.find(path, method);
|
|
20
42
|
if (!handler) {
|
|
21
|
-
//
|
|
43
|
+
// Method exists but not allowed for this route
|
|
22
44
|
if (allowed && allowed.length) {
|
|
23
45
|
return toAstroResponse(methodNotAllowed('Method Not Allowed', {
|
|
24
46
|
Allow: allowed.join(', '),
|
|
25
47
|
}));
|
|
26
48
|
}
|
|
49
|
+
// No route matched at all → 404
|
|
27
50
|
const notFoundHandler = options.onNotFound ? options.onNotFound() : notFound('Not Found');
|
|
28
51
|
return toAstroResponse(notFoundHandler);
|
|
29
52
|
}
|
|
53
|
+
// Match found → delegate to handler
|
|
30
54
|
return defineHandler(handler)({ ...ctx, params: { ...ctx.params, ...params } });
|
|
31
55
|
};
|
|
32
56
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { HttpMethod } from '../HttpMethod';
|
|
3
|
+
import { type Route } from '../defineRoute';
|
|
4
|
+
type JsonValue = any;
|
|
5
|
+
/**
|
|
6
|
+
* A writer interface for streaming JSON data to the response body.
|
|
7
|
+
* Supports both NDJSON and array formats.
|
|
8
|
+
*/
|
|
9
|
+
export interface JsonStreamWriter {
|
|
10
|
+
/**
|
|
11
|
+
* Send a JSON-serializable value to the response stream.
|
|
12
|
+
* - In `ndjson` mode: appends a newline after each object.
|
|
13
|
+
* - In `array` mode: adds commas and wraps with brackets.
|
|
14
|
+
* @param value - Any serializable object or array item
|
|
15
|
+
*/
|
|
16
|
+
send: (value: JsonValue) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Write raw text or bytes to the stream.
|
|
19
|
+
* Used for low-level control if needed.
|
|
20
|
+
*/
|
|
21
|
+
write: (chunk: string | Uint8Array) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Close the stream. In `array` mode, it writes the closing `]`.
|
|
24
|
+
*/
|
|
25
|
+
close: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Override response headers dynamically before the response is sent.
|
|
28
|
+
* Only safe before the first write.
|
|
29
|
+
* @param key - Header name
|
|
30
|
+
* @param value - Header value
|
|
31
|
+
*/
|
|
32
|
+
setHeader: (key: string, value: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Internal configuration options for `createJsonStreamRoute`.
|
|
36
|
+
*/
|
|
37
|
+
interface JsonStreamOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Streaming mode: 'ndjson' or 'array'.
|
|
40
|
+
*/
|
|
41
|
+
mode: 'ndjson' | 'array';
|
|
42
|
+
/**
|
|
43
|
+
* HTTP method to define (e.g. GET, POST).
|
|
44
|
+
*/
|
|
45
|
+
method: HttpMethod;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a streaming JSON route that supports both NDJSON and JSON array formats.
|
|
49
|
+
*
|
|
50
|
+
* - Sets appropriate `Content-Type` headers
|
|
51
|
+
* - Supports cancellation via `AbortSignal`
|
|
52
|
+
* - Provides a `response` object with `send`, `close`, `write`, `setHeader` methods
|
|
53
|
+
*
|
|
54
|
+
* Use this function inside `streamJsonND()` or `streamJsonArray()` to avoid duplication.
|
|
55
|
+
*
|
|
56
|
+
* @param path - The route path (e.g. `/logs`)
|
|
57
|
+
* @param handler - A function that receives Astro `ctx` and a `JsonStreamWriter`
|
|
58
|
+
* @param options - Streaming options (`mode`, `method`)
|
|
59
|
+
* @returns A streaming-compatible Route
|
|
60
|
+
*/
|
|
61
|
+
export declare function createJsonStreamRoute(path: string, handler: (ctx: APIContext & {
|
|
62
|
+
response: JsonStreamWriter;
|
|
63
|
+
}) => void | Promise<void>, options: JsonStreamOptions): Route;
|
|
64
|
+
export {};
|