react-router-define-api 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,103 +16,76 @@ npm install react-router-define-api
16
16
  // app/routes/api.users.ts
17
17
  import { defineApi } from 'react-router-define-api';
18
18
 
19
- const api = defineApi({
19
+ export const { loader, action } = defineApi({
20
20
  GET: async ({ params }) => {
21
- const users = await db.users.findMany();
22
- return { users };
21
+ return { users: await db.users.findMany() };
23
22
  },
24
23
  POST: async ({ request }) => {
25
24
  const body = await request.formData();
26
- const user = await db.users.create({ name: body.get('name') });
27
- return { user };
25
+ return { user: await db.users.create({ name: body.get('name') }) };
28
26
  },
29
27
  DELETE: async ({ params }) => {
30
28
  await db.users.delete(params.id);
31
29
  return { deleted: true };
32
30
  },
33
31
  });
34
-
35
- export const loader = api.loader;
36
- export const action = api.action;
37
32
  ```
38
33
 
39
34
  ### How it works
40
35
 
41
- - `GET` handler exported as `loader`
42
- - `POST`, `PUT`, `PATCH`, `DELETE` handlers → dispatched inside `action` by `request.method`
43
- - Undefined methods return a `405 Method Not Allowed` response
36
+ - `GET` → `loader`
37
+ - `POST`, `PUT`, `PATCH`, `DELETE` → dispatched inside `action` by `request.method`
38
+ - Undefined methods `405 Method Not Allowed`
44
39
 
45
- ### All methods
40
+ ### Handler wrapper
41
+
42
+ Wrap all handlers with a higher-order function for error handling, response transformation, logging, etc.:
46
43
 
47
44
  ```ts
48
- const api = defineApi({
49
- GET: async (args) => {
50
- /* ... */
51
- },
52
- POST: async (args) => {
53
- /* ... */
54
- },
55
- PUT: async (args) => {
56
- /* ... */
57
- },
58
- PATCH: async (args) => {
59
- /* ... */
60
- },
61
- DELETE: async (args) => {
62
- /* ... */
45
+ export const { loader, action } = defineApi({
46
+ GET: async () => ({ name: 'John' }),
47
+ POST: async ({ request }) => {
48
+ const body = await request.formData();
49
+ return { name: body.get('name') };
63
50
  },
51
+ }, {
52
+ handler: loaderActionHandler,
64
53
  });
65
54
  ```
66
55
 
67
- ## API
68
-
69
- ### `defineApi(handlers)`
70
-
71
- | Parameter | Type | Description |
72
- | ---------- | ------------- | ---------------------------------------- |
73
- | `handlers` | `ApiHandlers` | Map of HTTP methods to handler functions |
74
-
75
- Returns `{ loader, action }` — each is `undefined` if no relevant methods are defined.
56
+ Example `loaderActionHandler`:
76
57
 
77
- Handler args are the same as React Router's `LoaderFunctionArgs` / `ActionFunctionArgs`:
78
-
79
- - `request` — the incoming `Request` object
80
- - `params` route parameters
81
- - `context` app context
58
+ ```ts
59
+ const loaderActionHandler = (fn) => async (args) => {
60
+ try {
61
+ const result = await fn(args);
62
+ return { success: true, status: 200, data: result };
63
+ } catch (error) {
64
+ return { success: false, status: 500, message: String(error) };
65
+ }
66
+ };
67
+ ```
82
68
 
83
- ## TypeScript
69
+ ### Response type helpers
84
70
 
85
- Full type inference — return types are inferred from your handler functions.
71
+ Access inferred response types for client-side fetchers or shared contracts:
86
72
 
87
73
  ```ts
88
74
  const api = defineApi({
89
75
  GET: async ({ params }) => ({ id: params.id, name: 'John' }),
90
- POST: async ({ request }) => {
91
- const body = await request.formData();
92
- return { created: true, name: body.get('name') };
93
- },
76
+ POST: async () => ({ created: true }),
94
77
  });
95
78
 
96
- // api.loader return type is inferred as { id: string | undefined, name: string }
97
- // api.action is undefined when no action methods defined
98
- ```
99
-
100
- ### Response type helpers
79
+ export const { loader, action } = api;
101
80
 
102
- Access inferred response types via `typeof api.*Response` — useful for typing client-side fetchers or shared contracts:
103
-
104
- ```ts
105
81
  type GetRes = typeof api.GetResponse;
106
82
  // → { id: string | undefined; name: string }
107
83
 
108
84
  type PostRes = typeof api.PostResponse;
109
- // → { created: boolean; name: FormDataEntryValue | null }
110
-
111
- // Undefined methods → never
112
- type PutRes = typeof api.PutResponse; // → never
85
+ // → { created: boolean }
113
86
  ```
114
87
 
115
- These are **type-only** — zero runtime cost.
88
+ These are **type-only** — zero runtime cost. Undefined methods resolve to `never`.
116
89
 
117
90
  ## License
118
91
 
package/dist/index.cjs CHANGED
@@ -31,17 +31,21 @@ var ACTION_METHODS = [
31
31
  "PATCH",
32
32
  "DELETE"
33
33
  ];
34
- function defineApi(handlers) {
35
- const loader = handlers.GET ?? void 0;
36
- const hasActionHandlers = ACTION_METHODS.some((m) => handlers[m] != null);
37
- const action = hasActionHandlers ? async (args) => {
34
+ function defineApi(handlers, options) {
35
+ const { handler: wrapper } = options ?? {};
36
+ const wrap = (fn) => wrapper ? wrapper(fn) : fn;
37
+ const loader = handlers.GET ? wrap(handlers.GET) : void 0;
38
+ const hasActionHandlers = ACTION_METHODS.some(
39
+ (m) => handlers[m] != null
40
+ );
41
+ const action = hasActionHandlers ? wrap(async (args) => {
38
42
  const method = args.request.method.toUpperCase();
39
43
  const handler = handlers[method];
40
44
  if (typeof handler === "function") {
41
45
  return handler(args);
42
46
  }
43
47
  throw new Response("Method Not Allowed", { status: 405 });
44
- } : void 0;
48
+ }) : void 0;
45
49
  return { loader, action };
46
50
  }
47
51
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -28,22 +28,47 @@ type ActionReturn<H> = (H extends {
28
28
  } ? HandlerReturn<F> : never);
29
29
  /** Extract response type for a specific method, or never if not defined */
30
30
  type ResponseOf<H, M extends HttpMethod> = H extends Record<M, infer F> ? HandlerReturn<F> : never;
31
+ /**
32
+ * Higher-order function that wraps each method handler.
33
+ * Receives the original handler and returns a new handler with transformed behavior.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const loaderActionHandler: HandlerWrapper<BaseResponse<T>> = (fn) =>
38
+ * async (args) => {
39
+ * try {
40
+ * const result = await fn(args);
41
+ * return { success: true, status: 200, data: result };
42
+ * } catch (error) {
43
+ * return { success: false, status: 500, message: String(error) };
44
+ * }
45
+ * };
46
+ * ```
47
+ */
48
+ type HandlerWrapper<W = unknown> = <TArgs, TResult>(fn: (args: TArgs) => Promise<TResult>) => (args: TArgs) => Promise<W>;
49
+ /** Options for defineApi */
50
+ interface DefineApiOptions<W = never> {
51
+ /** Higher-order function to wrap all method handlers (e.g. for error handling, response transformation) */
52
+ handler?: HandlerWrapper<W>;
53
+ }
54
+ /** Apply wrapper type: if wrapper is provided, use its return type; otherwise use the raw handler return */
55
+ type WithWrapper<R, W> = [W] extends [never] ? R : Awaited<W>;
31
56
  /** Return type of defineApi — loader/action presence and return types inferred from handlers */
32
- type ApiExports<H extends ApiHandlers> = {
57
+ type ApiExports<H extends ApiHandlers, W = never> = {
33
58
  loader: H extends {
34
59
  GET: infer F;
35
- } ? (args: LoaderFunctionArgs) => Promise<HandlerReturn<F>> : undefined;
36
- action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<ActionReturn<H>> : undefined;
60
+ } ? (args: LoaderFunctionArgs) => Promise<WithWrapper<HandlerReturn<F>, W>> : undefined;
61
+ action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<WithWrapper<ActionReturn<H>, W>> : undefined;
37
62
  /** Inferred return type of the GET handler */
38
- GetResponse: ResponseOf<H, 'GET'>;
63
+ GetResponse: WithWrapper<ResponseOf<H, 'GET'>, W>;
39
64
  /** Inferred return type of the POST handler */
40
- PostResponse: ResponseOf<H, 'POST'>;
65
+ PostResponse: WithWrapper<ResponseOf<H, 'POST'>, W>;
41
66
  /** Inferred return type of the PUT handler */
42
- PutResponse: ResponseOf<H, 'PUT'>;
67
+ PutResponse: WithWrapper<ResponseOf<H, 'PUT'>, W>;
43
68
  /** Inferred return type of the PATCH handler */
44
- PatchResponse: ResponseOf<H, 'PATCH'>;
69
+ PatchResponse: WithWrapper<ResponseOf<H, 'PATCH'>, W>;
45
70
  /** Inferred return type of the DELETE handler */
46
- DeleteResponse: ResponseOf<H, 'DELETE'>;
71
+ DeleteResponse: WithWrapper<ResponseOf<H, 'DELETE'>, W>;
47
72
  };
48
73
 
49
74
  /**
@@ -52,18 +77,24 @@ type ApiExports<H extends ApiHandlers> = {
52
77
  *
53
78
  * @example
54
79
  * ```ts
55
- * const api = defineApi({
80
+ * export const { loader, action } = defineApi({
56
81
  * GET: async ({ params }) => ({ user: params.id }),
57
82
  * POST: async ({ request }) => {
58
83
  * const body = await request.formData();
59
84
  * return { created: true };
60
85
  * },
61
86
  * });
87
+ * ```
62
88
  *
63
- * export const loader = api.loader;
64
- * export const action = api.action;
89
+ * @example With handler wrapper
90
+ * ```ts
91
+ * export const { loader, action } = defineApi({
92
+ * GET: async () => ({ data: 'ok' }),
93
+ * }, {
94
+ * handler: loaderActionHandler,
95
+ * });
65
96
  * ```
66
97
  */
67
- declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
98
+ declare function defineApi<const H extends ApiHandlers, W = never>(handlers: H, options?: DefineApiOptions<W>): ApiExports<H, W>;
68
99
 
69
- export { type ApiHandlers, type HttpMethod, type MethodHandler, defineApi };
100
+ export { type ApiHandlers, type DefineApiOptions, type HandlerWrapper, type HttpMethod, type MethodHandler, defineApi };
package/dist/index.d.ts CHANGED
@@ -28,22 +28,47 @@ type ActionReturn<H> = (H extends {
28
28
  } ? HandlerReturn<F> : never);
29
29
  /** Extract response type for a specific method, or never if not defined */
30
30
  type ResponseOf<H, M extends HttpMethod> = H extends Record<M, infer F> ? HandlerReturn<F> : never;
31
+ /**
32
+ * Higher-order function that wraps each method handler.
33
+ * Receives the original handler and returns a new handler with transformed behavior.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const loaderActionHandler: HandlerWrapper<BaseResponse<T>> = (fn) =>
38
+ * async (args) => {
39
+ * try {
40
+ * const result = await fn(args);
41
+ * return { success: true, status: 200, data: result };
42
+ * } catch (error) {
43
+ * return { success: false, status: 500, message: String(error) };
44
+ * }
45
+ * };
46
+ * ```
47
+ */
48
+ type HandlerWrapper<W = unknown> = <TArgs, TResult>(fn: (args: TArgs) => Promise<TResult>) => (args: TArgs) => Promise<W>;
49
+ /** Options for defineApi */
50
+ interface DefineApiOptions<W = never> {
51
+ /** Higher-order function to wrap all method handlers (e.g. for error handling, response transformation) */
52
+ handler?: HandlerWrapper<W>;
53
+ }
54
+ /** Apply wrapper type: if wrapper is provided, use its return type; otherwise use the raw handler return */
55
+ type WithWrapper<R, W> = [W] extends [never] ? R : Awaited<W>;
31
56
  /** Return type of defineApi — loader/action presence and return types inferred from handlers */
32
- type ApiExports<H extends ApiHandlers> = {
57
+ type ApiExports<H extends ApiHandlers, W = never> = {
33
58
  loader: H extends {
34
59
  GET: infer F;
35
- } ? (args: LoaderFunctionArgs) => Promise<HandlerReturn<F>> : undefined;
36
- action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<ActionReturn<H>> : undefined;
60
+ } ? (args: LoaderFunctionArgs) => Promise<WithWrapper<HandlerReturn<F>, W>> : undefined;
61
+ action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<WithWrapper<ActionReturn<H>, W>> : undefined;
37
62
  /** Inferred return type of the GET handler */
38
- GetResponse: ResponseOf<H, 'GET'>;
63
+ GetResponse: WithWrapper<ResponseOf<H, 'GET'>, W>;
39
64
  /** Inferred return type of the POST handler */
40
- PostResponse: ResponseOf<H, 'POST'>;
65
+ PostResponse: WithWrapper<ResponseOf<H, 'POST'>, W>;
41
66
  /** Inferred return type of the PUT handler */
42
- PutResponse: ResponseOf<H, 'PUT'>;
67
+ PutResponse: WithWrapper<ResponseOf<H, 'PUT'>, W>;
43
68
  /** Inferred return type of the PATCH handler */
44
- PatchResponse: ResponseOf<H, 'PATCH'>;
69
+ PatchResponse: WithWrapper<ResponseOf<H, 'PATCH'>, W>;
45
70
  /** Inferred return type of the DELETE handler */
46
- DeleteResponse: ResponseOf<H, 'DELETE'>;
71
+ DeleteResponse: WithWrapper<ResponseOf<H, 'DELETE'>, W>;
47
72
  };
48
73
 
49
74
  /**
@@ -52,18 +77,24 @@ type ApiExports<H extends ApiHandlers> = {
52
77
  *
53
78
  * @example
54
79
  * ```ts
55
- * const api = defineApi({
80
+ * export const { loader, action } = defineApi({
56
81
  * GET: async ({ params }) => ({ user: params.id }),
57
82
  * POST: async ({ request }) => {
58
83
  * const body = await request.formData();
59
84
  * return { created: true };
60
85
  * },
61
86
  * });
87
+ * ```
62
88
  *
63
- * export const loader = api.loader;
64
- * export const action = api.action;
89
+ * @example With handler wrapper
90
+ * ```ts
91
+ * export const { loader, action } = defineApi({
92
+ * GET: async () => ({ data: 'ok' }),
93
+ * }, {
94
+ * handler: loaderActionHandler,
95
+ * });
65
96
  * ```
66
97
  */
67
- declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
98
+ declare function defineApi<const H extends ApiHandlers, W = never>(handlers: H, options?: DefineApiOptions<W>): ApiExports<H, W>;
68
99
 
69
- export { type ApiHandlers, type HttpMethod, type MethodHandler, defineApi };
100
+ export { type ApiHandlers, type DefineApiOptions, type HandlerWrapper, type HttpMethod, type MethodHandler, defineApi };
package/dist/index.js CHANGED
@@ -5,17 +5,21 @@ var ACTION_METHODS = [
5
5
  "PATCH",
6
6
  "DELETE"
7
7
  ];
8
- function defineApi(handlers) {
9
- const loader = handlers.GET ?? void 0;
10
- const hasActionHandlers = ACTION_METHODS.some((m) => handlers[m] != null);
11
- const action = hasActionHandlers ? async (args) => {
8
+ function defineApi(handlers, options) {
9
+ const { handler: wrapper } = options ?? {};
10
+ const wrap = (fn) => wrapper ? wrapper(fn) : fn;
11
+ const loader = handlers.GET ? wrap(handlers.GET) : void 0;
12
+ const hasActionHandlers = ACTION_METHODS.some(
13
+ (m) => handlers[m] != null
14
+ );
15
+ const action = hasActionHandlers ? wrap(async (args) => {
12
16
  const method = args.request.method.toUpperCase();
13
17
  const handler = handlers[method];
14
18
  if (typeof handler === "function") {
15
19
  return handler(args);
16
20
  }
17
21
  throw new Response("Method Not Allowed", { status: 405 });
18
- } : void 0;
22
+ }) : void 0;
19
23
  return { loader, action };
20
24
  }
21
25
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-router-define-api",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Define HTTP method handlers for React Router v7 routes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -49,5 +49,13 @@
49
49
  "typescript": "^6.0.2",
50
50
  "ultracite": "^7.4.0",
51
51
  "vitest": "^4.1.2"
52
- }
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/sonicname/react-router-define-api.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/sonicname/react-router-define-api/issues"
59
+ },
60
+ "homepage": "https://github.com/sonicname/react-router-define-api#readme"
53
61
  }