@aura-stack/router 0.2.0 → 0.3.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/dist/router.cjs CHANGED
@@ -20,16 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/router.ts
21
21
  var router_exports = {};
22
22
  __export(router_exports, {
23
- createRouter: () => createRouter
23
+ createNode: () => createNode,
24
+ createRouter: () => createRouter,
25
+ insert: () => insert,
26
+ search: () => search
24
27
  });
25
28
  module.exports = __toCommonJS(router_exports);
26
29
 
27
- // src/assert.ts
28
- var supportedBodyMethods = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
29
- var isSupportedBodyMethod = (method) => {
30
- return supportedBodyMethods.has(method);
31
- };
32
-
33
30
  // src/error.ts
34
31
  var statusCode = {
35
32
  OK: 200,
@@ -57,50 +54,57 @@ var statusCode = {
57
54
  SERVICE_UNAVAILABLE: 503,
58
55
  HTTP_VERSION_NOT_SUPPORTED: 505
59
56
  };
60
- var statusText = Object.entries(statusCode).reduce(
61
- (previous, [status, code]) => {
62
- return { ...previous, [code]: status };
57
+ var statusText = Object.keys(statusCode).reduce(
58
+ (previous, status) => {
59
+ return { ...previous, [status]: status };
63
60
  },
64
61
  {}
65
62
  );
66
63
  var AuraStackRouterError = class extends Error {
67
64
  constructor(type, message, name) {
68
65
  super(message);
69
- this.name = name ?? "AuraStackRouterError";
66
+ this.name = name ?? "RouterError";
70
67
  this.status = statusCode[type];
71
- this.statusText = statusText[this.status];
68
+ this.statusText = statusText[type];
69
+ }
70
+ };
71
+ var RouterError = class extends AuraStackRouterError {
72
+ constructor(type, message, name) {
73
+ super(type, message, name);
74
+ this.name = name ?? "RouterError";
72
75
  }
73
76
  };
74
77
 
75
- // src/endpoint.ts
76
- var createRoutePattern = (route) => {
77
- const pattern = route.replace(/:[^/]+/g, "([^/]+)").replace(/\//g, "\\/");
78
- return new RegExp(`^${pattern}$`);
78
+ // src/assert.ts
79
+ var supportedMethods = /* @__PURE__ */ new Set(["GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD", "TRACE", "CONNECT"]);
80
+ var supportedBodyMethods = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
81
+ var isSupportedMethod = (method) => {
82
+ return supportedMethods.has(method);
83
+ };
84
+ var isSupportedBodyMethod = (method) => {
85
+ return supportedBodyMethods.has(method);
86
+ };
87
+ var isRouterError = (error) => {
88
+ return error instanceof RouterError;
79
89
  };
80
90
 
81
91
  // src/context.ts
82
- var getRouteParams = (route, path) => {
83
- const routeRegex = createRoutePattern(route);
84
- if (!routeRegex.test(path)) {
85
- throw new AuraStackRouterError("BAD_REQUEST", `Missing required route params for route: ${route}`);
86
- }
87
- const params = routeRegex.exec(route)?.slice(1).map((seg) => seg.replace(":", ""));
88
- if (!params) return {};
89
- const values = routeRegex.exec(path)?.slice(1);
90
- return params.reduce(
91
- (previous, now, idx) => ({
92
- ...previous,
93
- [now]: decodeURIComponent(values?.[idx] ?? "")
94
- }),
95
- {}
96
- );
92
+ var getRouteParams = (params, config) => {
93
+ if (config.schemas?.params) {
94
+ const parsed = config.schemas.params.safeParse(params);
95
+ if (!parsed.success) {
96
+ throw new RouterError("UNPROCESSABLE_ENTITY", "Invalid route parameters");
97
+ }
98
+ return parsed.data;
99
+ }
100
+ return params;
97
101
  };
98
102
  var getSearchParams = (url, config) => {
99
103
  const route = new URL(url);
100
104
  if (config.schemas?.searchParams) {
101
105
  const parsed = config.schemas.searchParams.safeParse(Object.fromEntries(route.searchParams.entries()));
102
106
  if (!parsed.success) {
103
- throw new AuraStackRouterError("UNPROCESSABLE_ENTITY", "Invalid search parameters");
107
+ throw new RouterError("UNPROCESSABLE_ENTITY", "Invalid search parameters");
104
108
  }
105
109
  return parsed.data;
106
110
  }
@@ -111,33 +115,42 @@ var getHeaders = (request) => {
111
115
  };
112
116
  var getBody = async (request, config) => {
113
117
  if (!isSupportedBodyMethod(request.method)) {
114
- return void 0;
118
+ return null;
115
119
  }
116
- const contentType = request.headers.get("Content-Type") ?? "";
117
- if (contentType.includes("application/json")) {
118
- const json = await request.json();
120
+ const clone = request.clone();
121
+ const contentType = clone.headers.get("Content-Type") ?? "";
122
+ if (contentType.includes("application/json") || config.schemas?.body) {
123
+ const json = await clone.json();
119
124
  if (config.schemas?.body) {
120
125
  const parsed = config.schemas.body.safeParse(json);
121
126
  if (!parsed.success) {
122
- throw new AuraStackRouterError("UNPROCESSABLE_ENTITY", "Invalid request body");
127
+ throw new RouterError("UNPROCESSABLE_ENTITY", "Invalid request body");
123
128
  }
124
129
  return parsed.data;
125
130
  }
126
131
  return json;
127
132
  }
128
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
129
- return await request.formData();
130
- }
131
- if (contentType.includes("text/")) {
132
- return await request.text();
133
- }
134
- if (contentType.includes("application/octet-stream")) {
135
- return await request.arrayBuffer();
136
- }
137
- if (contentType.includes("image/") || contentType.includes("video/") || contentType.includes("audio/")) {
138
- return await request.blob();
133
+ try {
134
+ if (createContentTypeRegex(["application/x-www-form-urlencoded", "multipart/form-data"], contentType)) {
135
+ return await clone.formData();
136
+ }
137
+ if (createContentTypeRegex(["text/", "application/xml"], contentType)) {
138
+ return await clone.text();
139
+ }
140
+ if (createContentTypeRegex(["application/octet-stream"], contentType)) {
141
+ return await clone.arrayBuffer();
142
+ }
143
+ if (createContentTypeRegex(["image/", "video/", "audio/", "application/pdf"], contentType)) {
144
+ return await clone.blob();
145
+ }
146
+ return null;
147
+ } catch {
148
+ throw new RouterError("UNPROCESSABLE_ENTITY", "Invalid request body, the content-type does not match the body format");
139
149
  }
140
- return null;
150
+ };
151
+ var createContentTypeRegex = (contentTypes, contenType) => {
152
+ const regex = new RegExp(`${contentTypes.join("|")}`);
153
+ return regex.test(contenType);
141
154
  };
142
155
 
143
156
  // src/middlewares.ts
@@ -145,7 +158,7 @@ var executeGlobalMiddlewares = async (request, middlewares) => {
145
158
  if (!middlewares) return request;
146
159
  for (const middleware of middlewares) {
147
160
  if (typeof middleware !== "function") {
148
- throw new AuraStackRouterError("BAD_REQUEST", "Global middlewares must be functions");
161
+ throw new RouterError("BAD_REQUEST", "Global middlewares must be functions");
149
162
  }
150
163
  const executed = await middleware(request);
151
164
  if (executed instanceof Response) {
@@ -154,7 +167,7 @@ var executeGlobalMiddlewares = async (request, middlewares) => {
154
167
  request = executed;
155
168
  }
156
169
  if (!request || !(request instanceof Request)) {
157
- throw new AuraStackRouterError("BAD_REQUEST", "Global middleware must return a Request or Response object");
170
+ throw new RouterError("BAD_REQUEST", "Global middleware must return a Request or Response object");
158
171
  }
159
172
  return request;
160
173
  };
@@ -163,68 +176,137 @@ var executeMiddlewares = async (request, context, middlewares = []) => {
163
176
  let ctx = context;
164
177
  for (const middleware of middlewares) {
165
178
  if (typeof middleware !== "function") {
166
- throw new AuraStackRouterError("BAD_REQUEST", "Middleware must be a function");
179
+ throw new RouterError("BAD_REQUEST", "Middleware must be a function");
167
180
  }
168
181
  ctx = await middleware(request, ctx);
169
182
  }
170
183
  return ctx;
171
184
  } catch {
172
- throw new AuraStackRouterError("BAD_REQUEST", "Handler threw an error");
185
+ throw new RouterError("BAD_REQUEST", "Handler threw an error");
173
186
  }
174
187
  };
175
188
 
176
189
  // src/router.ts
190
+ var createNode = () => ({
191
+ statics: /* @__PURE__ */ new Map(),
192
+ endpoints: /* @__PURE__ */ new Map()
193
+ });
194
+ var insert = (root, endpoint) => {
195
+ if (!root || !endpoint) return;
196
+ let node = root;
197
+ const segments = endpoint.route === "/" ? [] : endpoint.route.split("/").filter(Boolean);
198
+ for (const segment of segments) {
199
+ if (segment.startsWith(":")) {
200
+ const name = segment.slice(1);
201
+ if (!node.param) {
202
+ node.param = { name, node: createNode() };
203
+ } else if (node.param.name !== name) {
204
+ throw new RouterError(
205
+ "BAD_REQUEST",
206
+ `Conflicting in the route by the dynamic segment "${node.param.name}" and "${name}"`
207
+ );
208
+ }
209
+ node = node.param.node;
210
+ } else {
211
+ if (!node.statics.has(segment)) {
212
+ node.statics.set(segment, createNode());
213
+ }
214
+ node = node.statics.get(segment);
215
+ }
216
+ }
217
+ if (node.endpoints.has(endpoint.method)) {
218
+ throw new RouterError("BAD_REQUEST", `Duplicate endpoint for ${endpoint?.method} ${endpoint?.route}`);
219
+ }
220
+ node.endpoints.set(endpoint.method, endpoint);
221
+ };
222
+ var search = (method, root, pathname) => {
223
+ let node = root;
224
+ const params = {};
225
+ const segments = pathname === "/" ? [] : pathname.split("/").filter(Boolean);
226
+ for (const segment of segments) {
227
+ if (node?.statics.has(segment)) {
228
+ node = node.statics.get(segment);
229
+ } else if (node?.param) {
230
+ params[node.param.name] = decodeURIComponent(segment);
231
+ node = node.param.node;
232
+ } else {
233
+ throw new RouterError("NOT_FOUND", `No route found for path: ${pathname}`);
234
+ }
235
+ }
236
+ if (!node.endpoints.has(method)) {
237
+ throw new RouterError("NOT_FOUND", `No route found for path: ${pathname}`);
238
+ }
239
+ return { endpoint: node.endpoints.get(method), params };
240
+ };
241
+ var handleError = async (error, request, config) => {
242
+ if (config.onError) {
243
+ try {
244
+ const response = await config.onError(error, request);
245
+ return response;
246
+ } catch {
247
+ return Response.json(
248
+ { message: "A critical failure occurred during error handling" },
249
+ { status: 500, statusText: statusText.INTERNAL_SERVER_ERROR }
250
+ );
251
+ }
252
+ }
253
+ if (isRouterError(error)) {
254
+ const { message, status, statusText: statusText2 } = error;
255
+ return Response.json({ message }, { status, statusText: statusText2 });
256
+ }
257
+ return Response.json({ message: "Internal Server Error" }, { status: 500, statusText: statusText.INTERNAL_SERVER_ERROR });
258
+ };
259
+ var handleRequest = async (method, request, config, root) => {
260
+ try {
261
+ if (!isSupportedMethod(request.method)) {
262
+ throw new RouterError("METHOD_NOT_ALLOWED", `The HTTP method '${request.method}' is not supported`);
263
+ }
264
+ const globalRequest = await executeGlobalMiddlewares(request, config.middlewares);
265
+ if (globalRequest instanceof Response) return globalRequest;
266
+ const url = new URL(globalRequest.url);
267
+ const pathnameWithBase = url.pathname;
268
+ if (globalRequest.method !== method) {
269
+ throw new RouterError("METHOD_NOT_ALLOWED", `The HTTP method '${globalRequest.method}' is not allowed`);
270
+ }
271
+ const { endpoint, params } = search(method, root, pathnameWithBase);
272
+ if (endpoint.method !== globalRequest.method) {
273
+ throw new RouterError("METHOD_NOT_ALLOWED", `The HTTP method '${globalRequest.method}' is not allowed`);
274
+ }
275
+ const dynamicParams = getRouteParams(params, endpoint.config);
276
+ const body = await getBody(globalRequest, endpoint.config);
277
+ const searchParams = getSearchParams(globalRequest.url, endpoint.config);
278
+ const headers = getHeaders(globalRequest);
279
+ const context = {
280
+ params: dynamicParams,
281
+ searchParams,
282
+ headers,
283
+ body
284
+ };
285
+ await executeMiddlewares(globalRequest, context, endpoint.config.middlewares);
286
+ const response = await endpoint.handler(globalRequest, context);
287
+ return response;
288
+ } catch (error) {
289
+ return handleError(error, request, config);
290
+ }
291
+ };
177
292
  var createRouter = (endpoints, config = {}) => {
293
+ const root = createNode();
178
294
  const server = {};
179
- const groups = /* @__PURE__ */ new Map();
295
+ const methods = /* @__PURE__ */ new Set();
180
296
  for (const endpoint of endpoints) {
181
- if (!groups.has(endpoint.method)) {
182
- groups.set(endpoint.method, []);
183
- }
184
- groups.get(endpoint.method)?.push(endpoint);
185
- }
186
- for (const method of groups.keys()) {
187
- server[method] = async (request) => {
188
- try {
189
- const globalRequest = await executeGlobalMiddlewares(request, config.middlewares);
190
- if (globalRequest instanceof Response) {
191
- return globalRequest;
192
- }
193
- const url = new URL(globalRequest.url);
194
- const pathname = url.pathname;
195
- const endpoint = groups.get(method)?.find((endpoint2) => {
196
- const withBasePath = config.basePath ? `${config.basePath}${endpoint2.route}` : endpoint2.route;
197
- const regex = createRoutePattern(withBasePath);
198
- return regex.test(pathname);
199
- });
200
- if (endpoint) {
201
- const withBasePath = config.basePath ? `${config.basePath}${endpoint.route}` : endpoint.route;
202
- const body = await getBody(globalRequest, endpoint.config);
203
- const params = getRouteParams(withBasePath, pathname);
204
- const searchParams = getSearchParams(globalRequest.url, endpoint.config);
205
- const headers = getHeaders(globalRequest);
206
- const context = {
207
- params,
208
- searchParams,
209
- headers,
210
- body
211
- };
212
- await executeMiddlewares(globalRequest, context, endpoint.config.middlewares);
213
- return endpoint.handler(globalRequest, context);
214
- }
215
- return Response.json({ message: "Not Found" }, { status: 404 });
216
- } catch (error) {
217
- if (error instanceof AuraStackRouterError) {
218
- const { message, status, statusText: statusText2 } = error;
219
- return Response.json({ message }, { status, statusText: statusText2 });
220
- }
221
- return Response.json({ message: "Internal Server Error" }, { status: 500 });
222
- }
223
- };
297
+ const withBasePath = config.basePath ? `${config.basePath}${endpoint.route}` : endpoint.route;
298
+ insert(root, { ...endpoint, route: withBasePath });
299
+ methods.add(endpoint.method);
300
+ }
301
+ for (const method of methods) {
302
+ server[method] = (request) => handleRequest(method, request, config, root);
224
303
  }
225
304
  return server;
226
305
  };
227
306
  // Annotate the CommonJS export names for ESM import in node:
228
307
  0 && (module.exports = {
229
- createRouter
308
+ createNode,
309
+ createRouter,
310
+ insert,
311
+ search
230
312
  });
package/dist/router.d.cts CHANGED
@@ -1,6 +1,28 @@
1
- import { RouteEndpoint, RouterConfig, GetHttpHandlers } from './types.cjs';
1
+ import { RouteEndpoint, RouterConfig, GetHttpHandlers, HTTPMethod, RoutePattern, EndpointSchemas, MiddlewareFunction } from './types.cjs';
2
2
  import 'zod';
3
+ import './error.cjs';
3
4
 
5
+ interface TrieNode {
6
+ statics: Map<string, TrieNode>;
7
+ param?: {
8
+ name: string;
9
+ node: TrieNode;
10
+ };
11
+ endpoints: Map<HTTPMethod, RouteEndpoint>;
12
+ }
13
+ declare const createNode: () => TrieNode;
14
+ declare const insert: (root: TrieNode, endpoint: RouteEndpoint) => void;
15
+ declare const search: (method: HTTPMethod, root: TrieNode, pathname: string) => {
16
+ endpoint: RouteEndpoint<HTTPMethod, RoutePattern, {
17
+ schemas?: EndpointSchemas | undefined;
18
+ middlewares?: MiddlewareFunction<{} | {
19
+ [x: string]: string;
20
+ }, {
21
+ schemas: EndpointSchemas;
22
+ }>[] | undefined;
23
+ }>;
24
+ params: Record<string, string>;
25
+ };
4
26
  /**
5
27
  * Creates the entry point for the server, handling the endpoints defined in the router.
6
28
  * It groups endpoints by HTTP method and matches incoming requests to the appropriate endpoint.
@@ -12,4 +34,4 @@ import 'zod';
12
34
  */
13
35
  declare const createRouter: <const Endpoints extends RouteEndpoint[]>(endpoints: Endpoints, config?: RouterConfig) => GetHttpHandlers<Endpoints>;
14
36
 
15
- export { createRouter };
37
+ export { createNode, createRouter, insert, search };
package/dist/router.d.ts CHANGED
@@ -1,6 +1,28 @@
1
- import { RouteEndpoint, RouterConfig, GetHttpHandlers } from './types.js';
1
+ import { RouteEndpoint, RouterConfig, GetHttpHandlers, HTTPMethod, RoutePattern, EndpointSchemas, MiddlewareFunction } from './types.js';
2
2
  import 'zod';
3
+ import './error.js';
3
4
 
5
+ interface TrieNode {
6
+ statics: Map<string, TrieNode>;
7
+ param?: {
8
+ name: string;
9
+ node: TrieNode;
10
+ };
11
+ endpoints: Map<HTTPMethod, RouteEndpoint>;
12
+ }
13
+ declare const createNode: () => TrieNode;
14
+ declare const insert: (root: TrieNode, endpoint: RouteEndpoint) => void;
15
+ declare const search: (method: HTTPMethod, root: TrieNode, pathname: string) => {
16
+ endpoint: RouteEndpoint<HTTPMethod, RoutePattern, {
17
+ schemas?: EndpointSchemas | undefined;
18
+ middlewares?: MiddlewareFunction<{} | {
19
+ [x: string]: string;
20
+ }, {
21
+ schemas: EndpointSchemas;
22
+ }>[] | undefined;
23
+ }>;
24
+ params: Record<string, string>;
25
+ };
4
26
  /**
5
27
  * Creates the entry point for the server, handling the endpoints defined in the router.
6
28
  * It groups endpoints by HTTP method and matches incoming requests to the appropriate endpoint.
@@ -12,4 +34,4 @@ import 'zod';
12
34
  */
13
35
  declare const createRouter: <const Endpoints extends RouteEndpoint[]>(endpoints: Endpoints, config?: RouterConfig) => GetHttpHandlers<Endpoints>;
14
36
 
15
- export { createRouter };
37
+ export { createNode, createRouter, insert, search };
package/dist/router.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import {
2
- createRouter
3
- } from "./chunk-DR4C6QTF.js";
4
- import "./chunk-OXDCFAMF.js";
5
- import "./chunk-YUX3YHXF.js";
6
- import "./chunk-JRJKKBSH.js";
7
- import "./chunk-O6SY753N.js";
8
- import "./chunk-RFYOPPMW.js";
2
+ createNode,
3
+ createRouter,
4
+ insert,
5
+ search
6
+ } from "./chunk-JIA6NLL6.js";
7
+ import "./chunk-PT4GU6PH.js";
8
+ import "./chunk-JNMXLKDG.js";
9
+ import "./chunk-FWDOXDWG.js";
10
+ import "./chunk-GJC3ODME.js";
9
11
  export {
10
- createRouter
12
+ createNode,
13
+ createRouter,
14
+ insert,
15
+ search
11
16
  };
package/dist/types.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ZodObject, z } from 'zod';
2
+ import { RouterError } from './error.cjs';
2
3
 
3
4
  /**
4
5
  * Route pattern must start with a slash and can contain parameters prefixed with a colon.
@@ -8,13 +9,14 @@ import { ZodObject, z } from 'zod';
8
9
  */
9
10
  type RoutePattern = `/${string}` | `/${string}/:${string}`;
10
11
  /**
11
- * HTTP methods supported by the router.
12
+ * HTTP methods defined in HTTP/1.1 specification.
13
+ * @see https://datatracker.ietf.org/doc/html/rfc7231#section-4.3
12
14
  */
13
- type HTTPMethod = "GET" | "POST" | "DELETE" | "PUT" | "PATCH";
15
+ type HTTPMethod = "GET" | "POST" | "DELETE" | "PUT" | "PATCH" | "OPTIONS" | "HEAD" | "TRACE" | "CONNECT";
14
16
  /**
15
17
  * Content types supported by the router.
16
18
  */
17
- type ContentType = "application/json" | "application/x-www-form-urlencoded" | "text/plain";
19
+ type ContentType = "application/json" | "application/x-www-form-urlencoded" | "text/plain" | "multipart/form-data" | "application/xml" | "application/octet-stream" | `text/${string}` | `image/${string}` | `video/${string}` | `audio/${string}` | "application/pdf";
18
20
  type Prettify<Obj extends object> = {
19
21
  [Key in keyof Obj]: Obj[Key];
20
22
  } & {};
@@ -33,20 +35,20 @@ type Prettify<Obj extends object> = {
33
35
  * // Expected: {}
34
36
  * type NoParams = Params<"/about">;
35
37
  */
36
- type Params<Route extends RoutePattern> = Route extends `/${string}/:${infer Param}/${infer Str}` ? Prettify<{
38
+ type GetRouteParams<Route extends RoutePattern> = Route extends `/${string}/:${infer Param}/${infer Str}` ? Prettify<{
37
39
  [K in Param]: string;
38
- } & Params<`/${Str}`>> : Route extends `/${string}/:${infer Param}` ? Prettify<{
40
+ } & GetRouteParams<`/${Str}`>> : Route extends `/${string}/:${infer Param}` ? Prettify<{
39
41
  [K in Param]: string;
40
42
  }> : Route extends `/:${infer Param}` ? Prettify<{
41
43
  [K in Param]: string;
42
44
  }> : {};
43
- type GetRouteParams<T extends RoutePattern> = Params<T>;
44
45
  /**
45
46
  * Available schemas validation for an endpoint. It can include body and searchParams schemas.
46
47
  */
47
48
  interface EndpointSchemas {
48
49
  body?: ZodObject<any>;
49
50
  searchParams?: ZodObject<any>;
51
+ params?: ZodObject<any>;
50
52
  }
51
53
  /**
52
54
  * Configuration for an endpoint, including optional schemas for request validation and middlewares.
@@ -77,12 +79,22 @@ type ContextBody<Schemas extends EndpointConfig["schemas"]> = Schemas extends {
77
79
  } : {
78
80
  body: undefined;
79
81
  };
82
+ /**
83
+ * Infer the type of route parameters from the provided value in the `EndpointConfig`.
84
+ */
85
+ type ContextParams<Schemas extends EndpointConfig["schemas"], Default = Record<string, string>> = Schemas extends {
86
+ params: ZodObject;
87
+ } ? {
88
+ params: z.infer<Schemas["params"]>;
89
+ } : {
90
+ params: Default;
91
+ };
80
92
  /**
81
93
  * Context object passed to route handlers and middlewares defined in the
82
94
  * `createEndpoint/createEndpointConfig` function or globally in the `createRouter` function.
83
95
  */
84
96
  interface RequestContext<RouteParams = Record<string, string>, Config extends EndpointConfig = EndpointConfig> {
85
- params: RouteParams;
97
+ params: ContextParams<Config["schemas"], RouteParams>["params"];
86
98
  headers: Headers;
87
99
  body: ContextBody<Config["schemas"]>["body"];
88
100
  searchParams: ContextSearchParams<Config["schemas"]>["searchParams"];
@@ -101,7 +113,7 @@ type MiddlewareFunction<RouteParams = Record<string, string>, Config extends End
101
113
  * The handler receives the request object and a context containing route parameters, headers,
102
114
  * and optionally validated body and search parameters based on the endpoint configuration.
103
115
  */
104
- type RouteHandler<Route extends RoutePattern, Config extends EndpointConfig> = (request: Request, ctx: Prettify<RequestContext<GetRouteParams<Route>, Config>>) => Promise<Response>;
116
+ type RouteHandler<Route extends RoutePattern, Config extends EndpointConfig> = (request: Request, ctx: Prettify<RequestContext<GetRouteParams<Route>, Config>>) => Response | Promise<Response>;
105
117
  /**
106
118
  * Represents a route endpoint definition, specifying the HTTP method, route pattern,
107
119
  * handler function with inferred context types, and associated configuration.
@@ -121,14 +133,59 @@ type InferMethod<Endpoints extends RouteEndpoint[]> = Endpoints extends unknown[
121
133
  * Each method is a function that takes a request and context, returning a promise of a response.
122
134
  */
123
135
  type GetHttpHandlers<Endpoints extends RouteEndpoint[]> = {
124
- [Method in InferMethod<Endpoints>]: (req: Request) => Promise<Response>;
136
+ [Method in InferMethod<Endpoints>]: (req: Request) => Response | Promise<Response>;
125
137
  };
126
138
  /**
127
139
  * Configuration options for `createRouter` function.
128
140
  */
129
141
  interface RouterConfig {
142
+ /**
143
+ * Prefix path for all routes/endpoints defined in the router.
144
+ *
145
+ * @example
146
+ * basePath: "/api/v1"
147
+ *
148
+ * // will match the "/users" endpoint.
149
+ * new Request("https://example.com/api/v1/users")
150
+ *
151
+ * // will NOT match the "/users" endpoint.
152
+ * new Request("https://example.com/users")
153
+ */
130
154
  basePath?: RoutePattern;
155
+ /**
156
+ * Global middlewares that run before route matching for all endpoints in the router.
157
+ * You can use this to modify the request or return a response early.
158
+ *
159
+ * @example
160
+ * middlewares: [
161
+ * async (request) => {
162
+ * if(request.headers.get("Authorization")?.startsWith("Bearer ")) {
163
+ * return Response.json({ message: "Unauthorized" }, { status: 401 })
164
+ * }
165
+ * return request
166
+ * }
167
+ * ]
168
+ */
131
169
  middlewares?: GlobalMiddleware[];
170
+ /**
171
+ * Error handler function that runs when an error is thrown in a router handler or middleware.
172
+ * It can be used to customize the default error response provided by the router. If is an internal
173
+ * error the error is from the `RouterError` class, otherwise the error is a generic
174
+ * `Error` instance which was caused by a handler or middleware, for how to distinguish them you can use
175
+ * the `isRouterError` function from the `assert` module.
176
+ *
177
+ * @param error - The error thrown in the router
178
+ * @param request - The original request that caused the error
179
+ * @returns A response to be sent back to the client
180
+ * @example
181
+ * onError: (error, request) => {
182
+ * if(isRouterError(error)) {
183
+ * return Response.json({ message: error.message }, { status: error.statusCode })
184
+ * }
185
+ * return Response.json({ message: "Internal Server Error" }, { status: 500 })
186
+ * }
187
+ */
188
+ onError?: (error: Error | RouterError, request: Request) => Response | Promise<Response>;
132
189
  }
133
190
 
134
- export type { ContentType, ContextBody, ContextSearchParams, EndpointConfig, EndpointSchemas, GetHttpHandlers, GetRouteParams, GlobalMiddleware, HTTPMethod, InferMethod, MiddlewareFunction, Params, Prettify, RequestContext, RouteEndpoint, RouteHandler, RoutePattern, RouterConfig };
191
+ export type { ContentType, ContextBody, ContextParams, ContextSearchParams, EndpointConfig, EndpointSchemas, GetHttpHandlers, GetRouteParams, GlobalMiddleware, HTTPMethod, InferMethod, MiddlewareFunction, Prettify, RequestContext, RouteEndpoint, RouteHandler, RoutePattern, RouterConfig };