astro-routify 1.2.2 → 1.4.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
@@ -92,11 +92,15 @@ endpoint system.
92
92
 
93
93
  ### `defineRoute()`
94
94
 
95
- Declare a single route:
95
+ Declare a single route. Now supports middlewares and metadata:
96
96
 
97
97
  ```ts
98
- defineRoute(HttpMethod.GET, "/users/:id", ({params}) => {
99
- return ok({userId: params.id});
98
+ defineRoute({
99
+ method: 'GET',
100
+ path: '/users/:id',
101
+ middlewares: [authMiddleware],
102
+ metadata: { summary: 'Get user by ID' },
103
+ handler: ({params}) => ok({userId: params.id})
100
104
  });
101
105
  ```
102
106
 
@@ -110,6 +114,125 @@ export const GET = defineRouter([
110
114
  ]);
111
115
  ```
112
116
 
117
+ ### Advanced Matching
118
+
119
+ `astro-routify` supports advanced routing patterns including wildcards and regex constraints.
120
+
121
+ #### 1. Wildcards
122
+ - `*` matches exactly one segment.
123
+ - `**` matches zero or more segments (catch-all).
124
+
125
+ ```ts
126
+ builder.addGet('/files/*/download', () => ok('one segment'));
127
+ builder.addGet('/static/**', () => ok('all segments'));
128
+ ```
129
+
130
+ #### 2. Regex Constraints
131
+ You can constrain parameters using regex by wrapping the pattern in parentheses: `:param(regex)`.
132
+
133
+ ```ts
134
+ // Matches only numeric IDs
135
+ builder.addGet('/users/:id(\\d+)', ({ params }) => ok(params.id));
136
+
137
+ // Matches hex colors
138
+ builder.addGet('/color/:hex(^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$)', ({ params }) => ok(params.hex));
139
+ ```
140
+
141
+ #### 3. Matching Priority
142
+ When multiple routes could match a path, the router follows a deterministic priority order:
143
+ 1. **Static Match** (e.g., `/p/static`)
144
+ 2. **Regex Match** (e.g., `/p/:id(\\d+)`)
145
+ 3. **Param Match** (e.g., `/p/:id`)
146
+ 4. **Wildcard Match** (e.g., `/p/*`)
147
+ 5. **Catch-all Match** (e.g., `/**`)
148
+
149
+ ### 🛡️ Middleware & Security
150
+
151
+ `astro-routify` provides a powerful middleware system and built-in security helpers.
152
+
153
+ #### 1. Middleware Support
154
+ Middleware can be applied globally, to groups, or to individual routes.
155
+
156
+ ```ts
157
+ const builder = new RouterBuilder();
158
+
159
+ // Global middleware
160
+ builder.use(async (ctx, next) => {
161
+ const start = performance.now();
162
+ const res = await next();
163
+ console.log(`Duration: ${performance.now() - start}ms`);
164
+ return res;
165
+ });
166
+
167
+ // Group middleware
168
+ builder.group('/admin')
169
+ .use(checkAuth)
170
+ .addGet('/dashboard', (ctx) => ok('Admin only'));
171
+
172
+ // Route middleware
173
+ builder.addPost('/user', validate(UserSchema), (ctx) => {
174
+ return ok(ctx.data.body);
175
+ });
176
+ ```
177
+
178
+ #### 2. Request Validation
179
+ Built-in `validate()` middleware works with Zod, Valibot, or any library implementing a `safeParse` method.
180
+
181
+ ```ts
182
+ import { validate } from 'astro-routify';
183
+ import { z } from 'zod';
184
+
185
+ const UserSchema = z.object({
186
+ name: z.string(),
187
+ email: z.string().email()
188
+ });
189
+
190
+ builder.addPost('/register', validate({ body: UserSchema }), (ctx) => {
191
+ const user = ctx.data.body; // Fully typed if using TypeScript correctly
192
+ return ok(user);
193
+ });
194
+ ```
195
+
196
+ #### 3. Security Middlewares (CORS & Headers)
197
+ Protect your API with `cors()` and `securityHeaders()`.
198
+
199
+ ```ts
200
+ import { cors, securityHeaders } from 'astro-routify';
201
+
202
+ builder.use(cors({ origin: 'https://example.com' }));
203
+ builder.use(securityHeaders());
204
+ ```
205
+
206
+ ### 🛠 Advanced Configuration
207
+
208
+ #### Centralized Error Handling
209
+ Handle all API errors in one place:
210
+
211
+ ```ts
212
+ export const ALL = createRouter({
213
+ onError: (err, ctx) => {
214
+ console.error(err);
215
+ return json({ error: 'Something went wrong' }, 500);
216
+ }
217
+ });
218
+ ```
219
+
220
+ #### OpenAPI (Swagger) Generation
221
+ Automatically generate API documentation:
222
+
223
+ ```ts
224
+ import { generateOpenAPI } from 'astro-routify';
225
+
226
+ const router = builder.build();
227
+ const spec = generateOpenAPI(router, {
228
+ title: 'My API',
229
+ version: '1.0.0'
230
+ });
231
+
232
+ // Serve the spec
233
+ builder.addGet('/openapi.json', () => ok(spec));
234
+ ```
235
+
113
236
  > 🧠 `defineRouter()` supports all HTTP methods — but Astro only executes the method you export (`GET`, `POST`, etc.)
114
237
 
115
238
  ### `RouterBuilder` (Catch-All & Fluent Builder)
@@ -130,18 +253,112 @@ builder
130
253
  export const ALL = builder.build();
131
254
  ```
132
255
 
133
- You can also group routes:
256
+ #### 🏗 Vertical Slices & Auto-Discovery
257
+
258
+ To avoid a long list of manual registrations, you can use `addModules` combined with Vite's `import.meta.glob`. This allows
259
+ you to define routes anywhere in your project (near your components) and have them automatically registered.
260
+
261
+ ```ts
262
+ // src/pages/api/[...all].ts
263
+ import { RouterBuilder, createRouter } from 'astro-routify';
264
+
265
+ // 1. Using the builder
266
+ const builder = new RouterBuilder();
267
+ builder.addModules(import.meta.glob('../../**/*.routes.ts', { eager: true }));
268
+ export const ALL = builder.build();
269
+
270
+ // 2. Or using the one-liner helper
271
+ export const ALL = createRouter(
272
+ import.meta.glob('../../**/*.routes.ts', { eager: true }),
273
+ { debug: true } // optional: enable match logging
274
+ );
275
+ ```
276
+
277
+ #### 🛡️ Agnostic Auto-Registration (Global Registry)
278
+
279
+ If you want to avoid passing glob results or knowing the relative path to your routes, you can use the **global registry**. By setting the `autoRegister` flag or using **decorators**, routes will register themselves as soon as their module is loaded.
280
+
281
+ ##### 1. Enable Auto-Registration in your routes
282
+
283
+ ```ts
284
+ // src/components/User/User.routes.ts
285
+ import { defineRoute, defineGroup, ok, Get } from 'astro-routify';
286
+
287
+ // Option A: Using the flag
288
+ export const GET_USER = defineRoute('GET', '/users/:id', ({params}) => ok({id: params.id}), true);
289
+
290
+ // Option B: Using a group flag
291
+ defineGroup('/admin', (g) => {
292
+ g.addGet('/stats', () => ok({}));
293
+ }, true);
294
+
295
+ // Option C: Using Decorators (requires experimentalDecorators: true)
296
+ class UserRoutes {
297
+ @Get('/profile')
298
+ static getProfile() { return ok({ name: 'Alex' }); }
299
+ }
300
+ ```
301
+
302
+ ##### 2. Initialize the router agnostically
303
+
304
+ In your catch-all endpoint, simply call `import.meta.glob` to trigger the loading of your route files, and then call `createRouter()` without module arguments.
305
+
306
+ ```ts
307
+ // src/pages/api/[...all].ts
308
+ import { createRouter } from 'astro-routify';
309
+
310
+ // Trigger loading of all route files (agnostic of relative path using /src root alias)
311
+ import.meta.glob('/src/**/*.routes.ts', { eager: true });
312
+
313
+ // createRouter() will automatically include all auto-registered routes
314
+ export const ALL = createRouter({ debug: true });
315
+ ```
316
+
317
+ You can also use the global `RouterBuilder` instance directly:
318
+
319
+ ```ts
320
+ import { RouterBuilder } from 'astro-routify';
321
+ import.meta.glob('/src/**/*.routes.ts', { eager: true });
322
+
323
+ export const ALL = RouterBuilder.global.build();
324
+ ```
325
+
326
+ You can also still manually add routes or groups:
134
327
 
135
328
  ```ts
