react-router-define-api 0.1.5 → 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,12 +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
 
90
+ ### Middleware
91
+
92
+ Add middleware that runs before every handler. Middleware uses the onion model — call `next()` to proceed, or return early to short-circuit.
93
+
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
+
110
+ export const { loader, action } = defineApi({
111
+ middleware: [auth, logger],
112
+ GET: async ({ params }) => ({ user: params.id }),
113
+ POST: async ({ request }) => {
114
+ const body = await request.formData();
115
+ return { created: true };
116
+ },
117
+ });
118
+ ```
119
+
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:
129
+
130
+ ```ts
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
+ });
154
+ ```
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
+
40
165
  ### Response type helpers
41
166
 
42
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,22 +92,85 @@ var ACTION_METHODS = [
31
92
  "PATCH",
32
93
  "DELETE"
33
94
  ];
34
- function defineApi(handlers) {
35
- const loader = 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;
36
148
  const hasActionHandlers = ACTION_METHODS.some(
37
149
  (m) => handlers[m] != null
38
150
  );
39
151
  const action = hasActionHandlers ? async (args) => {
40
152
  const method = args.request.method.toUpperCase();
41
- const handler = handlers[method];
42
- if (typeof handler === "function") {
43
- return handler(args);
153
+ const entry = handlers[method];
154
+ if (entry == null) {
155
+ throw new Response("Method Not Allowed", { status: 405 });
156
+ }
157
+ const handler = wrapWithValidation(entry);
158
+ if (mw.length > 0) {
159
+ return runMiddleware(mw, args, handler);
44
160
  }
45
- throw new Response("Method Not Allowed", { status: 405 });
161
+ return handler(args);
46
162
  } : void 0;
47
163
  return { loader, action };
48
164
  }
165
+ ApiBuilder._buildFn = buildExports;
166
+ function defineApi(handlers) {
167
+ if (handlers === void 0) {
168
+ return new ApiBuilder();
169
+ }
170
+ return buildExports(handlers);
171
+ }
49
172
  // Annotate the CommonJS export names for ESM import in node:
50
173
  0 && (module.exports = {
174
+ ApiBuilder,
51
175
  defineApi
52
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 */
@@ -46,21 +65,74 @@ type ApiExports<H extends ApiHandlers> = {
46
65
  DeleteResponse: ResponseOf<H, 'DELETE'>;
47
66
  };
48
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;
49
72
  /**
50
- * Define HTTP method handlers for a React Router v7 route.
51
- * 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.
52
75
  *
53
76
  * @example
54
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
55
121
  * export const { loader, action } = defineApi({
56
122
  * GET: async ({ params }) => ({ user: params.id }),
57
- * POST: async ({ request }) => {
58
- * const body = await request.formData();
59
- * return { created: true };
60
- * },
123
+ * POST: async ({ request }) => ({ created: true }),
61
124
  * });
62
125
  * ```
126
+ *
127
+ * **Builder style** — chain methods fluently:
128
+ * ```ts
129
+ * export const { loader, action } = defineApi()
130
+ * .get(async ({ params }) => ({ user: params.id }))
131
+ * .post(async ({ request }) => ({ created: true }))
132
+ * .build();
133
+ * ```
63
134
  */
135
+ declare function defineApi(): ApiBuilder;
64
136
  declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
65
137
 
66
- export { type ApiHandlers, 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 */
@@ -46,21 +65,74 @@ type ApiExports<H extends ApiHandlers> = {
46
65
  DeleteResponse: ResponseOf<H, 'DELETE'>;
47
66
  };
48
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;
49
72
  /**
50
- * Define HTTP method handlers for a React Router v7 route.
51
- * 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.
52
75
  *
53
76
  * @example
54
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
55
121
  * export const { loader, action } = defineApi({
56
122
  * GET: async ({ params }) => ({ user: params.id }),
57
- * POST: async ({ request }) => {
58
- * const body = await request.formData();
59
- * return { created: true };
60
- * },
123
+ * POST: async ({ request }) => ({ created: true }),
61
124
  * });
62
125
  * ```
126
+ *
127
+ * **Builder style** — chain methods fluently:
128
+ * ```ts
129
+ * export const { loader, action } = defineApi()
130
+ * .get(async ({ params }) => ({ user: params.id }))
131
+ * .post(async ({ request }) => ({ created: true }))
132
+ * .build();
133
+ * ```
63
134
  */
135
+ declare function defineApi(): ApiBuilder;
64
136
  declare function defineApi<const H extends ApiHandlers>(handlers: H): ApiExports<H>;
65
137
 
66
- export { type ApiHandlers, 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,21 +65,84 @@ var ACTION_METHODS = [
5
65
  "PATCH",
6
66
  "DELETE"
7
67
  ];
8
- function defineApi(handlers) {
9
- const loader = 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;
10
121
  const hasActionHandlers = ACTION_METHODS.some(
11
122
  (m) => handlers[m] != null
12
123
  );
13
124
  const action = hasActionHandlers ? async (args) => {
14
125
  const method = args.request.method.toUpperCase();
15
- const handler = handlers[method];
16
- if (typeof handler === "function") {
17
- return handler(args);
126
+ const entry = handlers[method];
127
+ if (entry == null) {
128
+ throw new Response("Method Not Allowed", { status: 405 });
18
129
  }
19
- throw new Response("Method Not Allowed", { status: 405 });
130
+ const handler = wrapWithValidation(entry);
131
+ if (mw.length > 0) {
132
+ return runMiddleware(mw, args, handler);
133
+ }
134
+ return handler(args);
20
135
  } : void 0;
21
136
  return { loader, action };
22
137
  }
138
+ ApiBuilder._buildFn = buildExports;
139
+ function defineApi(handlers) {
140
+ if (handlers === void 0) {
141
+ return new ApiBuilder();
142
+ }
143
+ return buildExports(handlers);
144
+ }
23
145
  export {
146
+ ApiBuilder,
24
147
  defineApi
25
148
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-router-define-api",
3
- "version": "0.1.5",
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",