react-router-define-api 0.1.4 → 0.1.6

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
@@ -31,41 +31,137 @@ export const { loader, action } = defineApi({
31
31
  });
32
32
  ```
33
33
 
34
+ ### Builder API
35
+
36
+ Prefer a fluent style? Call `defineApi()` with no arguments to get a chainable builder:
37
+
38
+ ```ts
39
+ import { defineApi } from 'react-router-define-api';
40
+
41
+ export const { loader, action } = defineApi()
42
+ .middleware([auth, logger])
43
+ .get(async ({ params }) => {
44
+ return { user: await db.users.find(params.id) };
45
+ })
46
+ .post(async ({ request }) => {
47
+ const body = await request.formData();
48
+ return { user: await db.users.create({ name: body.get('name') }) };
49
+ })
50
+ .delete(async ({ params }) => {
51
+ await db.users.delete(params.id);
52
+ return { deleted: true };
53
+ })
54
+ .build();
55
+ ```
56
+
57
+ Available methods: `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.middleware()`, `.build()`. Each accepts plain functions or validated handler configs:
58
+
59
+ ```ts
60
+ export const { loader, action } = defineApi()
61
+ .middleware([auth, logger])
62
+ .get({
63
+ params: z.object({ id: z.string().uuid() }),
64
+ handler: async ({ params }) => {
65
+ return { user: await db.users.find(params.id) };
66
+ },
67
+ })
68
+ .post({
69
+ body: z.object({ name: z.string() }),
70
+ handler: async ({ body }) => {
71
+ return { user: await db.users.create(body) };
72
+ },
73
+ })
74
+ .delete({
75
+ params: z.object({ id: z.string() }),
76
+ handler: async ({ params }) => {
77
+ await db.users.delete(params.id);
78
+ return { deleted: true };
79
+ },
80
+ })
81
+ .build();
82
+ ```
83
+
34
84
  ### How it works
35
85
 
36
86
  - `GET` → `loader`
37
87
  - `POST`, `PUT`, `PATCH`, `DELETE` → dispatched inside `action` by `request.method`
38
88
  - Undefined methods → `405 Method Not Allowed`
39
89
 
40
- ### Handler wrapper
90
+ ### Middleware
41
91
 
42
- Wrap all handlers with a higher-order function for error handling, response transformation, logging, etc.:
92
+ Add middleware that runs before every handler. Middleware uses the onion model — call `next()` to proceed, or return early to short-circuit.
43
93
 
44
94
  ```ts
95
+ import type { MiddlewareFn } from 'react-router-define-api';
96
+
97
+ const auth: MiddlewareFn = async (args, next) => {
98
+ const token = args.request.headers.get('Authorization');
99
+ if (!token) {
100
+ throw new Response('Unauthorized', { status: 401 });
101
+ }
102
+ return next();
103
+ };
104
+
105
+ const logger: MiddlewareFn = async (args, next) => {
106
+ console.log(`${args.request.method} ${args.request.url}`);
107
+ return next();
108
+ };
109
+
45
110
  export const { loader, action } = defineApi({
46
- GET: async () => ({ name: 'John' }),
111
+ middleware: [auth, logger],
112
+ GET: async ({ params }) => ({ user: params.id }),
47
113
  POST: async ({ request }) => {
48
114
  const body = await request.formData();
49
- return { name: body.get('name') };
115
+ return { created: true };
50
116
  },
51
- }, {
52
- handler: loaderActionHandler,
53
117
  });
54
118
  ```
55
119
 
56
- Example `loaderActionHandler`:
120
+ Middleware executes in array order. Each middleware can:
121
+
122
+ - **Pass through** — call `next()` and return its result
123
+ - **Short-circuit** — return a value without calling `next()`
124
+ - **Transform** — call `next()`, modify the result, then return it
125
+
126
+ ### Request validation
127
+
128
+ Validate params and body using any schema library with a `.parse()` method (Zod, Valibot, ArkType, etc.). Pass a handler config object instead of a plain function:
57
129
 
58
130
  ```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