136
329
  const users = defineGroup("/users")
137
330
  .addGet("/:id", ({params}) => ok({id: params.id}));
138
331
 
139
332
  builder.addGroup(users);
333
+ builder.addGet("/ping", () => ok("pong"));
140
334
  ```
141
335
 
142
336
  > 🔁 While `.register()` is still available, it's **deprecated** in favor of `.addGroup()` and `.addRoute()` for better
143
337
  > structure and reusability.
144
338
 
339
+ Your route files can export single routes, groups, or arrays:
340
+
341
+ ```ts
342
+ // src/components/User/UserList.routes.ts
343
+ import { defineRoute, defineGroup, ok } from 'astro-routify';
344
+
345
+ export const GET = defineRoute('GET', '/users', () => ok([]));
346
+
347
+ export const AdminRoutes = defineGroup('/admin')
348
+ .addPost('/reset', () => ok('done'));
349
+ ```
350
+
351
+ #### 🛠 Development & Debugging
352
+
353
+ `astro-routify` provides built-in logging to help you see your route table during development.
354
+
355
+ - **Auto-logging**: In development mode (`NODE_ENV=development`), `RouterBuilder` automatically prints the registered routes to the console when `build()` is called.
356
+ - **Match Tracing**: Set `debug: true` in `RouterOptions` to see a log of every incoming request and which route it matched (or why it failed with 404/405).
357
+
358
+ ```ts
359
+ const router = new RouterBuilder({ debug: true });
360
+ ```
361
+
145
362
  ---
146
363
 
147
364
  ## 🔁 Response Helpers
@@ -1,14 +1,15 @@
1
1
  import { HttpMethod } from './HttpMethod';
2
- import type { Handler } from './defineHandler';
2
+ import type { Route } from './defineRoute';
3
3
  interface RouteMatch {
4
- handler: Handler | null;
4
+ route: Route | null;
5
5
  allowed?: HttpMethod[];
6
6
  params: Record<string, string | undefined>;
7
7
  }
8
8
  export declare class RouteTrie {
9
9
  private readonly root;
10
- insert(path: string, method: HttpMethod, handler: Handler): void;
10
+ insert(route: Route): void;
11
11
  find(path: string, method: HttpMethod): RouteMatch;
12
+ private matchNode;
12
13
  private segmentize;
13
14
  }
14
15
  export {};
@@ -1,56 +1,125 @@
1
1
  export class RouteTrie {
2
2
  constructor() {
3
- this.root = { children: new Map(), handlers: new Map() };
3
+ this.root = { children: new Map(), routes: new Map() };
4
4
  }
5
- insert(path, method, handler) {
6
- const segments = this.segmentize(path);
5
+ insert(route) {
6
+ if (!route || typeof route.path !== 'string') {
7
+ return;
8
+ }
9
+ const segments = this.segmentize(route.path);
7
10
  let node = this.root;
8
11
  for (const segment of segments) {
9
- if (segment.startsWith(':')) {
10
- if (!node.paramChild) {
11
- node.paramChild = {
12
- children: new Map(),
13
- handlers: new Map(),
14
- paramName: segment.slice(1),
15
- };
12
+ if (segment === '**') {
13
+ if (!node.catchAllChild) {
14
+ node.catchAllChild = { children: new Map(), routes: new Map() };
15
+ }
16
+ node = node.catchAllChild;
17
+ break;
18
+ }
19
+ else if (segment === '*') {
20
+ if (!node.wildcardChild) {
21
+ node.wildcardChild = { children: new Map(), routes: new Map() };
22
+ }
23
+ node = node.wildcardChild;
24
+ }
25
+ else if (segment.startsWith(':')) {
26
+ const regexMatch = segment.match(/^:([a-zA-Z0-9_]+)\((.*)\)$/);
27
+ if (regexMatch) {
28
+ const paramName = regexMatch[1];
29
+ const regexStr = regexMatch[2];
30
+ if (!node.regexChildren)
31
+ node.regexChildren = [];
32
+ let regexNode = node.regexChildren.find((rc) => rc.regex.source === regexStr && rc.paramName === paramName)?.node;
33
+ if (!regexNode) {
34
+ regexNode = { children: new Map(), routes: new Map() };
35
+ node.regexChildren.push({
36
+ regex: new RegExp(regexStr),
37
+ paramName,
38
+ node: regexNode,
39
+ });
40
+ }
41
+ node = regexNode;
42
+ }
43
+ else {
44
+ const paramName = segment.slice(1);
45
+ if (!node.paramChild) {
46
+ node.paramChild = { children: new Map(), routes: new Map(), paramName };
47
+ }
48
+ node = node.paramChild;
16
49
  }
17
- node = node.paramChild;
18
50
  }
19
51
  else {
20
52
  if (!node.children.has(segment)) {
21
- node.children.set(segment, { children: new Map(), handlers: new Map() });
53
+ node.children.set(segment, { children: new Map(), routes: new Map() });
22
54
  }
23
55
  node = node.children.get(segment);
24
56
  }
25
57
  }
26
- node.handlers.set(method, handler);
58
+ node.routes.set(route.method, route);
27
59
  }
28
60
  find(path, method) {
29
61
  const segments = this.segmentize(path);
30
- let node = this.root;
31
- const params = {};
32
- for (const segment of segments) {
33
- if (node?.children.has(segment)) {
34
- node = node.children.get(segment);
62
+ return this.matchNode(this.root, segments, 0, method);
63
+ }
64
+ matchNode(node, segments, index, method) {
65
+ if (index === segments.length) {
66
+ let route = node.routes.get(method) ?? null;
67
+ let allowed = route ? undefined : [...node.routes.keys()];
68
+ // If no route here, check if there's a catch-all that matches "empty" remaining
69
+ if (!route && node.catchAllChild) {
70
+ route = node.catchAllChild.routes.get(method) ?? null;
71
+ allowed = route ? undefined : [...node.catchAllChild.routes.keys()];
35
72
  }
36
- else if (node?.paramChild) {
37
- params[node.paramChild.paramName] = segment;
38
- node = node.paramChild;
73
+ return { route, allowed, params: {} };
74
+ }
75
+ const segment = segments[index];
76
+ // 1. Static Match
77
+ const staticChild = node.children.get(segment);
78
+ if (staticChild) {
79
+ const match = this.matchNode(staticChild, segments, index + 1, method);
80
+ if (match.route || (match.allowed && match.allowed.length > 0))
81
+ return match;
82
+ }
83
+ // 2. Regex Match
84
+ if (node.regexChildren) {
85
+ for (const rc of node.regexChildren) {
86
+ if (rc.regex.test(segment)) {
87
+ const match = this.matchNode(rc.node, segments, index + 1, method);
88
+ if (match.route || (match.allowed && match.allowed.length > 0)) {
89
+ match.params[rc.paramName] = segment;
90
+ return match;
91
+ }
92
+ }
39
93
  }
40
- else {
41
- return { handler: null, params };
94
+ }
95
+ // 3. Param Match
96
+ if (node.paramChild) {
97
+ const match = this.matchNode(node.paramChild, segments, index + 1, method);
98
+ if (match.route || (match.allowed && match.allowed.length > 0)) {
99
+ match.params[node.paramChild.paramName] = segment;
100
+ return match;
42
101
  }
43
102
  }
44
- if (!node)
45
- return { handler: null, params };
46
- const handler = node.handlers.get(method) ?? null;
47
- return {
48
- handler,
49
- allowed: handler ? undefined : [...node.handlers.keys()],
50
- params,
51
- };
103
+ // 4. Wildcard Match (*)
104
+ if (node.wildcardChild) {
105
+ const match = this.matchNode(node.wildcardChild, segments, index + 1, method);
106
+ if (match.route || (match.allowed && match.allowed.length > 0))
107
+ return match;
108
+ }
109
+ // 5. Catch-all Match (**)
110
+ if (node.catchAllChild) {
111
+ const route = node.catchAllChild.routes.get(method) ?? null;
112
+ return {
113
+ route,
114
+ allowed: route ? undefined : [...node.catchAllChild.routes.keys()],
115
+ params: {},
116
+ };
117
+ }
118
+ return { route: null, params: {} };
52
119
  }
53
120
  segmentize(path) {
54
- return path.replace(/(^\/|\/$)/g, '').split('/');
121
+ if (typeof path !== 'string')
122
+ return [];
123
+ return path.split('/').filter(Boolean);
55
124
  }
56
125
  }
@@ -1,7 +1,8 @@
1
+ import type { APIRoute } from 'astro';
1
2
  import { type Route } from './defineRoute';
2
3
  import { type RouterOptions } from './defineRouter';
3
4
  import { RouteGroup } from './defineGroup';
4
- import { type Handler } from './defineHandler';
5
+ import { type Middleware } from './defineHandler';
5
6
  /**
6
7
  * A fluent builder for creating and composing API routes in Astro.
7
8
  *
@@ -27,12 +28,72 @@ import { type Handler } from './defineHandler';
27
28
  *
28
29
  * export const GET = router.build();
29
30
  * ```
