astro-routify 1.0.0 → 1.1.0

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
@@ -3,44 +3,75 @@
3
3
  **A high-performance API router for [Astro](https://astro.build/) built on a Trie matcher.**
4
4
  Define API routes using clean, flat structures — no folders or boilerplate logic.
5
5
 
6
+ ![npm](https://img.shields.io/npm/v/astro-routify)
7
+ ![license](https://img.shields.io/npm/l/astro-routify)
8
+ ![downloads](https://img.shields.io/npm/dt/astro-routify)
9
+ ![feedback-welcome](https://img.shields.io/badge/feedback-welcome-blue)
10
+
6
11
  ---
7
12
 
13
+ ## Installing
14
+
15
+ ```shell
16
+ npm install astro-routify
17
+ ```
18
+
8
19
  ## ⚡️ Quickstart
9
20
 
10
21
  ```ts
11
22
  // src/pages/api/index.ts
12
- import { defineRoute, defineRouter, HttpMethod, ok } from "astro-routify";
23
+ import {
24
+ defineRoute,
25
+ defineRouter,
26
+ defineGroup,
27
+ HttpMethod,
28
+ ok,
29
+ } from 'astro-routify';
30
+
31
+ const userGroup = defineGroup('/users', (group) => {
32
+ group.addGet('/:id', ({ params }) => ok({ id: params.id }));
33
+ });
13
34
 
14
35
  export const GET = defineRouter([
15
- defineRoute(HttpMethod.GET, "/ping", () => ok("pong")),
16
- defineRoute(HttpMethod.GET, "/users/:id", ({ params }) => ok({ id: params.id }))
36
+ defineRoute(HttpMethod.GET, '/ping', () => ok('pong')),
37
+ ...userGroup.getRoutes(),
17
38
  ]);
18
39
  ```
19
40
 
20
41
  Or to handle everything in a single place:
21
42
 
22
43
  ```ts
23
- import { RouterBuilder, defineRoute, HttpMethod, ok } from "astro-routify";
44
+ import { RouterBuilder, ok } from 'astro-routify';
24
45
 
25
46
  const builder = new RouterBuilder();
26
- builder.register([
27
- defineRoute(HttpMethod.GET, "/ping", () => ok("pong")),
28
- defineRoute(HttpMethod.POST, "/submit", async ({ request }) => {
47
+
48
+ builder
49
+ .addGet('/ping', () => ok('pong'))
50
+ .addPost('/submit', async ({ request }) => {
29
51
  const body = await request.json();
30
52
  return ok({ received: body });
31
- })
32
- ]);
53
+ });
33
54
 
34
55
  export const ALL = builder.build(); // catch-all
35
56
  ```
36
57
 
58
+ ## 💡 Full Example
59
+
60
+ You can find an implementation example in the [astro-routify-example](https://github.com/oamm/astro-routify-example) repository.
61
+ It showcases a minimal Astro app with API endpoints configured under:
62
+
63
+ ```text
64
+ /src/pages/api/[...path].ts
65
+ ```
66
+
67
+ This setup demonstrates how to route requests dynamically using astro-routify, while still leveraging Astro's native endpoint system.
68
+
37
69
  ---
38
70
 
39
71
  ## 🚀 Features
40
72
 
41
73
  - ⚡ Fully compatible with Astro’s native APIContext — no extra setup needed.
42
- - 🧩 Use middleware, access cookies, headers, and request bodies exactly as you would in a normal Astro endpoints.
43
-
74
+ - 🧩 Use middleware, access cookies, headers, and request bodies exactly as you would in a normal Astro endpoint.
44
75
  - ✅ Flat-file, code-based routing (no folders required)
45
76
  - ✅ Dynamic segments (`:id`)
46
77
  - ✅ ALL-mode for monolithic routing (`RouterBuilder`)
@@ -48,6 +79,8 @@ export const ALL = builder.build(); // catch-all
48
79
  - ✅ Trie-based matcher for fast route lookup
49
80
  - ✅ Fully typed — no magic strings
50
81
 
82
+ > 🔄 See [CHANGELOG.md](./CHANGELOG.md) for recent updates and improvements.
83
+
51
84
  ---
52
85
 
53
86
  ## 🧠 Core Concepts
@@ -57,8 +90,8 @@ export const ALL = builder.build(); // catch-all
57
90
  Declare a single route:
58
91
 
59
92
  ```ts
60
- defineRoute(HttpMethod.GET, "/users/:id", ({ params }) => {
61
- return ok({ userId: params.id });
93
+ defineRoute(HttpMethod.GET, "/users/:id", ({params}) => {
94
+ return ok({userId: params.id});
62
95
  });
63
96
  ```
64
97
 
@@ -74,18 +107,34 @@ export const GET = defineRouter([
74
107
 
75
108
  > 🧠 `defineRouter()` supports all HTTP methods — but Astro only executes the method you export (`GET`, `POST`, etc.)
76
109
 
77
- ### `RouterBuilder` (Catch-All)
110
+ ### `RouterBuilder` (Catch-All & Fluent Builder)
78
111
 
79
- Designed specifically for `ALL`:
112
+ Use `RouterBuilder` when you want to build routes dynamically, catch all HTTP methods via `ALL`, or organize routes more fluently with helpers.
80
113
 
81
114
  ```ts
82
115
  const builder = new RouterBuilder();
83
- builder.register([
84
- defineRoute(HttpMethod.GET, "/info", () => ok({ app: "astro-routify" }))
85
- ]);
116
+
117
+ builder
118
+ .addGet("/ping", () => ok("pong"))
119
+ .addPost("/submit", async ({request}) => {
120
+ const body = await request.json();
121
+ return ok({received: body});
122
+ });
123
+
86
124
  export const ALL = builder.build();
87
125
  ```
88
126
 
127
+ You can also group routes:
128
+
129
+ ```ts
130
+ const users = defineGroup("/users")
131
+ .addGet("/:id", ({params}) => ok({id: params.id}));
132
+
133
+ builder.addGroup(users);
134
+ ```
135
+
136
+ > 🔁 While `.register()` is still available, it's **deprecated** in favor of `.addGroup()` and `.addRoute()` for better structure and reusability.
137
+
89
138
  ---
90
139
 
91
140
  ## 🔁 Response Helpers
@@ -93,11 +142,19 @@ export const ALL = builder.build();
93
142
  Avoid boilerplate `new Response(JSON.stringify(...))`:
94
143
 
95
144
  ```ts
96
- ok(data); // 200 OK
97
- created(data); // 201 Created
98
- noContent(); // 204
99
- notFound("Missing"); // 404
100
- internalError(err); // 500
145
+ import {fileResponse} from 'astro-routify';
146
+
147
+ ok(data); // 200 OK
148
+ created(data); // 201 Created
149
+ noContent(); // 204
150
+ notFound("Missing"); // 404
151
+ internalError(err); // 500
152
+ ```
153
+
154
+ ### File downloads
155
+
156
+ ```ts
157
+ fileResponse(content, "application/pdf", "report.pdf"); // sets Content-Type and Content-Disposition
101
158
  ```
102
159
 
103
160
  ---
@@ -107,9 +164,17 @@ internalError(err); // 500
107
164
  Any route param like `:id` is extracted into `ctx.params`:
108
165
 
109
166
  ```ts
110
- defineRoute(HttpMethod.GET, "/items/:id", ({ params }) => {
111
- return ok({ itemId: params.id });
167
+ const builder = new RouterBuilder();
168
+
169
+ builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
170
+
171
+
172
+ //OR
173
+
174
+ defineRoute(HttpMethod.GET, "/items/:id", ({params}) => {
175
+ return ok({itemId: params.id});
112
176
  });
177
+
113
178
  ```
114
179
 
115
180
  ---
@@ -121,35 +186,33 @@ defineRoute(HttpMethod.GET, "/items/:id", ({ params }) => {
121
186
  ```ts
122
187
  // src/pages/api/[...slug].ts
123
188
  export const GET = async ({request}) => {
124
- const url = new URL(request.url);
125
- const path = url.pathname;
126
-
127
- if (path.startsWith('/api/users/')) {
128
- // Try to extract ID
129
- const id = path.split('/').pop();
130
- return new Response(JSON.stringify({id}), {
131
- status: 200,
132
- headers: {'Content-Type': 'application/json'},
133
- });
134
- }
135
-
136
- if (path === '/api/users') {
137
- return new Response(JSON.stringify([{id: 1}, {id: 2}]), {
138
- status: 200,
139
- headers: {'Content-Type': 'application/json'},
140
- });
141
- }
142
-
143
-
144
- if (path === '/api/ping') {
145
- return new Response(JSON.stringify({pong: true}), {
146
- status: 200,
147
- headers: {'Content-Type': 'application/json'}
148
- });
149
- }
150
-
151
-
152
- return new Response('Not Found', {status: 404});
189
+ const url = new URL(request.url);
190
+ const path = url.pathname;
191
+
192
+ if (path.startsWith('/api/users/')) {
193
+ // Try to extract ID
194
+ const id = path.split('/').pop();
195
+ return new Response(JSON.stringify({id}), {
196
+ status: 200,
197
+ headers: {'Content-Type': 'application/json'},
198
+ });
199
+ }
200
+
201
+ if (path === '/api/users') {
202
+ return new Response(JSON.stringify([{id: 1}, {id: 2}]), {
203
+ status: 200,
204
+ headers: {'Content-Type': 'application/json'},
205
+ });
206
+ }
207
+
208
+ if (path === '/api/ping') {
209
+ return new Response(JSON.stringify({pong: true}), {
210
+ status: 200,
211
+ headers: {'Content-Type': 'application/json'}
212
+ });
213
+ }
214
+
215
+ return new Response('Not Found', {status: 404});
153
216
  };
154
217
  ```
155
218
 
@@ -169,11 +232,19 @@ src/
169
232
  ### ✅ With `astro-routify`
170
233
 
171
234
  ```ts
172
- //src/pages/api/[...slug].ts
235
+ // src/pages/api/[...slug].ts
236
+
237
+ const builder = new RouterBuilder();
238
+ builder.addGet("/ping", () => ok({pong: true}));
239
+ builder.addGet("/users/:id", ({params}) => ok({userId: params.id}));
240
+
241
+ // OR
242
+
173
243
  export const ALL = defineRouter([
174
- defineRoute(HttpMethod.GET, "/ping", () => ok({ pong: true })),
175
- defineRoute(HttpMethod.GET, "/users/:id", ({ params }) => ok({ id: params.id }))
244
+ defineRoute(HttpMethod.GET, "/ping", () => ok({pong: true})),
245
+ defineRoute(HttpMethod.GET, "/users/:id", ({params}) => ok({id: params.id}))
176
246
  ]);
247
+
177
248
  ```
178
249
 
179
250
  ---
@@ -196,11 +267,10 @@ Tests ran on a mid-range development setup:
196
267
  - **GPU**: NVIDIA GeForce GTX 1080 (8 GB)
197
268
  - **OS**: Windows 10 Pro 64-bit
198
269
  - **Node.js**: v20.x
199
- - **Benchmark Tool**: [Vitest Bench](https://vitest.dev/guide/benchmarks.html)
270
+ - **Benchmark Tool**: [Vitest Bench](https://vitest.dev/guide/features.html#benchmarking)
200
271
 
201
272
  Results may vary slightly on different hardware.
202
273
 
203
-
204
274
  ### 🔬 Realistic route shapes (5000 registered routes):
205
275
 
206
276
  ```
@@ -225,11 +295,13 @@ Results may vary slightly on different hardware.
225
295
  ```
226
296
 
227
297
  > ⚡ Performance stays consistently fast even with 10k+ routes
298
+
228
299
  ---
229
300
 
230
301
  ## 🛠 Designed to Scale
231
302
 
232
- While focused on simplicity and speed today, `astro-routify` is designed to evolve — enabling more advanced routing patterns in the future.
303
+ While focused on simplicity and speed today, `astro-routify` is designed to evolve — enabling more advanced routing
304
+ patterns in the future.
233
305
 
234
306
  ---
235
307
 
@@ -1,8 +1,119 @@
1
- import type { Route } from './defineRoute';
1
+ import { type Route } from './defineRoute';
2
2
  import { type RouterOptions } from './defineRouter';
3
+ import { RouteGroup } from './defineGroup';
4
+ import { type Handler } from './defineHandler';
5
+ /**
6
+ * A fluent builder for creating and composing API routes in Astro.
7
+ *
8
+ * `RouterBuilder` supports both simple method-based additions (`addGet`, `addPost`, etc.)
9
+ * and organized groups via `defineGroup()` + `addGroup()` for scalable applications.
10
+ *
11
+ * @example Basic usage:
12
+ * ```ts
13
+ * const router = new RouterBuilder()
14
+ * .addGet('/ping', () => ok('pong'))
15
+ * .addPost('/submit', handler);
16
+ *
17
+ * export const ALL = router.build();
18
+ * ```
19
+ *
20
+ * @example Using groups:
21
+ * ```ts
22
+ * const users = defineGroup('/users')
23
+ * .addGet('/:id', userHandler);
24
+ *
25
+ * const router = new RouterBuilder({ basePath: '/api' })
26
+ * .addGroup(users);
27
+ *
28
+ * export const GET = router.build();
29
+ * ```
30
+ */
3
31
  export declare class RouterBuilder {
4
- private routes;
32
+ private _options;
33
+ private _routes;
34
+ private static _registerWarned;
35
+ constructor(options?: RouterOptions);
36
+ /**
37
+ * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
38
+ *
39
+ * Registers a single route manually.
40
+ *
41
+ * This method is deprecated in favor of a more scalable and organized approach using
42
+ * `defineGroup()` + `addGroup()` or `addRoute()` for ad-hoc additions.
43
+ *
44
+ * @param route A single `Route` object to register.
45
+ */
5
46
  register(route: Route): void;
47
+ /**
48
+ * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
49
+ *
50
+ * Registers multiple routes at once.
51
+ *
52
+ * This method is deprecated and discouraged for larger codebases in favor of group-based composition.
53
+ *
54
+ * @param routes An array of `Route` objects to register.
55
+ */
6
56
  register(routes: Route[]): void;
7
- build(options?: RouterOptions): import("astro").APIRoute;
57
+ /**
58
+ * Adds a single route to the router.
59
+ *
60
+ * @param route The route to add.
61
+ * @returns The current builder instance (for chaining).
62
+ */
63
+ addRoute(route: Route): this;
64
+ /**
65
+ * Adds a group of pre-defined routes (from `defineGroup()`).
66
+ *
67
+ * @param group A `RouteGroup` instance.
68
+ * @returns The current builder instance (for chaining).
69
+ */
70
+ addGroup(group: RouteGroup): this;
71
+ /**
72
+ * Adds a GET route.
73
+ * @param path Route path (e.g., `/items/:id`)
74
+ * @param handler Request handler function.
75
+ */
76
+ addGet(path: string, handler: Handler): this;
77
+ /**
78
+ * Adds a POST route.
79
+ * @param path Route path.
80
+ * @param handler Request handler function.
81
+ */
82
+ addPost(path: string, handler: Handler): this;
83
+ /**
84
+ * Adds a PUT route.
85
+ * @param path Route path.
86
+ * @param handler Request handler function.
87
+ */
88
+ addPut(path: string, handler: Handler): this;
89
+ /**
90
+ * Adds a DELETE route.
91
+ * @param path Route path.
92
+ * @param handler Request handler function.
93
+ */
94
+ addDelete(path: string, handler: Handler): this;
95
+ /**
96
+ * Adds a PATCH route.
97
+ * @param path Route path.
98
+ * @param handler Request handler function.
99
+ */
100
+ addPatch(path: string, handler: Handler): this;
101
+ /**
102
+ * Internal helper to add a route with any HTTP method.
103
+ *
104
+ * @param method The HTTP method.
105
+ * @param subPath Path segment (can be relative or absolute).
106
+ * @param handler Request handler.
107
+ * @returns The current builder instance (for chaining).
108
+ */
109
+ private add;
110
+ /**
111
+ * Finalizes the router and returns an Astro-compatible route handler.
112
+ *
113
+ * This function should be used to export a method like `GET`, `POST`, or `ALL`
114
+ * inside Astro API endpoints.
115
+ *
116
+ * @returns A fully resolved Astro route handler.
117
+ */
118
+ build(): import("astro").APIRoute;
8
119
  }
@@ -1,17 +1,144 @@
1
+ import { defineRoute } from './defineRoute';
1
2
  import { defineRouter } from './defineRouter';
3
+ import { HttpMethod } from './HttpMethod';
4
+ /**
5
+ * A fluent builder for creating and composing API routes in Astro.
6
+ *
7
+ * `RouterBuilder` supports both simple method-based additions (`addGet`, `addPost`, etc.)
8
+ * and organized groups via `defineGroup()` + `addGroup()` for scalable applications.
9
+ *
10
+ * @example Basic usage:
11
+ * ```ts
12
+ * const router = new RouterBuilder()
13
+ * .addGet('/ping', () => ok('pong'))
14
+ * .addPost('/submit', handler);
15
+ *
16
+ * export const ALL = router.build();
17
+ * ```
18
+ *
19
+ * @example Using groups:
20
+ * ```ts
21
+ * const users = defineGroup('/users')
22
+ * .addGet('/:id', userHandler);
23
+ *
24
+ * const router = new RouterBuilder({ basePath: '/api' })
25
+ * .addGroup(users);
26
+ *
27
+ * export const GET = router.build();
28
+ * ```
29
+ */
2
30
  export class RouterBuilder {
3
- constructor() {
4
- this.routes = [];
31
+ constructor(options) {
32
+ this._routes = [];
33
+ this._options = {
34
+ basePath: 'api',
35
+ ...options,
36
+ };
5
37
  }
38
+ /**
39
+ * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
40
+ *
41
+ * Registers one or more routes, supporting both a single `Route` or an array of them.
42
+ *
43
+ * Internally used by the two overloads above. Emits a console warning once per runtime.
44
+ *
45
+ * @param routeOrRoutes Either a single `Route` or an array of `Route`s.
46
+ */
6
47
  register(routeOrRoutes) {
48
+ if (!RouterBuilder._registerWarned) {
49
+ console.warn('[RouterBuilder] register() is deprecated. Use defineGroup() + addGroup() for route grouping and better structure.');
50
+ RouterBuilder._registerWarned = true;
51
+ }
7
52
  if (Array.isArray(routeOrRoutes)) {
8
- this.routes.push(...routeOrRoutes);
53
+ this._routes.push(...routeOrRoutes);
9
54
  }
10
55
  else {
11
- this.routes.push(routeOrRoutes);
56
+ this._routes.push(routeOrRoutes);
12
57
  }
13
58
  }
14
- build(options) {
15
- return defineRouter(this.routes, options);
59
+ /**
60
+ * Adds a single route to the router.
61
+ *
62
+ * @param route The route to add.
63
+ * @returns The current builder instance (for chaining).
64
+ */
65
+ addRoute(route) {
66
+ this._routes.push(route);
67
+ return this;
68
+ }
69
+ /**
70
+ * Adds a group of pre-defined routes (from `defineGroup()`).
71
+ *
72
+ * @param group A `RouteGroup` instance.
73
+ * @returns The current builder instance (for chaining).
74
+ */
75
+ addGroup(group) {
76
+ this._routes.push(...group.getRoutes());
77
+ return this;
78
+ }
79
+ /**
80
+ * Adds a GET route.
81
+ * @param path Route path (e.g., `/items/:id`)
82
+ * @param handler Request handler function.
83
+ */
84
+ addGet(path, handler) {
85
+ return this.add(HttpMethod.GET, path, handler);
86
+ }
87
+ /**
88
+ * Adds a POST route.
89
+ * @param path Route path.
90
+ * @param handler Request handler function.
91
+ */
92
+ addPost(path, handler) {
93
+ return this.add(HttpMethod.POST, path, handler);
94
+ }
95
+ /**
96
+ * Adds a PUT route.
97
+ * @param path Route path.
98
+ * @param handler Request handler function.
99
+ */
100
+ addPut(path, handler) {
101
+ return this.add(HttpMethod.PUT, path, handler);
102
+ }
103
+ /**
104
+ * Adds a DELETE route.
105
+ * @param path Route path.
106
+ * @param handler Request handler function.
107
+ */
108
+ addDelete(path, handler) {
109
+ return this.add(HttpMethod.DELETE, path, handler);
110
+ }
111
+ /**
112
+ * Adds a PATCH route.
113
+ * @param path Route path.
114
+ * @param handler Request handler function.
115
+ */
116
+ addPatch(path, handler) {
117
+ return this.add(HttpMethod.PATCH, path, handler);
118
+ }
119
+ /**
120
+ * Internal helper to add a route with any HTTP method.
121
+ *
122
+ * @param method The HTTP method.
123
+ * @param subPath Path segment (can be relative or absolute).
124
+ * @param handler Request handler.
125
+ * @returns The current builder instance (for chaining).
126
+ */
127
+ add(method, subPath, handler) {
128
+ const normalizedPath = subPath.startsWith('/') ? subPath : `/${subPath}`;
129
+ this._routes.push(defineRoute(method, normalizedPath, handler));
130
+ return this;
131
+ }
132
+ /**
133
+ * Finalizes the router and returns an Astro-compatible route handler.
134
+ *
135
+ * This function should be used to export a method like `GET`, `POST`, or `ALL`
136
+ * inside Astro API endpoints.
137
+ *
138
+ * @returns A fully resolved Astro route handler.
139
+ */
140
+ build() {
141
+ return defineRouter(this._routes, this._options);
16
142
  }
17
143
  }
144
+ RouterBuilder._registerWarned = false;
@@ -0,0 +1,87 @@
1
+ import { type Route } from './defineRoute';
2
+ import { Handler } from "./defineHandler";
3
+ /**
4
+ * Represents a group of routes under a shared base path.
5
+ *
6
+ * Use this class to organize related endpoints, applying a consistent prefix
7
+ * and reducing duplication when defining similar routes.
8
+ *
9
+ * @example
10
+ * const users = new RouteGroup('/users')
11
+ * .addGet('/:id', handler)
12
+ * .addPost('/', createUser);
13
+ */
14
+ export declare class RouteGroup {
15
+ private basePath;
16
+ private routes;
17
+ /**
18
+ * Creates a new route group with the specified base path.
19
+ * Trailing slashes are automatically removed.
20
+ *
21
+ * @param basePath - The common prefix for all routes in the group (e.g. "/users")
22
+ */
23
+ constructor(basePath: string);
24
+ /**
25
+ * Returns the normalized base path used by the group.
26
+ */
27
+ getBasePath(): string;
28
+ /**
29
+ * Registers a GET route under the group's base path.
30
+ *
31
+ * @param path - Path relative to the base path (e.g. "/:id")
32
+ * @param handler - The handler function for this route
33
+ */
34
+ addGet(path: string, handler: Handler): this;
35
+ /**
36
+ * Registers a POST route under the group's base path.
37
+ *
38
+ * @param path - Path relative to the base path
39
+ * @param handler - The handler function for this route
40
+ */
41
+ addPost(path: string, handler: Handler): this;
42
+ /**
43
+ * Registers a PUT route under the group's base path.
44
+ *
45
+ * @param path - Path relative to the base path
46
+ * @param handler - The handler function for this route
47
+ */
48
+ addPut(path: string, handler: Handler): this;
49
+ /**
50
+ * Registers a DELETE route under the group's base path.
51
+ *
52
+ * @param path - Path relative to the base path
53
+ * @param handler - The handler function for this route
54
+ */
55
+ addDelete(path: string, handler: Handler): this;
56
+ /**
57
+ * Registers a PATCH route under the group's base path.
58
+ *
59
+ * @param path - Path relative to the base path
60
+ * @param handler - The handler function for this route
61
+ */
62
+ addPatch(path: string, handler: Handler): this;
63
+ /**
64
+ * Internal method to register a route under the group with any HTTP method.
65
+ *
66
+ * @param method - HTTP verb
67
+ * @param subPath - Route path relative to the base
68
+ * @param handler - The handler function for this route
69
+ */
70
+ private add;
71
+ /**
72
+ * Returns all the registered routes in the group.
73
+ */
74
+ getRoutes(): Route[];
75
+ }
76
+ /**
77
+ * Helper to define a `RouteGroup` with optional inline configuration.
78
+ *
79
+ * @param basePath - The base path prefix for all routes
80
+ * @param configure - Optional callback to configure the group inline
81
+ *
82
+ * @example
83
+ * const users = defineGroup('/users', (group) => {
84
+ * group.addGet('/:id', handler);
85
+ * });
86
+ */
87
+ export declare function defineGroup(basePath: string, configure?: (group: RouteGroup) => void): RouteGroup;
@@ -0,0 +1,111 @@
1
+ import { HttpMethod } from './HttpMethod';
2
+ import { defineRoute } from './defineRoute';
3
+ /**
4
+ * Represents a group of routes under a shared base path.
5
+ *
6
+ * Use this class to organize related endpoints, applying a consistent prefix
7
+ * and reducing duplication when defining similar routes.
8
+ *
9
+ * @example
10
+ * const users = new RouteGroup('/users')
11
+ * .addGet('/:id', handler)
12
+ * .addPost('/', createUser);
13
+ */
14
+ export class RouteGroup {
15
+ /**
16
+ * Creates a new route group with the specified base path.
17
+ * Trailing slashes are automatically removed.
18
+ *
19
+ * @param basePath - The common prefix for all routes in the group (e.g. "/users")
20
+ */
21
+ constructor(basePath) {
22
+ this.routes = [];
23
+ this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
24
+ }
25
+ /**
26
+ * Returns the normalized base path used by the group.
27
+ */
28
+ getBasePath() {
29
+ return this.basePath;
30
+ }
31
+ /**
32
+ * Registers a GET route under the group's base path.
33
+ *
34
+ * @param path - Path relative to the base path (e.g. "/:id")
35
+ * @param handler - The handler function for this route
36
+ */
37
+ addGet(path, handler) {
38
+ return this.add(HttpMethod.GET, path, handler);
39
+ }
40
+ /**
41
+ * Registers a POST route under the group's base path.
42
+ *
43
+ * @param path - Path relative to the base path
44
+ * @param handler - The handler function for this route
45
+ */
46
+ addPost(path, handler) {
47
+ return this.add(HttpMethod.POST, path, handler);
48
+ }
49
+ /**
50
+ * Registers a PUT route under the group's base path.
51
+ *
52
+ * @param path - Path relative to the base path
53
+ * @param handler - The handler function for this route
54
+ */
55
+ addPut(path, handler) {
56
+ return this.add(HttpMethod.PUT, path, handler);
57
+ }
58
+ /**
59
+ * Registers a DELETE route under the group's base path.
60
+ *
61
+ * @param path - Path relative to the base path
62
+ * @param handler - The handler function for this route
63
+ */
64
+ addDelete(path, handler) {
65
+ return this.add(HttpMethod.DELETE, path, handler);
66
+ }
67
+ /**
68
+ * Registers a PATCH route under the group's base path.
69
+ *
70
+ * @param path - Path relative to the base path
71
+ * @param handler - The handler function for this route
72
+ */
73
+ addPatch(path, handler) {
74
+ return this.add(HttpMethod.PATCH, path, handler);
75
+ }
76
+ /**
77
+ * Internal method to register a route under the group with any HTTP method.
78
+ *
79
+ * @param method - HTTP verb
80
+ * @param subPath - Route path relative to the base
81
+ * @param handler - The handler function for this route
82
+ */
83
+ add(method, subPath, handler) {
84
+ const normalizedPath = subPath.startsWith('/') ? subPath : `/${subPath}`;
85
+ this.routes.push(defineRoute(method, `${this.basePath}${normalizedPath}`, handler));
86
+ return this;
87
+ }
88
+ /**
89
+ * Returns all the registered routes in the group.
90
+ */
91
+ getRoutes() {
92
+ return this.routes;
93
+ }
94
+ }
95
+ /**
96
+ * Helper to define a `RouteGroup` with optional inline configuration.
97
+ *
98
+ * @param basePath - The base path prefix for all routes
99
+ * @param configure - Optional callback to configure the group inline
100
+ *
101
+ * @example
102
+ * const users = defineGroup('/users', (group) => {
103
+ * group.addGet('/:id', handler);
104
+ * });
105
+ */
106
+ export function defineGroup(basePath, configure) {
107
+ const group = new RouteGroup(basePath);
108
+ if (configure)
109
+ configure(group);
110
+ return group;
111
+ }
@@ -2,3 +2,4 @@ import type { APIContext, APIRoute } from 'astro';
2
2
  import { type ResultResponse } from './responseHelpers';
3
3
  export type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> | ResultResponse | Response;
4
4
  export declare function defineHandler(handler: Handler): APIRoute;
5
+ export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
@@ -16,6 +16,17 @@ export function defineHandler(handler) {
16
16
  logResponse(result.status, start);
17
17
  return result;
18
18
  }
19
+ // If it's a file response (manual BodyInit with appropriate headers)
20
+ if (result?.body instanceof Blob ||
21
+ result?.body instanceof ArrayBuffer ||
22
+ isReadableStream(result?.body)) {
23
+ const res = new Response(result.body, {
24
+ status: result.status,
25
+ headers: result.headers,
26
+ });
27
+ logResponse(res.status, start);
28
+ return res;
29
+ }
19
30
  const finalResponse = toAstroResponse(result);
20
31
  logResponse(finalResponse.status, start);
21
32
  return finalResponse;
@@ -28,3 +39,8 @@ export function defineHandler(handler) {
28
39
  }
29
40
  };
30
41
  }
42
+ export function isReadableStream(value) {
43
+ return (typeof value === 'object' &&
44
+ value !== null &&
45
+ typeof value.getReader === 'function');
46
+ }
@@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
2
2
  import { notFound } from './responseHelpers';
3
3
  import type { Route } from './defineRoute';
4
4
  export interface RouterOptions {
5
+ basePath?: string;
5
6
  /** Custom 404 handler */
6
7
  onNotFound?: () => ReturnType<typeof notFound>;
7
8
  }
@@ -7,9 +7,14 @@ export function defineRouter(routes, options = {}) {
7
7
  for (const route of routes) {
8
8
  trie.insert(route.path, route.method, route.handler);
9
9
  }
10
- // Wrap every user handler through defineHandler for uniform logging & error handling
11
10
  return async (ctx) => {
12
- const path = new URL(ctx.request.url).pathname.replace(/^\/api/, '');
11
+ const pathname = new URL(ctx.request.url).pathname;
12
+ let basePath = options.basePath ?? '/api';
13
+ if (!basePath.startsWith('/')) {
14
+ basePath = '/' + basePath;
15
+ }
16
+ const basePathRegex = new RegExp(`^${basePath}`);
17
+ const path = pathname.replace(basePathRegex, '');
13
18
  const method = normalizeMethod(ctx.request.method);
14
19
  const { handler, allowed, params } = trie.find(path, method);
15
20
  if (!handler) {
@@ -1,4 +1,4 @@
1
- import { HeadersInit } from "undici";
1
+ import { BodyInit, HeadersInit } from "undici";
2
2
  export interface ResultResponse<T = unknown> {
3
3
  body?: T;
4
4
  status: number;
@@ -14,4 +14,5 @@ export declare const forbidden: <T = string>(body?: T, headers?: HeadersInit) =>
14
14
  export declare const notFound: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
15
15
  export declare const methodNotAllowed: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
16
16
  export declare const internalError: (err: unknown, headers?: HeadersInit) => ResultResponse<string>;
17
+ export declare const fileResponse: (content: Blob | ArrayBuffer | ReadableStream<Uint8Array>, contentType: string, fileName?: string, headers?: HeadersInit) => ResultResponse<BodyInit>;
17
18
  export declare function toAstroResponse(result: ResultResponse | undefined): Response;
@@ -11,6 +11,20 @@ export const forbidden = (body = 'Forbidden', headers) => createResponse(403, bo
11
11
  export const notFound = (body = 'Not Found', headers) => createResponse(404, body, headers);
12
12
  export const methodNotAllowed = (body = 'Method Not Allowed', headers) => createResponse(405, body, headers);
13
13
  export const internalError = (err, headers) => createResponse(500, err instanceof Error ? err.message : String(err), headers);
14
+ export const fileResponse = (content, contentType, fileName, headers) => {
15
+ const disposition = fileName
16
+ ? { 'Content-Disposition': `attachment; filename="${fileName}"` }
17
+ : {};
18
+ return {
19
+ status: 200,
20
+ body: content,
21
+ headers: {
22
+ 'Content-Type': contentType,
23
+ ...disposition,
24
+ ...headers,
25
+ },
26
+ };
27
+ };
14
28
  export function toAstroResponse(result) {
15
29
  if (!result)
16
30
  return new Response(null, { status: 204 });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as astro from 'astro';
2
2
  import { APIContext, APIRoute } from 'astro';
3
- import { HeadersInit } from 'undici';
3
+ import { HeadersInit, BodyInit } from 'undici';
4
4
 
5
5
  declare enum HttpMethod {
6
6
  GET = "GET",
@@ -31,10 +31,12 @@ declare const forbidden: <T = string>(body?: T, headers?: HeadersInit) => Result
31
31
  declare const notFound: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
32
32
  declare const methodNotAllowed: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
33
33
  declare const internalError: (err: unknown, headers?: HeadersInit) => ResultResponse<string>;
34
+ declare const fileResponse: (content: Blob | ArrayBuffer | ReadableStream<Uint8Array>, contentType: string, fileName?: string, headers?: HeadersInit) => ResultResponse<BodyInit>;
34
35
  declare function toAstroResponse(result: ResultResponse | undefined): Response;
35
36
 
36
37
  type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> | ResultResponse | Response;
37
38
  declare function defineHandler(handler: Handler): APIRoute;
39
+ declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
38
40
 
39
41
  interface Route {
40
42
  method: HttpMethod;
@@ -45,11 +47,98 @@ declare function defineRoute(route: Route): Route;
45
47
  declare function defineRoute(method: HttpMethod, path: string, handler: Handler): Route;
46
48
 
47
49
  interface RouterOptions {
50
+ basePath?: string;
48
51
  /** Custom 404 handler */
49
52
  onNotFound?: () => ReturnType<typeof notFound>;
50
53
  }
51
54
  declare function defineRouter(routes: Route[], options?: RouterOptions): APIRoute;
52
55
 
56
+ /**
57
+ * Represents a group of routes under a shared base path.
58
+ *
59
+ * Use this class to organize related endpoints, applying a consistent prefix
60
+ * and reducing duplication when defining similar routes.
61
+ *
62
+ * @example
63
+ * const users = new RouteGroup('/users')
64
+ * .addGet('/:id', handler)
65
+ * .addPost('/', createUser);
66
+ */
67
+ declare class RouteGroup {
68
+ private basePath;
69
+ private routes;
70
+ /**
71
+ * Creates a new route group with the specified base path.
72
+ * Trailing slashes are automatically removed.
73
+ *
74
+ * @param basePath - The common prefix for all routes in the group (e.g. "/users")
75
+ */
76
+ constructor(basePath: string);
77
+ /**
78
+ * Returns the normalized base path used by the group.
79
+ */
80
+ getBasePath(): string;
81
+ /**
82
+ * Registers a GET route under the group's base path.
83
+ *
84
+ * @param path - Path relative to the base path (e.g. "/:id")
85
+ * @param handler - The handler function for this route
86
+ */
87
+ addGet(path: string, handler: Handler): this;
88
+ /**
89
+ * Registers a POST route under the group's base path.
90
+ *
91
+ * @param path - Path relative to the base path
92
+ * @param handler - The handler function for this route
93
+ */
94
+ addPost(path: string, handler: Handler): this;
95
+ /**
96
+ * Registers a PUT route under the group's base path.
97
+ *
98
+ * @param path - Path relative to the base path
99
+ * @param handler - The handler function for this route
100
+ */
101
+ addPut(path: string, handler: Handler): this;
102
+ /**
103
+ * Registers a DELETE route under the group's base path.
104
+ *
105
+ * @param path - Path relative to the base path
106
+ * @param handler - The handler function for this route
107
+ */
108
+ addDelete(path: string, handler: Handler): this;
109
+ /**
110
+ * Registers a PATCH route under the group's base path.
111
+ *
112
+ * @param path - Path relative to the base path
113
+ * @param handler - The handler function for this route
114
+ */
115
+ addPatch(path: string, handler: Handler): this;
116
+ /**
117
+ * Internal method to register a route under the group with any HTTP method.
118
+ *
119
+ * @param method - HTTP verb
120
+ * @param subPath - Route path relative to the base
121
+ * @param handler - The handler function for this route
122
+ */
123
+ private add;
124
+ /**
125
+ * Returns all the registered routes in the group.
126
+ */
127
+ getRoutes(): Route[];
128
+ }
129
+ /**
130
+ * Helper to define a `RouteGroup` with optional inline configuration.
131
+ *
132
+ * @param basePath - The base path prefix for all routes
133
+ * @param configure - Optional callback to configure the group inline
134
+ *
135
+ * @example
136
+ * const users = defineGroup('/users', (group) => {
137
+ * group.addGet('/:id', handler);
138
+ * });
139
+ */
140
+ declare function defineGroup(basePath: string, configure?: (group: RouteGroup) => void): RouteGroup;
141
+
53
142
  interface RouteMatch {
54
143
  handler: Handler | null;
55
144
  allowed?: HttpMethod[];
@@ -62,12 +151,121 @@ declare class RouteTrie {
62
151
  private segmentize;
63
152
  }
64
153
 
154
+ /**
155
+ * A fluent builder for creating and composing API routes in Astro.
156
+ *
157
+ * `RouterBuilder` supports both simple method-based additions (`addGet`, `addPost`, etc.)
158
+ * and organized groups via `defineGroup()` + `addGroup()` for scalable applications.
159
+ *
160
+ * @example Basic usage:
161
+ * ```ts
162
+ * const router = new RouterBuilder()
163
+ * .addGet('/ping', () => ok('pong'))
164
+ * .addPost('/submit', handler);
165
+ *
166
+ * export const ALL = router.build();
167
+ * ```
168
+ *
169
+ * @example Using groups:
170
+ * ```ts
171
+ * const users = defineGroup('/users')
172
+ * .addGet('/:id', userHandler);
173
+ *
174
+ * const router = new RouterBuilder({ basePath: '/api' })
175
+ * .addGroup(users);
176
+ *
177
+ * export const GET = router.build();
178
+ * ```
179
+ */
65
180
  declare class RouterBuilder {
66
- private routes;
181
+ private _options;
182
+ private _routes;
183
+ private static _registerWarned;
184
+ constructor(options?: RouterOptions);
185
+ /**
186
+ * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
187
+ *
188
+ * Registers a single route manually.
189
+ *
190
+ * This method is deprecated in favor of a more scalable and organized approach using
191
+ * `defineGroup()` + `addGroup()` or `addRoute()` for ad-hoc additions.
192
+ *
193
+ * @param route A single `Route` object to register.
194
+ */
67
195
  register(route: Route): void;
196
+ /**
197
+ * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
198
+ *
199
+ * Registers multiple routes at once.
200
+ *
201
+ * This method is deprecated and discouraged for larger codebases in favor of group-based composition.
202
+ *
203
+ * @param routes An array of `Route` objects to register.
204
+ */
68
205
  register(routes: Route[]): void;
69
- build(options?: RouterOptions): astro.APIRoute;
206
+ /**
207
+ * Adds a single route to the router.
208
+ *
209
+ * @param route The route to add.
210
+ * @returns The current builder instance (for chaining).
211
+ */
212
+ addRoute(route: Route): this;
213
+ /**
214
+ * Adds a group of pre-defined routes (from `defineGroup()`).
215
+ *
216
+ * @param group A `RouteGroup` instance.
217
+ * @returns The current builder instance (for chaining).
218
+ */
219
+ addGroup(group: RouteGroup): this;
220
+ /**
221
+ * Adds a GET route.
222
+ * @param path Route path (e.g., `/items/:id`)
223
+ * @param handler Request handler function.
224
+ */
225
+ addGet(path: string, handler: Handler): this;
226
+ /**
227
+ * Adds a POST route.
228
+ * @param path Route path.
229
+ * @param handler Request handler function.
230
+ */
231
+ addPost(path: string, handler: Handler): this;
232
+ /**
233
+ * Adds a PUT route.
234
+ * @param path Route path.
235
+ * @param handler Request handler function.
236
+ */
237
+ addPut(path: string, handler: Handler): this;
238
+ /**
239
+ * Adds a DELETE route.
240
+ * @param path Route path.
241
+ * @param handler Request handler function.
242
+ */
243
+ addDelete(path: string, handler: Handler): this;
244
+ /**
245
+ * Adds a PATCH route.
246
+ * @param path Route path.
247
+ * @param handler Request handler function.
248
+ */
249
+ addPatch(path: string, handler: Handler): this;
250
+ /**
251
+ * Internal helper to add a route with any HTTP method.
252
+ *
253
+ * @param method The HTTP method.
254
+ * @param subPath Path segment (can be relative or absolute).
255
+ * @param handler Request handler.
256
+ * @returns The current builder instance (for chaining).
257
+ */
258
+ private add;
259
+ /**
260
+ * Finalizes the router and returns an Astro-compatible route handler.
261
+ *
262
+ * This function should be used to export a method like `GET`, `POST`, or `ALL`
263
+ * inside Astro API endpoints.
264
+ *
265
+ * @returns A fully resolved Astro route handler.
266
+ */
267
+ build(): astro.APIRoute;
70
268
  }
71
269
 
72
- export { ALLOWED_HTTP_METHODS, HttpMethod, RouteTrie, RouterBuilder, badRequest, created, defineHandler, defineRoute, defineRouter, forbidden, internalError, methodNotAllowed, noContent, normalizeMethod, notFound, notModified, ok, toAstroResponse, unauthorized };
270
+ export { ALLOWED_HTTP_METHODS, HttpMethod, RouteGroup, RouteTrie, RouterBuilder, badRequest, created, defineGroup, defineHandler, defineRoute, defineRouter, fileResponse, forbidden, internalError, isReadableStream, methodNotAllowed, noContent, normalizeMethod, notFound, notModified, ok, toAstroResponse, unauthorized };
73
271
  export type { Handler, ResultResponse, Route, RouterOptions };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './core/defineRoute';
2
2
  export * from './core/defineRouter';
3
3
  export * from './core/defineHandler';
4
+ export * from './core/defineGroup';
4
5
  export * from './core/RouteTrie';
5
6
  export * from './core/HttpMethod';
6
7
  export * from './core/responseHelpers';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-routify",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A high-performance API router for Astro using a Trie-based matcher.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "rimraf": "^6.0.1",
34
- "rollup": "^4.45.0",
34
+ "rollup": "^4.46.2",
35
35
  "rollup-plugin-dts": "^6.2.1",
36
36
  "typescript": "^5.3.3",
37
- "undici": "^7.11.0",
37
+ "undici": "^7.13.0",
38
38
  "vitest": "^3.2.4"
39
39
  },
40
40
  "engines": {