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 +125 -0
- package/dist/index.cjs +130 -6
- package/dist/index.d.cts +86 -14
- package/dist/index.d.ts +86 -14
- package/dist/index.js +129 -6
- package/package.json +1 -1
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
|
|
35
|
-
|
|
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
|
|
42
|
-
if (
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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> :
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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> :
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
|
9
|
-
|
|
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
|
|
16
|
-
if (
|
|
17
|
-
|
|
126
|
+
const entry = handlers[method];
|
|
127
|
+
if (entry == null) {
|
|
128
|
+
throw new Response("Method Not Allowed", { status: 405 });
|
|
18
129
|
}
|
|
19
|
-
|
|
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
|
};
|