31
+ *
32
+ * @example Auto-discovering routes via Vite glob:
33
+ * ```ts
34
+ * const router = new RouterBuilder()
35
+ * .addModules(import.meta.glob('./**\/*.routes.ts', { eager: true }));
36
+ *
37
+ * export const ALL = router.build();
38
+ * ```
30
39
  */
31
40
  export declare class RouterBuilder {
32
41
  private _options;
33
42
  private _routes;
43
+ private _groups;
44
+ private _middlewares;
45
+ private _shouldLog;
34
46
  private static _registerWarned;
47
+ /**
48
+ * A global RouterBuilder instance for easy, centralized route registration.
49
+ */
50
+ static readonly global: RouterBuilder;
35
51
  constructor(options?: RouterOptions);
52
+ /**
53
+ * Adds a global middleware to all routes registered in this builder.
54
+ *
55
+ * @param middleware - The middleware function.
56
+ * @returns The current builder instance.
57
+ */
58
+ use(middleware: Middleware): this;
59
+ /**
60
+ * Creates and adds a new route group to the builder.
61
+ *
62
+ * @param basePath - The base path for the group.
63
+ * @param configure - Optional callback to configure the group.
64
+ * @returns The created RouteGroup instance.
65
+ */
66
+ group(basePath: string, configure?: (group: RouteGroup) => void): RouteGroup;
67
+ /**
68
+ * Adds all routes and groups that have been auto-registered via `defineRoute(..., true)`
69
+ * or `defineGroup(..., true)`.
70
+ *
71
+ * @returns The current builder instance.
72
+ */
73
+ addRegistered(): this;
74
+ /**
75
+ * Bulk registers routes and groups from a module collection.
76
+ * Ideal for use with Vite's `import.meta.glob` (with `{ eager: true }`).
77
+ *
78
+ * It will search for both default and named exports that are either `Route` or `RouteGroup`.
79
+ *
80
+ * @param modules A record of modules (e.g. from `import.meta.glob`).
81
+ * @returns The current builder instance.
82
+ */
83
+ addModules(modules: Record<string, any>): this;
84
+ /**
85
+ * Prints all registered routes to the console.
86
+ * Useful for debugging during development.
87
+ *
88
+ * @returns The current builder instance.
89
+ */
90
+ logRoutes(): this;
91
+ /**
92
+ * Disables the automatic logging of routes that happens in development mode.
93
+ *
94
+ * @returns The current builder instance.
95
+ */
96
+ disableLogging(): this;
36
97
  /**
37
98
  * @deprecated Prefer `addGroup()` or `addRoute()` for structured routing.
38
99
  *
@@ -71,39 +132,39 @@ export declare class RouterBuilder {
71
132
  /**
72
133
  * Adds a GET route.
73
134
  * @param path Route path (e.g., `/items/:id`)
74
- * @param handler Request handler function.
135
+ * @param handlers Middleware(s) followed by a request handler function.
75
136
  */
76
- addGet(path: string, handler: Handler): this;
137
+ addGet(path: string, ...handlers: any[]): this;
77
138
  /**
78
139
  * Adds a POST route.
79
140
  * @param path Route path.
80
- * @param handler Request handler function.
141
+ * @param handlers Middleware(s) followed by a request handler function.
81
142
  */
82
- addPost(path: string, handler: Handler): this;
143
+ addPost(path: string, ...handlers: any[]): this;
83
144
  /**
84
145
  * Adds a PUT route.
85
146
  * @param path Route path.
86
- * @param handler Request handler function.
147
+ * @param handlers Middleware(s) followed by a request handler function.
87
148
  */
88
- addPut(path: string, handler: Handler): this;
149
+ addPut(path: string, ...handlers: any[]): this;
89
150
  /**
90
151
  * Adds a DELETE route.
91
152
  * @param path Route path.
92
- * @param handler Request handler function.
153
+ * @param handlers Middleware(s) followed by a request handler function.
93
154
  */
94
- addDelete(path: string, handler: Handler): this;
155
+ addDelete(path: string, ...handlers: any[]): this;
95
156
  /**
96
157
  * Adds a PATCH route.
97
158
  * @param path Route path.
98
- * @param handler Request handler function.
159
+ * @param handlers Middleware(s) followed by a request handler function.
99
160
  */
100
- addPatch(path: string, handler: Handler): this;
161
+ addPatch(path: string, ...handlers: any[]): this;
101
162
  /**
102
163
  * Internal helper to add a route with any HTTP method.
103
164
  *
104
165
  * @param method The HTTP method.
105
166
  * @param subPath Path segment (can be relative or absolute).
106
- * @param handler Request handler.
167
+ * @param args Middleware(s) and handler.
107
168
  * @returns The current builder instance (for chaining).
108
169
  */
109
170
  private add;
@@ -115,5 +176,27 @@ export declare class RouterBuilder {
115
176
  *
116
177
  * @returns A fully resolved Astro route handler.
117
178
  */
118
- build(): import("astro").APIRoute;
179
+ build(): APIRoute;
119
180
  }
181
+ /**
182
+ * A convenience helper to create a router.
183
+ *
184
+ * If modules are provided (e.g. from Vite's `import.meta.glob`), they will be registered.
185
+ * If no modules are provided, it will automatically include all routes that were
186
+ * registered via the auto-registration flags (`defineRoute(..., true)`).
187
+ *
188
+ * @example Auto-discovery via glob:
189
+ * ```ts
190
+ * export const ALL = createRouter(import.meta.glob('./**\/*.ts', { eager: true }));
191
+ * ```
192
+ *
193
+ * @example Auto-registration via global registry:
194
+ * ```ts
195
+ * export const ALL = createRouter();
196
+ * ```
197
+ *
198
+ * @param modulesOrOptions Either modules to register or router options.
199
+ * @param options Router options (if first arg is modules).
200
+ * @returns An Astro-compatible route handler.
201
+ */
202
+ export declare function createRouter(modulesOrOptions?: Record<string, any> | RouterOptions, options?: RouterOptions): APIRoute;