- };
131
+ import { z } from 'zod';
132
+
133
+ export const { loader, action } = defineApi({
134
+ GET: {
135
+ params: z.object({ id: z.string().uuid() }),
136
+ handler: async ({ params }) => {
137
+ return { user: await db.users.find(params.id) };
138
+ },
139
+ },
140
+ POST: {
141
+ body: z.object({ name: z.string(), email: z.string().email() }),
142
+ handler: async ({ body }) => {
143
+ return { user: await db.users.create(body) };
144
+ },
145
+ },
146
+ PUT: {
147
+ params: z.object({ id: z.string() }),
148
+ body: z.object({ name: z.string() }),
149
+ handler: async ({ params, body }) => {
150
+ return { user: await db.users.update(params.id, body) };
151
+ },
152
+ },
153
+ });
67
154
  ```
68
155
 
156
+ - **`params`** — validates `args.params` (route path parameters)
157
+ - **`body`** — auto-parses the request body based on `Content-Type`, then validates:
158
+ - `application/json` → `request.json()`
159
+ - `application/x-www-form-urlencoded` / `multipart/form-data` → `request.formData()`
160
+ - `text/*` → `request.text()`
161
+ - Other → `415 Unsupported Media Type`
162
+ - Validation failure → `400` response with error details
163
+ - Plain functions and handler configs can be mixed in the same `defineApi` call
164
+
69
165
  ### Response type helpers
70
166
 
71
167
  Access inferred response types for client-side fetchers or shared contracts:
package/dist/index.cjs CHANGED
@@ -20,10 +20,71 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ApiBuilder: () => ApiBuilder,
23
24
  defineApi: () => defineApi
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
27
 
28
+ // src/api-builder.ts
29
+ var ApiBuilder = class _ApiBuilder {
30
+ handlers;
31
+ constructor(handlers = {}) {
32
+ this.handlers = handlers;
33
+ }
34
+ /** Add middleware that runs before every handler */
35
+ middleware(fns) {
36
+ return new _ApiBuilder({
37
+ ...this.handlers,
38
+ middleware: [...this.handlers.middleware ?? [], ...fns]
39
+ });
40
+ }
41
+ /** Define the GET handler (becomes the loader) */
42
+ get(handler) {
43
+ return new _ApiBuilder({ ...this.handlers, GET: handler });
44
+ }
45
+ /** Define the POST handler */
46
+ post(handler) {
47
+ return new _ApiBuilder({ ...this.handlers, POST: handler });
48
+ }
49
+ /** Define the PUT handler */
50
+ put(handler) {
51
+ return new _ApiBuilder({ ...this.handlers, PUT: handler });
52
+ }
53
+ /** Define the PATCH handler */
54
+ patch(handler) {
55
+ return new _ApiBuilder({ ...this.handlers, PATCH: handler });
56
+ }
57
+ /** Define the DELETE handler */
58
+ delete(handler) {
59
+ return new _ApiBuilder({
60
+ ...this.handlers,
61
+ DELETE: handler
62
+ });
63
+ }
64
+ /** Build and return { loader, action } with response type helpers */
65
+ build() {
66
+ return _ApiBuilder._buildFn(this.handlers);
67
+ }
68
+ /** @internal — set by define-api.ts to wire up the build logic */
69
+ static _buildFn;
70
+ };
71
+
72
+ // src/parse-body.ts
73
+ async function parseBody(request) {
74
+ const contentType = request.headers.get("content-type") ?? "";
75
+ if (contentType.includes("application/json")) {
76
+ return request.json();
77
+ }
78
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
79
+ const formData = await request.formData();
80
+ return Object.fromEntries(formData);
81
+ }
82
+ if (contentType.includes("text/")) {
83
+ return request.text();
84
+ }
85
+ throw new Response("Unsupported Media Type", { status: 415 });
86
+ }
87
+
27
88
  // src/define-api.ts
28
89
  var ACTION_METHODS = [
29
90
  "POST",
@@ -31,24 +92,85 @@ var ACTION_METHODS = [
31
92
  "PATCH",
32
93
  "DELETE"
33
94
  ];
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;
95
+ function isHandlerDef(entry) {
96
+ return typeof entry === "object" && "handler" in entry;
97
+ }
98
+ function runMiddleware(middleware, args, handler) {
99
+ let index = 0;
100
+ const next = async () => {
101
+ if (index < middleware.length) {
102
+ return middleware[index++](args, next);
103
+ }
104
+ return handler(args);
105
+ };
106
+ return next();
107
+ }
108
+ function wrapWithValidation(entry) {
109
+ if (!isHandlerDef(entry)) return entry;
110
+ const { params: paramsSchema, body: bodySchema, handler } = entry;
111
+ return async (args) => {
112
+ let validatedParams = args.params;
113
+ if (paramsSchema) {
114
+ try {
115
+ validatedParams = paramsSchema.parse(args.params);
116
+ } catch (error) {
117
+ throw new Response(
118
+ JSON.stringify({ error: "Validation failed", details: error }),
119
+ { status: 400, headers: { "content-type": "application/json" } }
120
+ );
121
+ }
122
+ }
123
+ let body;
124
+ if (bodySchema) {
125
+ const raw = await parseBody(args.request);
126
+ try {
127
+ body = bodySchema.parse(raw);
128
+ } catch (error) {
129
+ throw new Response(
130
+ JSON.stringify({ error: "Validation failed", details: error }),
131
+ { status: 400, headers: { "content-type": "application/json" } }
132
+ );
133
+ }
134
+ }
135
+ const handlerArgs = {
136
+ ...args,
137
+ params: validatedParams,
138
+ ...bodySchema ? { body } : {}
139
+ };
140
+ return handler(handlerArgs);
141
+ };
142
+ }
143
+ function buildExports(handlers) {
144
+ const mw = handlers.middleware ?? [];
145
+ const getEntry = handlers.GET;
146
+ const getHandler = getEntry ? wrapWithValidation(getEntry) : void 0;
147
+ const loader = getHandler ? mw.length > 0 ? (args) => runMiddleware(mw, args, getHandler) : getHandler : void 0;
38
148
  const hasActionHandlers = ACTION_METHODS.some(
39
149
  (m) => handlers[m] != null
40
150
  );
41
- const action = hasActionHandlers ? wrap(async (args) => {
151
+ const action = hasActionHandlers ? async (args) => {
42
152
  const method = args.request.method.toUpperCase();
43
- const handler = handlers[method];
44
- if (typeof handler === "function") {
45
- return handler(args);
153
+ const entry = handlers[method];
154
+ if (entry == null) {
155
+ throw new Response("Method Not Allowed", { status: 405 });
46
156
  }
47
- throw new Response("Method Not Allowed", { status: 405 });
48
- }) : void 0;
157
+ const handler = wrapWithValidation(entry);
158
+ if (mw.length > 0) {
159
+ return runMiddleware(mw, args, handler);
160
+ }
161
+ return handler(args);
162
+ } : void 0;
49
163
  return { loader, action };
50
164
  }
165
+ ApiBuilder._buildFn = buildExports;
166
+ function defineApi(handlers) {
167
+ if (handlers === void 0) {
168
+ return new ApiBuilder();
169
+ }
170
+ return buildExports(handlers);
171
+ }
51
172
  // Annotate the CommonJS export names for ESM import in node:
52
173
  0 && (module.exports = {
174
+ ApiBuilder,
53
175
  defineApi
54
176
  });
package/dist/index.d.cts CHANGED
@@ -4,16 +4,35 @@ import { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
4
4
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
5
  /** Handler function for a given HTTP method */
6
6
  type MethodHandler<Args, R = unknown> = (args: Args) => R | Promise<R>;
7
+ /** Route handler args (union of loader and action args) */
8
+ type HandlerArgs = LoaderFunctionArgs | ActionFunctionArgs;
9
+ /** Middleware function — call next() to proceed, or return early to short-circuit */
10
+ type MiddlewareFn = (args: HandlerArgs, next: () => Promise<unknown>) => unknown | Promise<unknown>;
11
+ /** Generic schema interface — works with Zod, Valibot, ArkType, etc. */
12
+ interface Schema<T = unknown> {
13
+ parse(input: unknown): T;
14
+ }
15
+ /** Handler definition with optional validation schemas */
16
+ interface HandlerDef {
17
+ params?: Schema;
18
+ body?: Schema;
19
+ handler: (args: HandlerArgs & {
20
+ body?: unknown;
21
+ }) => unknown;
22
+ }
7
23
  /** Map of HTTP method handlers passed to defineApi */
8
24
  type ApiHandlers = {
9
- GET?: (args: LoaderFunctionArgs) => unknown;
10
- POST?: (args: ActionFunctionArgs) => unknown;
11
- PUT?: (args: ActionFunctionArgs) => unknown;
12
- PATCH?: (args: ActionFunctionArgs) => unknown;
13
- DELETE?: (args: ActionFunctionArgs) => unknown;
25
+ middleware?: MiddlewareFn[];
26
+ GET?: ((args: LoaderFunctionArgs) => unknown) | HandlerDef;
27
+ POST?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
28
+ PUT?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
29
+ PATCH?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
30
+ DELETE?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
14
31
  };
15
- /** Extract the awaited return type from a handler */
16
- type HandlerReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
32
+ /** Extract the awaited return type from a handler (plain function or config) */
33
+ type HandlerReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T extends {
34
+ handler: (...args: never[]) => infer R;
35
+ } ? Awaited<R> : never;
17
36
  /** Checks if handler map includes any action methods */
18
37
  type HasActionMethods<H> = 'POST' extends keyof H ? true : 'PUT' extends keyof H ? true : 'PATCH' extends keyof H ? true : 'DELETE' extends keyof H ? true : false;
19
38
  /** Build union return type from all defined action handlers */
@@ -28,73 +47,92 @@ type ActionReturn<H> = (H extends {
28
47
  } ? HandlerReturn<F> : never);
29
48
  /** Extract response type for a specific method, or never if not defined */
30
49
  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<unknown>> =
38
- * (fn) => 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> = (fn: (...args: any[]) => any) => (...args: any[]) => 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>;
56
50
  /** Return type of defineApi — loader/action presence and return types inferred from handlers */
57
- type ApiExports<H extends ApiHandlers, W = never> = {
51
+ type ApiExports<H extends ApiHandlers> = {
58
52
  loader: H extends {
59
53
  GET: infer F;
60
- } ? (args: LoaderFunctionArgs) => Promise<WithWrapper<HandlerReturn<F>, W>> : undefined;
61
- action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<WithWrapper<ActionReturn<H>, W>> : undefined;
54
+ } ? (args: LoaderFunctionArgs) => Promise<HandlerReturn<F>> : undefined;
55
+ action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<ActionReturn<H>> : undefined;
62
56
  /** Inferred return type of the GET handler */
63
- GetResponse: WithWrapper<ResponseOf<H, 'GET'>, W>;
57
+ GetResponse: ResponseOf<H, 'GET'>;
64
58
  /** Inferred return type of the POST handler */
65
- PostResponse: WithWrapper<ResponseOf<H, 'POST'>, W>;
59
+ PostResponse: ResponseOf<H, 'POST'>;
66
60
  /** Inferred return type of the PUT handler */
67
- PutResponse: WithWrapper<ResponseOf<H, 'PUT'>, W>;
61
+ PutResponse: ResponseOf<H, 'PUT'>;
68
62
  /** Inferred return type of the PATCH handler */
69
- PatchResponse: WithWrapper<ResponseOf<H, 'PATCH'>, W>;
63
+ PatchResponse: ResponseOf<H, 'PATCH'>;
70
64
  /** Inferred return type of the DELETE handler */
71
- DeleteResponse: WithWrapper<ResponseOf<H, 'DELETE'>, W>;
65
+ DeleteResponse: ResponseOf<H, 'DELETE'>;
72
66
  };
73
67
 
68
+ /** Loader handler — plain function or validated config */
69
+ type LoaderEntry = ((args: LoaderFunctionArgs) => unknown) | HandlerDef;
70
+ /** Action handler — plain function or validated config */
71
+ type ActionEntry = ((args: ActionFunctionArgs) => unknown) | HandlerDef;
74
72
  /**
75
- * Define HTTP method handlers for a React Router v7 route.
76
- * Returns `{ loader, action }` ready to export from a route module.
73
+ * Fluent builder for defining HTTP method handlers.
74
+ * Each method returns a new builder with updated type state.
77
75
  *
78
76
  * @example
79
77
  * ```ts
78
+ * const api = new ApiBuilder()
79
+ * .get(async ({ params }) => ({ user: params.id }))
80
+ * .post(async ({ request }) => ({ created: true }))
81
+ * .build();
82
+ * export const { loader, action } = api;
83
+ * ```
84
+ */
85
+ declare class ApiBuilder<H extends ApiHandlers = object> {
86
+ private handlers;
87
+ constructor(handlers?: ApiHandlers);
88
+ /** Add middleware that runs before every handler */
89
+ middleware(fns: MiddlewareFn[]): ApiBuilder<H>;
90
+ /** Define the GET handler (becomes the loader) */
91
+ get<F extends LoaderEntry>(handler: F): ApiBuilder<H & {
92
+ GET: F;
93
+ }>;
94
+ /** Define the POST handler */
95
+ post<F extends ActionEntry>(handler: F): ApiBuilder<H & {
96
+ POST: F;
97
+ }>;
98
+ /** Define the PUT handler */
99
+ put<F extends ActionEntry>(handler: F): ApiBuilder<H & {
100
+ PUT: F;
101
+ }>;
102
+ /** Define the PATCH handler */
103
+ patch<F extends ActionEntry>(handler: F): ApiBuilder<H & {
104
+ PATCH: F;
105
+ }>;
106
+ /** Define the DELETE handler */
107
+ delete<F extends ActionEntry>(handler: F): ApiBuilder<H & {
108
+ DELETE: F;
109
+ }>;
110
+ /** Build and return { loader, action } with response type helpers */
111
+ build(): ApiExports<H>;
112
+ /** @internal — set by define-api.ts to wire up the build logic */
113
+ static _buildFn: (handlers: ApiHandlers) => unknown;
114
+ }
115
+
116
+ /**
117
+ * Define HTTP method handlers for a React Router v7 route.
118
+ *
119
+ * **Object style** — pass all handlers at once:
120
+ * ```ts
80
121
  * export const { loader, action } = defineApi({
81
122
  * GET: async ({ params }) => ({ user: params.id }),
82
- * POST: async ({ request }) => {
83
- * const body = await request.formData();
84
- * return { created: true };
85
- * },
123
+ * POST: async ({ request }) => ({ created: true }),
86
124
  * });
87
125
  * ```
88
126
  *
89
- * @example With handler wrapper
127
+ * **Builder style** chain methods fluently:
90
128
  * ```ts
91
- * export const { loader, action } = defineApi({
92
- * GET: async () => ({ data: 'ok' }),
93
- * }, {
94
- * handler: loaderActionHandler,
95
- * });
129
+ * export const { loader, action } = defineApi()
130
+ * .get(async ({ params }) => ({ user: params.id }))
131
+ * .post(async ({ request }) => ({ created: true }))
132
+ * .build();
96
133
  * ```
97
134
  */
98
- declare function defineApi<const H extends ApiHandlers, W = never>(handlers: H, options?: DefineApiOptions<W>): ApiExports<H, W>;
135
+ declare function defineApi(): ApiBuilder;
136
+ declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
99
137
 
100
- export { type ApiHandlers, type DefineApiOptions, type HandlerWrapper, type HttpMethod, type MethodHandler, defineApi };
138
+ export { ApiBuilder, type ApiHandlers, type HandlerArgs, type HandlerDef, type HttpMethod, type MethodHandler, type MiddlewareFn, type Schema, defineApi };
package/dist/index.d.ts CHANGED
@@ -4,16 +4,35 @@ import { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
4
4
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
5
  /** Handler function for a given HTTP method */
6
6
  type MethodHandler<Args, R = unknown> = (args: Args) => R | Promise<R>;
7
+ /** Route handler args (union of loader and action args) */
8
+ type HandlerArgs = LoaderFunctionArgs | ActionFunctionArgs;
9
+ /** Middleware function — call next() to proceed, or return early to short-circuit */
10
+ type MiddlewareFn = (args: HandlerArgs, next: () => Promise<unknown>) => unknown | Promise<unknown>;
11
+ /** Generic schema interface — works with Zod, Valibot, ArkType, etc. */
12
+ interface Schema<T = unknown> {
13
+ parse(input: unknown): T;
14
+ }
15
+ /** Handler definition with optional validation schemas */
16
+ interface HandlerDef {
17
+ params?: Schema;
18
+ body?: Schema;
19
+ handler: (args: HandlerArgs & {
20
+ body?: unknown;
21
+ }) => unknown;
22
+ }
7
23
  /** Map of HTTP method handlers passed to defineApi */
8
24
  type ApiHandlers = {
9
- GET?: (args: LoaderFunctionArgs) => unknown;
10
- POST?: (args: ActionFunctionArgs) => unknown;
11
- PUT?: (args: ActionFunctionArgs) => unknown;
12
- PATCH?: (args: ActionFunctionArgs) => unknown;
13
- DELETE?: (args: ActionFunctionArgs) => unknown;
25
+ middleware?: MiddlewareFn[];
26
+ GET?: ((args: LoaderFunctionArgs) => unknown) | HandlerDef;
27
+ POST?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
28
+ PUT?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
29
+ PATCH?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
30
+ DELETE?: ((args: ActionFunctionArgs) => unknown) | HandlerDef;
14
31
  };
15
- /** Extract the awaited return type from a handler */
16
- type HandlerReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : never;
32
+ /** Extract the awaited return type from a handler (plain function or config) */
33
+ type HandlerReturn<T> = T extends (...args: never[]) => infer R ? Awaited<R> : T extends {
34
+ handler: (...args: never[]) => infer R;
35
+ } ? Awaited<R> : never;
17
36
  /** Checks if handler map includes any action methods */
18
37
  type HasActionMethods<H> = 'POST' extends keyof H ? true : 'PUT' extends keyof H ? true : 'PATCH' extends keyof H ? true : 'DELETE' extends keyof H ? true : false;
19
38
  /** Build union return type from all defined action handlers */
@@ -28,73 +47,92 @@ type ActionReturn<H> = (H extends {
28
47
  } ? HandlerReturn<F> : never);
29
48
  /** Extract response type for a specific method, or never if not defined */
30
49
  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<unknown>> =
38
- * (fn) => 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> = (fn: (...args: any[]) => any) => (...args: any[]) => 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>;
56
50
  /** Return type of defineApi — loader/action presence and return types inferred from handlers */
57
- type ApiExports<H extends ApiHandlers, W = never> = {
51
+ type ApiExports<H extends ApiHandlers> = {
58
52
  loader: H extends {
59
53
  GET: infer F;
60
- } ? (args: LoaderFunctionArgs) => Promise<WithWrapper<HandlerReturn<F>, W>> : undefined;
61
- action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<WithWrapper<ActionReturn<H>, W>> : undefined;
54
+ } ? (args: LoaderFunctionArgs) => Promise<HandlerReturn<F>> : undefined;
55
+ action: HasActionMethods<H> extends true ? (args: ActionFunctionArgs) => Promise<ActionReturn<H>> : undefined;
62
56
  /** Inferred return type of the GET handler */
63
- GetResponse: WithWrapper<ResponseOf<H, 'GET'>, W>;
57
+ GetResponse: ResponseOf<H, 'GET'>;
64
58
  /** Inferred return type of the POST handler */
65
- PostResponse: WithWrapper<ResponseOf<H, 'POST'>, W>;
59
+ PostResponse: ResponseOf<H, 'POST'>;
66
60
  /** Inferred return type of the PUT handler */
67
- PutResponse: WithWrapper<ResponseOf<H, 'PUT'>, W>;
61
+ PutResponse: ResponseOf<H, 'PUT'>;
68
62
  /** Inferred return type of the PATCH handler */
69
- PatchResponse: WithWrapper<ResponseOf<H, 'PATCH'>, W>;
63
+ PatchResponse: ResponseOf<H, 'PATCH'>;
70
64
  /** Inferred return type of the DELETE handler */
71
- DeleteResponse: WithWrapper<ResponseOf<H, 'DELETE'>, W>;
65
+ DeleteResponse: ResponseOf<H, 'DELETE'>;
72
66
  };
73
67
 
68
+ /** Loader handler — plain function or validated config */
69
+ type LoaderEntry = ((args: LoaderFunctionArgs) => unknown) | HandlerDef;
70
+ /** Action handler — plain function or validated config */
71
+ type ActionEntry = ((args: ActionFunctionArgs) => unknown) | HandlerDef;
74
72
  /**
75
- * Define HTTP method handlers for a React Router v7 route.
76
- * Returns `{ loader, action }` ready to export from a route module.
73
+ * Fluent builder for defining HTTP method handlers.
74
+ * Each method returns a new builder with updated type state.
77
75
  *
78
76
  * @example
79
77
  * ```ts
78
+ * const api = new ApiBuilder()
79
+ * .get(async ({ params }) => ({ user: params.id }))
80
+ * .post(async ({ request }) => ({ created: true }))
81
+ * .build();
82
+ * export const { loader, action } = api;
83
+ * ```
84
+ */
85
+ declare class ApiBuilder<H extends ApiHandlers = object> {
86
+ private handlers;
87
+ constructor(handlers?: ApiHandlers);
88
+ /** Add middleware that runs before every handler */
89
+ middleware(fns: MiddlewareFn[]): ApiBuilder<H>;
90
+ /** Define the GET handler (becomes the loader) */
91
+ get<F extends LoaderEntry>(handler: F): ApiBuilder<H & {
92
+ GET: F;
93
+ }>;
94
+ /** Define the POST handler */
95
+ post<F extends ActionEntry>(handler: F): ApiBuilder<H & {
96
+ POST: F;
97
+ }>;
98
+ /** Define the PUT handler */
99
+ put<F extends ActionEntry>(handler: F): ApiBuilder<H & {
100
+ PUT: F;
101
+ }>;
102
+ /** Define the PATCH handler */
103
+ patch<F extends ActionEntry>(handler: F): ApiBuilder<H & {
104
+ PATCH: F;
105
+ }>;
106
+ /** Define the DELETE handler */
107
+ delete<F extends ActionEntry>(handler: F): ApiBuilder<H & {
108
+ DELETE: F;
109
+ }>;
110
+ /** Build and return { loader, action } with response type helpers */
111
+ build(): ApiExports<H>;
112
+ /** @internal — set by define-api.ts to wire up the build logic */
113
+ static _buildFn: (handlers: ApiHandlers) => unknown;
114
+ }
115
+
116
+ /**
117
+ * Define HTTP method handlers for a React Router v7 route.
118
+ *
119
+ * **Object style** — pass all handlers at once:
120
+ * ```ts
80
121
  * export const { loader, action } = defineApi({
81
122
  * GET: async ({ params }) => ({ user: params.id }),
82
- * POST: async ({ request }) => {
83
- * const body = await request.formData();
84
- * return { created: true };
85
- * },
123
+ * POST: async ({ request }) => ({ created: true }),
86
124
  * });
87
125
  * ```
88
126
  *
89
- * @example With handler wrapper
127
+ * **Builder style** chain methods fluently:
90
128
  * ```ts
91
- * export const { loader, action } = defineApi({
92
- * GET: async () => ({ data: 'ok' }),
93
- * }, {
94
- * handler: loaderActionHandler,
95
- * });
129
+ * export const { loader, action } = defineApi()
130
+ * .get(async ({ params }) => ({ user: params.id }))
131
+ * .post(async ({ request }) => ({ created: true }))
132
+ * .build();
96
133
  * ```
97
134
  */
98
- declare function defineApi<const H extends ApiHandlers, W = never>(handlers: H, options?: DefineApiOptions<W>): ApiExports<H, W>;
135
+ declare function defineApi(): ApiBuilder;
136
+ declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
99
137
 
100
- export { type ApiHandlers, type DefineApiOptions, type HandlerWrapper, type HttpMethod, type MethodHandler, defineApi };
138
+ export { ApiBuilder, type ApiHandlers, type HandlerArgs, type HandlerDef, type HttpMethod, type MethodHandler, type MiddlewareFn, type Schema, defineApi };
package/dist/index.js CHANGED
@@ -1,3 +1,63 @@
1
+ // src/api-builder.ts
2
+ var ApiBuilder = class _ApiBuilder {
3
+ handlers;
4
+ constructor(handlers = {}) {
5
+ this.handlers = handlers;
6
+ }
7
+ /** Add middleware that runs before every handler */
8
+ middleware(fns) {
9
+ return new _ApiBuilder({
10
+ ...this.handlers,
11
+ middleware: [...this.handlers.middleware ?? [], ...fns]
12
+ });
13
+ }
14
+ /** Define the GET handler (becomes the loader) */
15
+ get(handler) {
16
+ return new _ApiBuilder({ ...this.handlers, GET: handler });
17
+ }
18
+ /** Define the POST handler */
19
+ post(handler) {
20
+ return new _ApiBuilder({ ...this.handlers, POST: handler });
21
+ }
22
+ /** Define the PUT handler */
23
+ put(handler) {
24
+ return new _ApiBuilder({ ...this.handlers, PUT: handler });
25
+ }
26
+ /** Define the PATCH handler */
27
+ patch(handler) {
28
+ return new _ApiBuilder({ ...this.handlers, PATCH: handler });
29
+ }
30
+ /** Define the DELETE handler */
31
+ delete(handler) {
32
+ return new _ApiBuilder({
33
+ ...this.handlers,
34
+ DELETE: handler
35
+ });
36
+ }
37
+ /** Build and return { loader, action } with response type helpers */
38
+ build() {
39
+ return _ApiBuilder._buildFn(this.handlers);
40
+ }
41
+ /** @internal — set by define-api.ts to wire up the build logic */
42
+ static _buildFn;
43
+ };
44
+
45
+ // src/parse-body.ts
46
+ async function parseBody(request) {
47
+ const contentType = request.headers.get("content-type") ?? "";
48
+ if (contentType.includes("application/json")) {
49
+ return request.json();
50
+ }
51
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
52
+ const formData = await request.formData();
53
+ return Object.fromEntries(formData);
54
+ }
55
+ if (contentType.includes("text/")) {
56
+ return request.text();
57
+ }
58
+ throw new Response("Unsupported Media Type", { status: 415 });
59
+ }
60
+
1
61
  // src/define-api.ts
2
62
  var ACTION_METHODS = [
3
63
  "POST",
@@ -5,23 +65,84 @@ var ACTION_METHODS = [
5
65
  "PATCH",
6
66
  "DELETE"
7
67
  ];
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;
68
+ function isHandlerDef(entry) {
69
+ return typeof entry === "object" && "handler" in entry;
70
+ }
71
+ function runMiddleware(middleware, args, handler) {
72
+ let index = 0;
73
+ const next = async () => {
74
+ if (index < middleware.length) {
75
+ return middleware[index++](args, next);
76
+ }
77
+ return handler(args);
78
+ };
79
+ return next();
80
+ }
81
+ function wrapWithValidation(entry) {
82
+ if (!isHandlerDef(entry)) return entry;
83
+ const { params: paramsSchema, body: bodySchema, handler } = entry;
84
+ return async (args) => {
85
+ let validatedParams = args.params;
86
+ if (paramsSchema) {
87
+ try {
88
+ validatedParams = paramsSchema.parse(args.params);
89
+ } catch (error) {
90
+ throw new Response(
91
+ JSON.stringify({ error: "Validation failed", details: error }),
92
+ { status: 400, headers: { "content-type": "application/json" } }
93
+ );
94
+ }
95
+ }
96
+ let body;
97
+ if (bodySchema) {
98
+ const raw = await parseBody(args.request);
99
+ try {
100
+ body = bodySchema.parse(raw);
101
+ } catch (error) {
102
+ throw new Response(
103
+ JSON.stringify({ error: "Validation failed", details: error }),
104
+ { status: 400, headers: { "content-type": "application/json" } }
105
+ );
106
+ }
107
+ }
108
+ const handlerArgs = {
109
+ ...args,
110
+ params: validatedParams,
111
+ ...bodySchema ? { body } : {}
112
+ };
113
+ return handler(handlerArgs);
114
+ };
115
+ }
116
+ function buildExports(handlers) {
117
+ const mw = handlers.middleware ?? [];
118
+ const getEntry = handlers.GET;
119
+ const getHandler = getEntry ? wrapWithValidation(getEntry) : void 0;
120
+ const loader = getHandler ? mw.length > 0 ? (args) => runMiddleware(mw, args, getHandler) : getHandler : void 0;
12
121
  const hasActionHandlers = ACTION_METHODS.some(
13
122
  (m) => handlers[m] != null
14
123
  );
15
- const action = hasActionHandlers ? wrap(async (args) => {
124
+ const action = hasActionHandlers ? async (args) => {
16
125
  const method = args.request.method.toUpperCase();
17
- const handler = handlers[method];
18
- if (typeof handler === "function") {
19
- return handler(args);
126
+ const entry = handlers[method];
127
+ if (entry == null) {
128
+ throw new Response("Method Not Allowed", { status: 405 });
20
129
  }
21
- throw new Response("Method Not Allowed", { status: 405 });
22
- }) : void 0;
130
+ const handler = wrapWithValidation(entry);
131
+ if (mw.length > 0) {
132
+ return runMiddleware(mw, args, handler);
133
+ }
134
+ return handler(args);
135
+ } : void 0;
23
136
  return { loader, action };
24
137
  }
138
+ ApiBuilder._buildFn = buildExports;
139
+ function defineApi(handlers) {
140
+ if (handlers === void 0) {
141
+ return new ApiBuilder();
142
+ }
143
+ return buildExports(handlers);
144
+ }
25
145
  export {
146
+ ApiBuilder,
26
147
  defineApi
27
148
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-router-define-api",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Define HTTP method handlers for React Router v7 routes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",