@routar/core 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,4 +8,4 @@ Core package for [routar](https://github.com/kbmin1129/routar) — endpoint defi
8
8
  npm install @routar/core
9
9
  ```
10
10
 
11
- See the [main README](https://github.com/kbmin1129/routar) for full documentation.
11
+ See the [documentation](https://routar.vercel.app) or the [main README](https://github.com/minr2kb/routar) for full documentation.
package/dist/index.cjs CHANGED
@@ -1,15 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // src/define-router.ts
4
- function defineRouter(prefix, endpoints) {
5
- return { prefix, endpoints };
6
- }
7
-
8
- // src/define-endpoint.ts
9
- function endpoint(spec) {
10
- return spec;
11
- }
12
-
13
3
  // src/utils/path.ts
14
4
  function joinPaths(...segments) {
15
5
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -41,33 +31,49 @@ var ValidationError = class extends Error {
41
31
  };
42
32
 
43
33
  // src/create-api.ts
44
- function createApi(executor, routerOrPrefixOrEndpoints, endpointsArg) {
34
+ function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
45
35
  let prefix;
46
36
  let endpoints;
37
+ let options;
47
38
  if (typeof routerOrPrefixOrEndpoints === "string") {
48
39
  prefix = routerOrPrefixOrEndpoints;
49
- if (!endpointsArg) throw new Error("endpoints is required when prefix is provided");
50
- endpoints = endpointsArg;
40
+ if (!endpointsArgOrOptions)
41
+ throw new Error("endpoints is required when prefix is provided");
42
+ endpoints = endpointsArgOrOptions;
43
+ options = optionsArg;
51
44
  } else if ("prefix" in routerOrPrefixOrEndpoints && "endpoints" in routerOrPrefixOrEndpoints) {
52
45
  prefix = routerOrPrefixOrEndpoints.prefix;
53
46
  endpoints = routerOrPrefixOrEndpoints.endpoints;
47
+ options = endpointsArgOrOptions;
54
48
  } else {
55
49
  prefix = "";
56
50
  endpoints = routerOrPrefixOrEndpoints;
51
+ options = endpointsArgOrOptions;
57
52
  }
58
- return buildClient(executor, prefix, endpoints);
53
+ return buildClient(executor, prefix, endpoints, options);
59
54
  }
60
- function buildClient(executor, prefix, endpoints) {
55
+ function shouldValidate(options, kind) {
56
+ const v = options?.validate;
57
+ if (v === void 0 || v === true) return true;
58
+ if (v === false) return false;
59
+ return v[kind] ?? true;
60
+ }
61
+ function buildClient(executor, prefix, endpoints, options) {
61
62
  const client = {};
62
63
  for (const [key, entry] of Object.entries(endpoints)) {
63
64
  if ("prefix" in entry && "endpoints" in entry) {
64
65
  const nested = entry;
65
- client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);
66
+ client[key] = buildClient(
67
+ executor,
68
+ joinPaths(prefix, nested.prefix),
69
+ nested.endpoints,
70
+ options
71
+ );
66
72
  } else {
67
73
  const spec = entry;
68
74
  client[key] = async (params = {}, signal) => {
69
75
  let validatedParams = params;
70
- if (spec.request) {
76
+ if (spec.request && shouldValidate(options, "request")) {
71
77
  try {
72
78
  validatedParams = spec.request.parse(params);
73
79
  } catch (err) {
@@ -85,16 +91,20 @@ function buildClient(executor, prefix, endpoints) {
85
91
  body: validatedParams?.body,
86
92
  signal
87
93
  });
88
- let validated;
89
- try {
90
- validated = spec.response.parse(raw);
91
- } catch (err) {
92
- throw new ValidationError("Response validation failed", err);
94
+ let result;
95
+ if (shouldValidate(options, "response")) {
96
+ try {
97
+ result = spec.response.parse(raw);
98
+ } catch (err) {
99
+ throw new ValidationError("Response validation failed", err);
100
+ }
101
+ } else {
102
+ result = raw;
93
103
  }
94
104
  if (spec.adapter) {
95
- return spec.adapter(validated);
105
+ return spec.adapter(result);
96
106
  }
97
- return validated;
107
+ return result;
98
108
  };
99
109
  }
100
110
  }
@@ -103,12 +113,24 @@ function buildClient(executor, prefix, endpoints) {
103
113
 
104
114
  // src/create-executor.ts
105
115
  function createExecutor(execute, middlewares = []) {
106
- const chain = middlewares.reduceRight(
107
- (next, mw) => (opts) => mw(opts, next),
108
- execute
109
- );
116
+ const chain = middlewares.reduceRight((next, mw) => (opts) => mw(opts, next), execute);
110
117
  return { execute: chain };
111
118
  }
119
+ function dispatchExecutor(resolver) {
120
+ return {
121
+ execute: (opts) => resolver(opts).execute(opts)
122
+ };
123
+ }
124
+
125
+ // src/define-endpoint.ts
126
+ function endpoint(spec) {
127
+ return spec;
128
+ }
129
+
130
+ // src/define-router.ts
131
+ function defineRouter(prefix, endpoints) {
132
+ return { prefix, endpoints };
133
+ }
112
134
 
113
135
  // src/middleware.ts
114
136
  function defineMiddleware(fn) {
@@ -145,13 +167,19 @@ function withLogger(options) {
145
167
  const log = options?.log ?? ((msg, data) => console.log(msg, data));
146
168
  return defineMiddleware(async (opts, next) => {
147
169
  const start = Date.now();
148
- log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });
170
+ log(`[routar] ${opts.method} ${opts.url}`, {
171
+ params: opts.params,
172
+ body: opts.body
173
+ });
149
174
  try {
150
175
  const result = await next(opts);
151
176
  log(`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - start}ms`);
152
177
  return result;
153
178
  } catch (err) {
154
- log(`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`, err);
179
+ log(
180
+ `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`,
181
+ err
182
+ );
155
183
  throw err;
156
184
  }
157
185
  });
@@ -189,6 +217,7 @@ exports.createApi = createApi;
189
217
  exports.createExecutor = createExecutor;
190
218
  exports.defineMiddleware = defineMiddleware;
191
219
  exports.defineRouter = defineRouter;
220
+ exports.dispatchExecutor = dispatchExecutor;
192
221
  exports.endpoint = endpoint;
193
222
  exports.joinPaths = joinPaths;
194
223
  exports.resolvePath = resolvePath;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/define-router.ts","../src/define-endpoint.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { RouterDef, RouterEndpoints } from './types.js';\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from './types.js';\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> =\n [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","export function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter(s => s !== '')\n .join('/')\n .replace(/\\/+/g, '/');\n return joined.endsWith('/') && joined.length > 1\n ? joined.slice(0, -1)\n : joined || '/';\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null) throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'ValidationError';\n if (cause !== undefined) {\n Object.defineProperty(this, 'cause', {\n value: cause,\n writable: true,\n enumerable: true,\n });\n }\n }\n}\n","import type {\n Executor,\n RouterDef,\n RouterEndpoints,\n EndpointSpec,\n InferResponse,\n RequestShape,\n} from './types.js';\nimport { joinPaths, resolvePath } from './utils/path.js';\nimport { ValidationError } from './utils/validate.js';\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (\n params: TSpec['request'] extends { parse: (data: unknown) => infer R } ? R : RequestShape,\n signal?: AbortSignal,\n) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints>\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints: RouterDef<any> | RouterEndpoints | string,\n endpointsArg?: RouterEndpoints,\n): Record<string, any> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n\n if (typeof routerOrPrefixOrEndpoints === 'string') {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArg) throw new Error('endpoints is required when prefix is provided');\n endpoints = endpointsArg;\n } else if (\n 'prefix' in routerOrPrefixOrEndpoints &&\n 'endpoints' in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<any>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<any>).endpoints;\n } else {\n prefix = '';\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n }\n\n return buildClient(executor, prefix, endpoints);\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n): Record<string, any> {\n const client: Record<string, any> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n if ('prefix' in entry && 'endpoints' in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<any>;\n client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);\n } else {\n // Leaf EndpointSpec\n const spec = entry as EndpointSpec<any, any, any>;\n client[key] = async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError('Request validation failed', err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let validated: unknown;\n try {\n validated = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError('Response validation failed', err);\n }\n\n if (spec.adapter) {\n return spec.adapter(validated as any);\n }\n return validated;\n };\n }\n }\n\n return client;\n}\n","import type { Executor, ExecuteOptions, ExecutorMiddleware } from './types.js';\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n return { execute: chain };\n}\n","import type { ExecutorMiddleware } from './types.js';\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n const signal = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : controller.signal;\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(`[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`, err);\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","export function serializeParams(params: Record<string, unknown>): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}
1
+ {"version":3,"sources":["../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/define-endpoint.ts","../src/define-router.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null) throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: true,\n enumerable: true,\n });\n }\n }\n}\n","import type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (\n params: TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? R\n : RequestShape,\n signal?: AbortSignal,\n) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n *\n * // Skip response validation in production\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n let options: CreateApiOptions | undefined;\n\n if (typeof routerOrPrefixOrEndpoints === \"string\") {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArgOrOptions)\n throw new Error(\"endpoints is required when prefix is provided\");\n endpoints = endpointsArgOrOptions as RouterEndpoints;\n options = optionsArg;\n } else if (\n \"prefix\" in routerOrPrefixOrEndpoints &&\n \"endpoints\" in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).endpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n } else {\n prefix = \"\";\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n }\n\n return buildClient(executor, prefix, endpoints, options);\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n if (\"prefix\" in entry && \"endpoints\" in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<RouterEndpoints>;\n client[key] = buildClient(\n executor,\n joinPaths(prefix, nested.prefix),\n nested.endpoints,\n options,\n );\n } else {\n // Leaf EndpointSpec\n const spec = entry as EndpointSpec<any, any, any>;\n client[key] = async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: unknown;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n if (spec.adapter) {\n return spec.adapter(result);\n }\n return result;\n };\n }\n }\n\n return client;\n}\n","import type { ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<\n (options: ExecuteOptions) => Promise<unknown>\n >((next, mw) => (opts) => mw(opts, next), execute);\n return { execute: chain };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * Use this to unify SSR and CSR behind a single API client — the resolver\n * picks the right executor per request, so `createApi` is called once and\n * works in both environments without duplicate `*ServerApi` instances.\n *\n * The resolver receives the full {@link ExecuteOptions} so it can branch on\n * environment, URL prefix, auth context, or any runtime condition.\n *\n * @param resolver - Called on every request; returns the executor to delegate to.\n *\n * @example\n * ```ts\n * // SSR vs CSR — pick transport based on environment\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n *\n * // Route by URL prefix — internal routes use a different transport\n * const apiExecutor = dispatchExecutor((opts) =>\n * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<TResponse extends Validator<unknown>, TOut>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<TResponse extends Validator<unknown>>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","import type { ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n const signal = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : controller.signal;\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`,\n err,\n );\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
1
+ type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
2
2
  /** Options passed to {@link Executor.execute} on every HTTP call. */
3
3
  interface ExecuteOptions {
4
4
  method: HttpMethod;
@@ -82,7 +82,7 @@ interface EndpointSpec<TRequest extends RequestShape = RequestShape, TResponse e
82
82
  * - With `adapter`: returns the adapter's output type.
83
83
  * - Without `adapter`: returns `ValidatorOutput<TResponse>`.
84
84
  */
85
- type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec['adapter'] extends (raw: any) => infer R ? R : ValidatorOutput<TSpec['response']>;
85
+ type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec["adapter"] extends (raw: any) => infer R ? R : ValidatorOutput<TSpec["response"]>;
86
86
  /**
87
87
  * A single entry inside a {@link RouterEndpoints} map.
88
88
  * Either a leaf endpoint spec or a nested {@link RouterDef}.
@@ -97,53 +97,156 @@ interface RouterDef<TEndpoints extends RouterEndpoints = RouterEndpoints> {
97
97
  }
98
98
  /**
99
99
  * Extracts request/response types from a typed API client for use in query
100
- * hooks or mutation handlers.
100
+ * hooks or mutation handlers. Supports nested router clients recursively.
101
101
  *
102
102
  * @example
103
103
  * ```ts
104
104
  * export type TodoApiTypes = ApiTypes<typeof todoApi>;
105
105
  * type CreateRequest = TodoApiTypes['create']['request'];
106
106
  * type CreateResponse = TodoApiTypes['create']['response'];
107
+ *
108
+ * // Nested router: api.users.todos.getList
109
+ * type NestedTypes = ApiTypes<typeof api>;
110
+ * type ListReq = NestedTypes['users']['todos']['getList']['request'];
107
111
  * ```
108
112
  */
109
113
  type ApiTypes<TApi> = {
110
114
  [K in keyof TApi]: TApi[K] extends (...args: any[]) => Promise<infer R> ? {
111
115
  request: Parameters<TApi[K]>[0];
112
116
  response: R;
113
- } : never;
117
+ } : TApi[K] extends object ? ApiTypes<TApi[K]> : never;
114
118
  };
119
+ /**
120
+ * Options for {@link createApi}.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * // Disable all validation in production
125
+ * createApi(executor, router, { validate: process.env.NODE_ENV !== 'production' });
126
+ *
127
+ * // Keep request validation (catch call-site bugs), skip response in prod
128
+ * createApi(executor, router, { validate: { request: true, response: false } });
129
+ * ```
130
+ */
131
+ interface CreateApiOptions {
132
+ /**
133
+ * Controls whether request and response schemas are run at call time.
134
+ *
135
+ * - `true` (default) — validate both request and response.
136
+ * - `false` — skip both; raw params and raw response pass through.
137
+ * - `{ request?, response? }` — enable/disable each independently.
138
+ */
139
+ validate?: boolean | {
140
+ request?: boolean;
141
+ response?: boolean;
142
+ };
143
+ }
115
144
 
145
+ /** Callable type for a single endpoint on the generated API client. */
146
+ type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec["request"] extends {
147
+ parse: (data: unknown) => infer R;
148
+ } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
116
149
  /**
117
- * Groups a set of endpoint specs (and optional nested routers) under a shared
118
- * URL prefix.
150
+ * Fully-typed API client produced by {@link createApi}.
151
+ * Nested {@link RouterDef} entries become nested sub-client objects.
152
+ */
153
+ type ApiClient<TEndpoints extends RouterEndpoints> = {
154
+ [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
155
+ };
156
+ /**
157
+ * Builds a fully-typed API client from an {@link Executor} and a router
158
+ * (or bare endpoint map).
119
159
  *
120
- * The returned {@link RouterDef} can be passed directly to {@link createApi}
121
- * to produce a fully-typed API client. Nesting another {@link RouterDef} as a
122
- * value creates a sub-client whose prefix is the concatenation of both prefixes.
160
+ * Three call signatures are supported:
161
+ * - `createApi(executor, router)` preferred; pass the result of {@link defineRouter}.
162
+ * - `createApi(executor, prefix, endpoints)` inline router without {@link defineRouter}.
163
+ * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
123
164
  *
124
- * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).
125
- * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.
165
+ * Each key in `endpoints` becomes a typed async function on the returned client.
166
+ * The function validates the request with `spec.request.parse` (if present),
167
+ * resolves path parameters, calls the executor, validates the response with
168
+ * `spec.response.parse`, and applies `spec.adapter` (if present).
169
+ *
170
+ * @param executor - Transport to use for every HTTP call.
171
+ * @param router - A {@link RouterDef} produced by {@link defineRouter}.
172
+ * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).
126
173
  *
127
174
  * @example
128
175
  * ```ts
129
- * // Flat router
130
- * export const todoRouter = defineRouter('/todos', {
131
- * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
132
- * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
133
- * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),
134
- * });
176
+ * const todoApi = createApi(executor, todoRouter);
177
+ * const todos = await todoApi.getList({});
178
+ * const todo = await todoApi.getDetail({ path: { id: 1 } });
135
179
  *
136
- * // Nested router api.users.todos.getList() resolves to GET /users/todos/
137
- * export const userRouter = defineRouter('/users', {
138
- * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),
139
- * todos: defineRouter('/todos', {
140
- * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
141
- * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
142
- * }),
180
+ * // Skip response validation in production
181
+ * const prodApi = createApi(executor, todoRouter, {
182
+ * validate: { request: true, response: process.env.NODE_ENV !== 'production' },
143
183
  * });
144
184
  * ```
145
185
  */
146
- declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
186
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>, options?: CreateApiOptions): ApiClient<TEndpoints>;
187
+ /**
188
+ * @param executor - Transport to use for every HTTP call.
189
+ * @param prefix - URL prefix prepended to every endpoint path.
190
+ * @param endpoints - Record of named endpoint specs.
191
+ * @param options - Optional settings.
192
+ */
193
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
194
+ /**
195
+ * @param executor - Transport to use for every HTTP call.
196
+ * @param endpoints - Record of named endpoint specs (no URL prefix).
197
+ * @param options - Optional settings.
198
+ */
199
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
200
+
201
+ /**
202
+ * Creates an {@link Executor} by wrapping a transport function with an
203
+ * optional middleware chain.
204
+ *
205
+ * Middlewares are applied in declaration order — the first middleware is the
206
+ * outermost wrapper and runs first on each request.
207
+ *
208
+ * @param execute - The underlying transport function (fetch, axios, etc.).
209
+ * @param middlewares - Ordered list of middlewares to apply.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const executor = createExecutor(
214
+ * async ({ method, url, body }) => {
215
+ * const res = await fetch(url, { method, body: JSON.stringify(body) });
216
+ * return res.json();
217
+ * },
218
+ * [withTimeout(5000), withRetry(3), withLogger()],
219
+ * );
220
+ * ```
221
+ */
222
+ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
223
+ /**
224
+ * Creates an {@link Executor} that selects the underlying transport at
225
+ * request time based on the result of `resolver`.
226
+ *
227
+ * Use this to unify SSR and CSR behind a single API client — the resolver
228
+ * picks the right executor per request, so `createApi` is called once and
229
+ * works in both environments without duplicate `*ServerApi` instances.
230
+ *
231
+ * The resolver receives the full {@link ExecuteOptions} so it can branch on
232
+ * environment, URL prefix, auth context, or any runtime condition.
233
+ *
234
+ * @param resolver - Called on every request; returns the executor to delegate to.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * // SSR vs CSR — pick transport based on environment
239
+ * const apiExecutor = dispatchExecutor(() =>
240
+ * typeof window === 'undefined' ? serverExecutor : clientExecutor,
241
+ * );
242
+ *
243
+ * // Route by URL prefix — internal routes use a different transport
244
+ * const apiExecutor = dispatchExecutor((opts) =>
245
+ * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
246
+ * );
247
+ * ```
248
+ */
249
+ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
147
250
 
148
251
  /**
149
252
  * Extracts `:param` segment names from a path template string as a union of
@@ -159,9 +262,7 @@ type PathParams<TPath extends string> = TPath extends `${string}:${infer Param}/
159
262
  * When `TPath` contains dynamic segments (`:param`), requires `request.path`
160
263
  * to include all extracted param names. No constraint for static paths.
161
264
  */
162
- type PathConstraint<TPath extends string> = [
163
- PathParams<TPath>
164
- ] extends [never] ? {} : {
265
+ type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never] ? {} : {
165
266
  path: Record<PathParams<TPath>, unknown>;
166
267
  };
167
268
  /**
@@ -242,76 +343,37 @@ declare function endpoint<TResponse extends Validator<unknown>>(spec: {
242
343
  response: TResponse;
243
344
  };
244
345
 
245
- /** Callable type for a single endpoint on the generated API client. */
246
- type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec['request'] extends {
247
- parse: (data: unknown) => infer R;
248
- } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
249
346
  /**
250
- * Fully-typed API client produced by {@link createApi}.
251
- * Nested {@link RouterDef} entries become nested sub-client objects.
252
- */
253
- type ApiClient<TEndpoints extends RouterEndpoints> = {
254
- [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
255
- };
256
- /**
257
- * Builds a fully-typed API client from an {@link Executor} and a router
258
- * (or bare endpoint map).
259
- *
260
- * Three call signatures are supported:
261
- * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.
262
- * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.
263
- * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
347
+ * Groups a set of endpoint specs (and optional nested routers) under a shared
348
+ * URL prefix.
264
349
  *
265
- * Each key in `endpoints` becomes a typed async function on the returned client.
266
- * The function validates the request with `spec.request.parse` (if present),
267
- * resolves path parameters, calls the executor, validates the response with
268
- * `spec.response.parse`, and applies `spec.adapter` (if present).
350
+ * The returned {@link RouterDef} can be passed directly to {@link createApi}
351
+ * to produce a fully-typed API client. Nesting another {@link RouterDef} as a
352
+ * value creates a sub-client whose prefix is the concatenation of both prefixes.
269
353
  *
270
- * @param executor - Transport to use for every HTTP call.
271
- * @param router - A {@link RouterDef} produced by {@link defineRouter}.
354
+ * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).
355
+ * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.
272
356
  *
273
357
  * @example
274
358
  * ```ts
275
- * const todoApi = createApi(executor, todoRouter);
276
- * const todos = await todoApi.getList({});
277
- * const todo = await todoApi.getDetail({ path: { id: 1 } });
278
- * ```
279
- */
280
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>): ApiClient<TEndpoints>;
281
- /**
282
- * @param executor - Transport to use for every HTTP call.
283
- * @param prefix - URL prefix prepended to every endpoint path.
284
- * @param endpoints - Record of named endpoint specs.
285
- */
286
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints): ApiClient<TEndpoints>;
287
- /**
288
- * @param executor - Transport to use for every HTTP call.
289
- * @param endpoints - Record of named endpoint specs (no URL prefix).
290
- */
291
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints): ApiClient<TEndpoints>;
292
-
293
- /**
294
- * Creates an {@link Executor} by wrapping a transport function with an
295
- * optional middleware chain.
296
- *
297
- * Middlewares are applied in declaration order — the first middleware is the
298
- * outermost wrapper and runs first on each request.
299
- *
300
- * @param execute - The underlying transport function (fetch, axios, etc.).
301
- * @param middlewares - Ordered list of middlewares to apply.
359
+ * // Flat router
360
+ * export const todoRouter = defineRouter('/todos', {
361
+ * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
362
+ * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
363
+ * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),
364
+ * });
302
365
  *
303
- * @example
304
- * ```ts
305
- * const executor = createExecutor(
306
- * async ({ method, url, body }) => {
307
- * const res = await fetch(url, { method, body: JSON.stringify(body) });
308
- * return res.json();
309
- * },
310
- * [withTimeout(5000), withRetry(3), withLogger()],
311
- * );
366
+ * // Nested router — api.users.todos.getList() resolves to GET /users/todos/
367
+ * export const userRouter = defineRouter('/users', {
368
+ * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),
369
+ * todos: defineRouter('/todos', {
370
+ * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
371
+ * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
372
+ * }),
373
+ * });
312
374
  * ```
313
375
  */
314
- declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
376
+ declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
315
377
 
316
378
  /**
317
379
  * Identity helper that returns the middleware as-is.
@@ -369,14 +431,14 @@ declare function withLogger(options?: {
369
431
  log?: (message: string, data?: unknown) => void;
370
432
  }): ExecutorMiddleware;
371
433
 
434
+ declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
435
+
372
436
  declare function joinPaths(...segments: string[]): string;
373
437
  declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
374
438
 
375
- declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
376
-
377
439
  declare class ValidationError extends Error {
378
440
  readonly cause?: unknown | undefined;
379
441
  constructor(message: string, cause?: unknown | undefined);
380
442
  }
381
443
 
382
- export { type ApiTypes, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
444
+ export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
1
+ type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
2
2
  /** Options passed to {@link Executor.execute} on every HTTP call. */
3
3
  interface ExecuteOptions {
4
4
  method: HttpMethod;
@@ -82,7 +82,7 @@ interface EndpointSpec<TRequest extends RequestShape = RequestShape, TResponse e
82
82
  * - With `adapter`: returns the adapter's output type.
83
83
  * - Without `adapter`: returns `ValidatorOutput<TResponse>`.
84
84
  */
85
- type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec['adapter'] extends (raw: any) => infer R ? R : ValidatorOutput<TSpec['response']>;
85
+ type InferResponse<TSpec extends EndpointSpec<any, any, any>> = TSpec["adapter"] extends (raw: any) => infer R ? R : ValidatorOutput<TSpec["response"]>;
86
86
  /**
87
87
  * A single entry inside a {@link RouterEndpoints} map.
88
88
  * Either a leaf endpoint spec or a nested {@link RouterDef}.
@@ -97,53 +97,156 @@ interface RouterDef<TEndpoints extends RouterEndpoints = RouterEndpoints> {
97
97
  }
98
98
  /**
99
99
  * Extracts request/response types from a typed API client for use in query
100
- * hooks or mutation handlers.
100
+ * hooks or mutation handlers. Supports nested router clients recursively.
101
101
  *
102
102
  * @example
103
103
  * ```ts
104
104
  * export type TodoApiTypes = ApiTypes<typeof todoApi>;
105
105
  * type CreateRequest = TodoApiTypes['create']['request'];
106
106
  * type CreateResponse = TodoApiTypes['create']['response'];
107
+ *
108
+ * // Nested router: api.users.todos.getList
109
+ * type NestedTypes = ApiTypes<typeof api>;
110
+ * type ListReq = NestedTypes['users']['todos']['getList']['request'];
107
111
  * ```
108
112
  */
109
113
  type ApiTypes<TApi> = {
110
114
  [K in keyof TApi]: TApi[K] extends (...args: any[]) => Promise<infer R> ? {
111
115
  request: Parameters<TApi[K]>[0];
112
116
  response: R;
113
- } : never;
117
+ } : TApi[K] extends object ? ApiTypes<TApi[K]> : never;
114
118
  };
119
+ /**
120
+ * Options for {@link createApi}.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * // Disable all validation in production
125
+ * createApi(executor, router, { validate: process.env.NODE_ENV !== 'production' });
126
+ *
127
+ * // Keep request validation (catch call-site bugs), skip response in prod
128
+ * createApi(executor, router, { validate: { request: true, response: false } });
129
+ * ```
130
+ */
131
+ interface CreateApiOptions {
132
+ /**
133
+ * Controls whether request and response schemas are run at call time.
134
+ *
135
+ * - `true` (default) — validate both request and response.
136
+ * - `false` — skip both; raw params and raw response pass through.
137
+ * - `{ request?, response? }` — enable/disable each independently.
138
+ */
139
+ validate?: boolean | {
140
+ request?: boolean;
141
+ response?: boolean;
142
+ };
143
+ }
115
144
 
145
+ /** Callable type for a single endpoint on the generated API client. */
146
+ type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec["request"] extends {
147
+ parse: (data: unknown) => infer R;
148
+ } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
116
149
  /**
117
- * Groups a set of endpoint specs (and optional nested routers) under a shared
118
- * URL prefix.
150
+ * Fully-typed API client produced by {@link createApi}.
151
+ * Nested {@link RouterDef} entries become nested sub-client objects.
152
+ */
153
+ type ApiClient<TEndpoints extends RouterEndpoints> = {
154
+ [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
155
+ };
156
+ /**
157
+ * Builds a fully-typed API client from an {@link Executor} and a router
158
+ * (or bare endpoint map).
119
159
  *
120
- * The returned {@link RouterDef} can be passed directly to {@link createApi}
121
- * to produce a fully-typed API client. Nesting another {@link RouterDef} as a
122
- * value creates a sub-client whose prefix is the concatenation of both prefixes.
160
+ * Three call signatures are supported:
161
+ * - `createApi(executor, router)` preferred; pass the result of {@link defineRouter}.
162
+ * - `createApi(executor, prefix, endpoints)` inline router without {@link defineRouter}.
163
+ * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
123
164
  *
124
- * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).
125
- * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.
165
+ * Each key in `endpoints` becomes a typed async function on the returned client.
166
+ * The function validates the request with `spec.request.parse` (if present),
167
+ * resolves path parameters, calls the executor, validates the response with
168
+ * `spec.response.parse`, and applies `spec.adapter` (if present).
169
+ *
170
+ * @param executor - Transport to use for every HTTP call.
171
+ * @param router - A {@link RouterDef} produced by {@link defineRouter}.
172
+ * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).
126
173
  *
127
174
  * @example
128
175
  * ```ts
129
- * // Flat router
130
- * export const todoRouter = defineRouter('/todos', {
131
- * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
132
- * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
133
- * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),
134
- * });
176
+ * const todoApi = createApi(executor, todoRouter);
177
+ * const todos = await todoApi.getList({});
178
+ * const todo = await todoApi.getDetail({ path: { id: 1 } });
135
179
  *
136
- * // Nested router api.users.todos.getList() resolves to GET /users/todos/
137
- * export const userRouter = defineRouter('/users', {
138
- * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),
139
- * todos: defineRouter('/todos', {
140
- * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
141
- * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
142
- * }),
180
+ * // Skip response validation in production
181
+ * const prodApi = createApi(executor, todoRouter, {
182
+ * validate: { request: true, response: process.env.NODE_ENV !== 'production' },
143
183
  * });
144
184
  * ```
145
185
  */
146
- declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
186
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>, options?: CreateApiOptions): ApiClient<TEndpoints>;
187
+ /**
188
+ * @param executor - Transport to use for every HTTP call.
189
+ * @param prefix - URL prefix prepended to every endpoint path.
190
+ * @param endpoints - Record of named endpoint specs.
191
+ * @param options - Optional settings.
192
+ */
193
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
194
+ /**
195
+ * @param executor - Transport to use for every HTTP call.
196
+ * @param endpoints - Record of named endpoint specs (no URL prefix).
197
+ * @param options - Optional settings.
198
+ */
199
+ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
200
+
201
+ /**
202
+ * Creates an {@link Executor} by wrapping a transport function with an
203
+ * optional middleware chain.
204
+ *
205
+ * Middlewares are applied in declaration order — the first middleware is the
206
+ * outermost wrapper and runs first on each request.
207
+ *
208
+ * @param execute - The underlying transport function (fetch, axios, etc.).
209
+ * @param middlewares - Ordered list of middlewares to apply.
210
+ *
211
+ * @example
212
+ * ```ts
213
+ * const executor = createExecutor(
214
+ * async ({ method, url, body }) => {
215
+ * const res = await fetch(url, { method, body: JSON.stringify(body) });
216
+ * return res.json();
217
+ * },
218
+ * [withTimeout(5000), withRetry(3), withLogger()],
219
+ * );
220
+ * ```
221
+ */
222
+ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
223
+ /**
224
+ * Creates an {@link Executor} that selects the underlying transport at
225
+ * request time based on the result of `resolver`.
226
+ *
227
+ * Use this to unify SSR and CSR behind a single API client — the resolver
228
+ * picks the right executor per request, so `createApi` is called once and
229
+ * works in both environments without duplicate `*ServerApi` instances.
230
+ *
231
+ * The resolver receives the full {@link ExecuteOptions} so it can branch on
232
+ * environment, URL prefix, auth context, or any runtime condition.
233
+ *
234
+ * @param resolver - Called on every request; returns the executor to delegate to.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * // SSR vs CSR — pick transport based on environment
239
+ * const apiExecutor = dispatchExecutor(() =>
240
+ * typeof window === 'undefined' ? serverExecutor : clientExecutor,
241
+ * );
242
+ *
243
+ * // Route by URL prefix — internal routes use a different transport
244
+ * const apiExecutor = dispatchExecutor((opts) =>
245
+ * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
246
+ * );
247
+ * ```
248
+ */
249
+ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
147
250
 
148
251
  /**
149
252
  * Extracts `:param` segment names from a path template string as a union of
@@ -159,9 +262,7 @@ type PathParams<TPath extends string> = TPath extends `${string}:${infer Param}/
159
262
  * When `TPath` contains dynamic segments (`:param`), requires `request.path`
160
263
  * to include all extracted param names. No constraint for static paths.
161
264
  */
162
- type PathConstraint<TPath extends string> = [
163
- PathParams<TPath>
164
- ] extends [never] ? {} : {
265
+ type PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never] ? {} : {
165
266
  path: Record<PathParams<TPath>, unknown>;
166
267
  };
167
268
  /**
@@ -242,76 +343,37 @@ declare function endpoint<TResponse extends Validator<unknown>>(spec: {
242
343
  response: TResponse;
243
344
  };
244
345
 
245
- /** Callable type for a single endpoint on the generated API client. */
246
- type EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (params: TSpec['request'] extends {
247
- parse: (data: unknown) => infer R;
248
- } ? R : RequestShape, signal?: AbortSignal) => Promise<InferResponse<TSpec>>;
249
346
  /**
250
- * Fully-typed API client produced by {@link createApi}.
251
- * Nested {@link RouterDef} entries become nested sub-client objects.
252
- */
253
- type ApiClient<TEndpoints extends RouterEndpoints> = {
254
- [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints> ? ApiClient<TNestedEndpoints> : TEndpoints[K] extends EndpointSpec<any, any, any> ? EndpointFn<TEndpoints[K]> : never;
255
- };
256
- /**
257
- * Builds a fully-typed API client from an {@link Executor} and a router
258
- * (or bare endpoint map).
259
- *
260
- * Three call signatures are supported:
261
- * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.
262
- * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.
263
- * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.
347
+ * Groups a set of endpoint specs (and optional nested routers) under a shared
348
+ * URL prefix.
264
349
  *
265
- * Each key in `endpoints` becomes a typed async function on the returned client.
266
- * The function validates the request with `spec.request.parse` (if present),
267
- * resolves path parameters, calls the executor, validates the response with
268
- * `spec.response.parse`, and applies `spec.adapter` (if present).
350
+ * The returned {@link RouterDef} can be passed directly to {@link createApi}
351
+ * to produce a fully-typed API client. Nesting another {@link RouterDef} as a
352
+ * value creates a sub-client whose prefix is the concatenation of both prefixes.
269
353
  *
270
- * @param executor - Transport to use for every HTTP call.
271
- * @param router - A {@link RouterDef} produced by {@link defineRouter}.
354
+ * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).
355
+ * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.
272
356
  *
273
357
  * @example
274
358
  * ```ts
275
- * const todoApi = createApi(executor, todoRouter);
276
- * const todos = await todoApi.getList({});
277
- * const todo = await todoApi.getDetail({ path: { id: 1 } });
278
- * ```
279
- */
280
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, router: RouterDef<TEndpoints>): ApiClient<TEndpoints>;
281
- /**
282
- * @param executor - Transport to use for every HTTP call.
283
- * @param prefix - URL prefix prepended to every endpoint path.
284
- * @param endpoints - Record of named endpoint specs.
285
- */
286
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, prefix: string, endpoints: TEndpoints): ApiClient<TEndpoints>;
287
- /**
288
- * @param executor - Transport to use for every HTTP call.
289
- * @param endpoints - Record of named endpoint specs (no URL prefix).
290
- */
291
- declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints): ApiClient<TEndpoints>;
292
-
293
- /**
294
- * Creates an {@link Executor} by wrapping a transport function with an
295
- * optional middleware chain.
296
- *
297
- * Middlewares are applied in declaration order — the first middleware is the
298
- * outermost wrapper and runs first on each request.
299
- *
300
- * @param execute - The underlying transport function (fetch, axios, etc.).
301
- * @param middlewares - Ordered list of middlewares to apply.
359
+ * // Flat router
360
+ * export const todoRouter = defineRouter('/todos', {
361
+ * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
362
+ * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
363
+ * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),
364
+ * });
302
365
  *
303
- * @example
304
- * ```ts
305
- * const executor = createExecutor(
306
- * async ({ method, url, body }) => {
307
- * const res = await fetch(url, { method, body: JSON.stringify(body) });
308
- * return res.json();
309
- * },
310
- * [withTimeout(5000), withRetry(3), withLogger()],
311
- * );
366
+ * // Nested router — api.users.todos.getList() resolves to GET /users/todos/
367
+ * export const userRouter = defineRouter('/users', {
368
+ * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),
369
+ * todos: defineRouter('/todos', {
370
+ * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),
371
+ * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),
372
+ * }),
373
+ * });
312
374
  * ```
313
375
  */
314
- declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
376
+ declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
315
377
 
316
378
  /**
317
379
  * Identity helper that returns the middleware as-is.
@@ -369,14 +431,14 @@ declare function withLogger(options?: {
369
431
  log?: (message: string, data?: unknown) => void;
370
432
  }): ExecutorMiddleware;
371
433
 
434
+ declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
435
+
372
436
  declare function joinPaths(...segments: string[]): string;
373
437
  declare function resolvePath(pathTemplate: string, params?: Record<string, unknown>): string;
374
438
 
375
- declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
376
-
377
439
  declare class ValidationError extends Error {
378
440
  readonly cause?: unknown | undefined;
379
441
  constructor(message: string, cause?: unknown | undefined);
380
442
  }
381
443
 
382
- export { type ApiTypes, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
444
+ export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
package/dist/index.js CHANGED
@@ -1,13 +1,3 @@
1
- // src/define-router.ts
2
- function defineRouter(prefix, endpoints) {
3
- return { prefix, endpoints };
4
- }
5
-
6
- // src/define-endpoint.ts
7
- function endpoint(spec) {
8
- return spec;
9
- }
10
-
11
1
  // src/utils/path.ts
12
2
  function joinPaths(...segments) {
13
3
  const joined = segments.filter((s) => s !== "").join("/").replace(/\/+/g, "/");
@@ -39,33 +29,49 @@ var ValidationError = class extends Error {
39
29
  };
40
30
 
41
31
  // src/create-api.ts
42
- function createApi(executor, routerOrPrefixOrEndpoints, endpointsArg) {
32
+ function createApi(executor, routerOrPrefixOrEndpoints, endpointsArgOrOptions, optionsArg) {
43
33
  let prefix;
44
34
  let endpoints;
35
+ let options;
45
36
  if (typeof routerOrPrefixOrEndpoints === "string") {
46
37
  prefix = routerOrPrefixOrEndpoints;
47
- if (!endpointsArg) throw new Error("endpoints is required when prefix is provided");
48
- endpoints = endpointsArg;
38
+ if (!endpointsArgOrOptions)
39
+ throw new Error("endpoints is required when prefix is provided");
40
+ endpoints = endpointsArgOrOptions;
41
+ options = optionsArg;
49
42
  } else if ("prefix" in routerOrPrefixOrEndpoints && "endpoints" in routerOrPrefixOrEndpoints) {
50
43
  prefix = routerOrPrefixOrEndpoints.prefix;
51
44
  endpoints = routerOrPrefixOrEndpoints.endpoints;
45
+ options = endpointsArgOrOptions;
52
46
  } else {
53
47
  prefix = "";
54
48
  endpoints = routerOrPrefixOrEndpoints;
49
+ options = endpointsArgOrOptions;
55
50
  }
56
- return buildClient(executor, prefix, endpoints);
51
+ return buildClient(executor, prefix, endpoints, options);
57
52
  }
58
- function buildClient(executor, prefix, endpoints) {
53
+ function shouldValidate(options, kind) {
54
+ const v = options?.validate;
55
+ if (v === void 0 || v === true) return true;
56
+ if (v === false) return false;
57
+ return v[kind] ?? true;
58
+ }
59
+ function buildClient(executor, prefix, endpoints, options) {
59
60
  const client = {};
60
61
  for (const [key, entry] of Object.entries(endpoints)) {
61
62
  if ("prefix" in entry && "endpoints" in entry) {
62
63
  const nested = entry;
63
- client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);
64
+ client[key] = buildClient(
65
+ executor,
66
+ joinPaths(prefix, nested.prefix),
67
+ nested.endpoints,
68
+ options
69
+ );
64
70
  } else {
65
71
  const spec = entry;
66
72
  client[key] = async (params = {}, signal) => {
67
73
  let validatedParams = params;
68
- if (spec.request) {
74
+ if (spec.request && shouldValidate(options, "request")) {
69
75
  try {
70
76
  validatedParams = spec.request.parse(params);
71
77
  } catch (err) {
@@ -83,16 +89,20 @@ function buildClient(executor, prefix, endpoints) {
83
89
  body: validatedParams?.body,
84
90
  signal
85
91
  });
86
- let validated;
87
- try {
88
- validated = spec.response.parse(raw);
89
- } catch (err) {
90
- throw new ValidationError("Response validation failed", err);
92
+ let result;
93
+ if (shouldValidate(options, "response")) {
94
+ try {
95
+ result = spec.response.parse(raw);
96
+ } catch (err) {
97
+ throw new ValidationError("Response validation failed", err);
98
+ }
99
+ } else {
100
+ result = raw;
91
101
  }
92
102
  if (spec.adapter) {
93
- return spec.adapter(validated);
103
+ return spec.adapter(result);
94
104
  }
95
- return validated;
105
+ return result;
96
106
  };
97
107
  }
98
108
  }
@@ -101,12 +111,24 @@ function buildClient(executor, prefix, endpoints) {
101
111
 
102
112
  // src/create-executor.ts
103
113
  function createExecutor(execute, middlewares = []) {
104
- const chain = middlewares.reduceRight(
105
- (next, mw) => (opts) => mw(opts, next),
106
- execute
107
- );
114
+ const chain = middlewares.reduceRight((next, mw) => (opts) => mw(opts, next), execute);
108
115
  return { execute: chain };
109
116
  }
117
+ function dispatchExecutor(resolver) {
118
+ return {
119
+ execute: (opts) => resolver(opts).execute(opts)
120
+ };
121
+ }
122
+
123
+ // src/define-endpoint.ts
124
+ function endpoint(spec) {
125
+ return spec;
126
+ }
127
+
128
+ // src/define-router.ts
129
+ function defineRouter(prefix, endpoints) {
130
+ return { prefix, endpoints };
131
+ }
110
132
 
111
133
  // src/middleware.ts
112
134
  function defineMiddleware(fn) {
@@ -143,13 +165,19 @@ function withLogger(options) {
143
165
  const log = options?.log ?? ((msg, data) => console.log(msg, data));
144
166
  return defineMiddleware(async (opts, next) => {
145
167
  const start = Date.now();
146
- log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });
168
+ log(`[routar] ${opts.method} ${opts.url}`, {
169
+ params: opts.params,
170
+ body: opts.body
171
+ });
147
172
  try {
148
173
  const result = await next(opts);
149
174
  log(`[routar] ${opts.method} ${opts.url} \u2014 ${Date.now() - start}ms`);
150
175
  return result;
151
176
  } catch (err) {
152
- log(`[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`, err);
177
+ log(
178
+ `[routar] ${opts.method} ${opts.url} \u2014 error after ${Date.now() - start}ms`,
179
+ err
180
+ );
153
181
  throw err;
154
182
  }
155
183
  });
@@ -182,6 +210,6 @@ function serializeParams(params) {
182
210
  return result;
183
211
  }
184
212
 
185
- export { ValidationError, createApi, createExecutor, defineMiddleware, defineRouter, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
213
+ export { ValidationError, createApi, createExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
186
214
  //# sourceMappingURL=index.js.map
187
215
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/define-router.ts","../src/define-endpoint.ts","../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { RouterDef, RouterEndpoints } from './types.js';\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from './types.js';\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> =\n [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","export function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter(s => s !== '')\n .join('/')\n .replace(/\\/+/g, '/');\n return joined.endsWith('/') && joined.length > 1\n ? joined.slice(0, -1)\n : joined || '/';\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null) throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = 'ValidationError';\n if (cause !== undefined) {\n Object.defineProperty(this, 'cause', {\n value: cause,\n writable: true,\n enumerable: true,\n });\n }\n }\n}\n","import type {\n Executor,\n RouterDef,\n RouterEndpoints,\n EndpointSpec,\n InferResponse,\n RequestShape,\n} from './types.js';\nimport { joinPaths, resolvePath } from './utils/path.js';\nimport { ValidationError } from './utils/validate.js';\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (\n params: TSpec['request'] extends { parse: (data: unknown) => infer R } ? R : RequestShape,\n signal?: AbortSignal,\n) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<infer TNestedEndpoints>\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints: RouterDef<any> | RouterEndpoints | string,\n endpointsArg?: RouterEndpoints,\n): Record<string, any> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n\n if (typeof routerOrPrefixOrEndpoints === 'string') {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArg) throw new Error('endpoints is required when prefix is provided');\n endpoints = endpointsArg;\n } else if (\n 'prefix' in routerOrPrefixOrEndpoints &&\n 'endpoints' in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<any>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<any>).endpoints;\n } else {\n prefix = '';\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n }\n\n return buildClient(executor, prefix, endpoints);\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n): Record<string, any> {\n const client: Record<string, any> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n if ('prefix' in entry && 'endpoints' in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<any>;\n client[key] = buildClient(executor, joinPaths(prefix, nested.prefix), nested.endpoints);\n } else {\n // Leaf EndpointSpec\n const spec = entry as EndpointSpec<any, any, any>;\n client[key] = async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError('Request validation failed', err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let validated: unknown;\n try {\n validated = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError('Response validation failed', err);\n }\n\n if (spec.adapter) {\n return spec.adapter(validated as any);\n }\n return validated;\n };\n }\n }\n\n return client;\n}\n","import type { Executor, ExecuteOptions, ExecutorMiddleware } from './types.js';\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<(options: ExecuteOptions) => Promise<unknown>>(\n (next, mw) => (opts) => mw(opts, next),\n execute,\n );\n return { execute: chain };\n}\n","import type { ExecutorMiddleware } from './types.js';\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n const signal = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : controller.signal;\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, { params: opts.params, body: opts.body });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(`[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`, err);\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","export function serializeParams(params: Record<string, unknown>): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}
1
+ {"version":3,"sources":["../src/utils/path.ts","../src/utils/validate.ts","../src/create-api.ts","../src/create-executor.ts","../src/define-endpoint.ts","../src/define-router.ts","../src/middleware.ts","../src/utils/params.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export function joinPaths(...segments: string[]): string {\n const joined = segments\n .filter((s) => s !== \"\")\n .join(\"/\")\n .replace(/\\/+/g, \"/\");\n return joined.endsWith(\"/\") && joined.length > 1\n ? joined.slice(0, -1)\n : joined || \"/\";\n}\n\nexport function resolvePath(\n pathTemplate: string,\n params?: Record<string, unknown>,\n): string {\n if (!params) return pathTemplate;\n return pathTemplate.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, key) => {\n const value = params[key];\n if (value == null) throw new Error(`Missing path parameter: ${key}`);\n return encodeURIComponent(String(value));\n });\n}\n","export class ValidationError extends Error {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"ValidationError\";\n if (cause !== undefined) {\n Object.defineProperty(this, \"cause\", {\n value: cause,\n writable: true,\n enumerable: true,\n });\n }\n }\n}\n","import type {\n CreateApiOptions,\n EndpointSpec,\n Executor,\n InferResponse,\n RequestShape,\n RouterDef,\n RouterEndpoints,\n} from \"./types.js\";\nimport { joinPaths, resolvePath } from \"./utils/path.js\";\nimport { ValidationError } from \"./utils/validate.js\";\n\n/** Callable type for a single endpoint on the generated API client. */\ntype EndpointFn<TSpec extends EndpointSpec<any, any, any>> = (\n params: TSpec[\"request\"] extends { parse: (data: unknown) => infer R }\n ? R\n : RequestShape,\n signal?: AbortSignal,\n) => Promise<InferResponse<TSpec>>;\n\n/**\n * Fully-typed API client produced by {@link createApi}.\n * Nested {@link RouterDef} entries become nested sub-client objects.\n */\ntype ApiClient<TEndpoints extends RouterEndpoints> = {\n [K in keyof TEndpoints]: TEndpoints[K] extends RouterDef<\n infer TNestedEndpoints\n >\n ? ApiClient<TNestedEndpoints>\n : TEndpoints[K] extends EndpointSpec<any, any, any>\n ? EndpointFn<TEndpoints[K]>\n : never;\n};\n\n/**\n * Builds a fully-typed API client from an {@link Executor} and a router\n * (or bare endpoint map).\n *\n * Three call signatures are supported:\n * - `createApi(executor, router)` — preferred; pass the result of {@link defineRouter}.\n * - `createApi(executor, prefix, endpoints)` — inline router without {@link defineRouter}.\n * - `createApi(executor, endpoints)` — no prefix; useful for flat endpoint maps.\n *\n * Each key in `endpoints` becomes a typed async function on the returned client.\n * The function validates the request with `spec.request.parse` (if present),\n * resolves path parameters, calls the executor, validates the response with\n * `spec.response.parse`, and applies `spec.adapter` (if present).\n *\n * @param executor - Transport to use for every HTTP call.\n * @param router - A {@link RouterDef} produced by {@link defineRouter}.\n * @param options - Optional settings (e.g. `validate` to skip schema parsing in production).\n *\n * @example\n * ```ts\n * const todoApi = createApi(executor, todoRouter);\n * const todos = await todoApi.getList({});\n * const todo = await todoApi.getDetail({ path: { id: 1 } });\n *\n * // Skip response validation in production\n * const prodApi = createApi(executor, todoRouter, {\n * validate: { request: true, response: process.env.NODE_ENV !== 'production' },\n * });\n * ```\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n router: RouterDef<TEndpoints>,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param prefix - URL prefix prepended to every endpoint path.\n * @param endpoints - Record of named endpoint specs.\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n prefix: string,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\n/**\n * @param executor - Transport to use for every HTTP call.\n * @param endpoints - Record of named endpoint specs (no URL prefix).\n * @param options - Optional settings.\n */\nexport function createApi<TEndpoints extends RouterEndpoints>(\n executor: Executor,\n endpoints: TEndpoints,\n options?: CreateApiOptions,\n): ApiClient<TEndpoints>;\n\nexport function createApi(\n executor: Executor,\n routerOrPrefixOrEndpoints: RouterDef<RouterEndpoints> | RouterEndpoints | string,\n endpointsArgOrOptions?: RouterEndpoints | CreateApiOptions,\n optionsArg?: CreateApiOptions,\n): Record<string, unknown> {\n let prefix: string;\n let endpoints: RouterEndpoints;\n let options: CreateApiOptions | undefined;\n\n if (typeof routerOrPrefixOrEndpoints === \"string\") {\n prefix = routerOrPrefixOrEndpoints;\n if (!endpointsArgOrOptions)\n throw new Error(\"endpoints is required when prefix is provided\");\n endpoints = endpointsArgOrOptions as RouterEndpoints;\n options = optionsArg;\n } else if (\n \"prefix\" in routerOrPrefixOrEndpoints &&\n \"endpoints\" in routerOrPrefixOrEndpoints\n ) {\n prefix = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).prefix;\n endpoints = (routerOrPrefixOrEndpoints as RouterDef<RouterEndpoints>).endpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n } else {\n prefix = \"\";\n endpoints = routerOrPrefixOrEndpoints as RouterEndpoints;\n options = endpointsArgOrOptions as CreateApiOptions | undefined;\n }\n\n return buildClient(executor, prefix, endpoints, options);\n}\n\nfunction shouldValidate(\n options: CreateApiOptions | undefined,\n kind: \"request\" | \"response\",\n): boolean {\n const v = options?.validate;\n if (v === undefined || v === true) return true;\n if (v === false) return false;\n return v[kind] ?? true;\n}\n\nfunction buildClient(\n executor: Executor,\n prefix: string,\n endpoints: RouterEndpoints,\n options?: CreateApiOptions,\n): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, entry] of Object.entries(endpoints)) {\n if (\"prefix\" in entry && \"endpoints\" in entry) {\n // Nested RouterDef — recurse with merged prefix\n const nested = entry as RouterDef<RouterEndpoints>;\n client[key] = buildClient(\n executor,\n joinPaths(prefix, nested.prefix),\n nested.endpoints,\n options,\n );\n } else {\n // Leaf EndpointSpec\n const spec = entry as EndpointSpec<any, any, any>;\n client[key] = async (params: RequestShape = {}, signal?: AbortSignal) => {\n let validatedParams: RequestShape = params;\n if (spec.request && shouldValidate(options, \"request\")) {\n try {\n validatedParams = spec.request.parse(params);\n } catch (err) {\n throw new ValidationError(\"Request validation failed\", err);\n }\n }\n\n const url = resolvePath(\n joinPaths(prefix, spec.path),\n validatedParams?.path,\n );\n\n const raw = await executor.execute({\n method: spec.method,\n url,\n params: validatedParams?.query as Record<string, unknown> | undefined,\n body: validatedParams?.body,\n signal,\n });\n\n let result: unknown;\n if (shouldValidate(options, \"response\")) {\n try {\n result = spec.response.parse(raw);\n } catch (err) {\n throw new ValidationError(\"Response validation failed\", err);\n }\n } else {\n result = raw;\n }\n\n if (spec.adapter) {\n return spec.adapter(result);\n }\n return result;\n };\n }\n }\n\n return client;\n}\n","import type { ExecuteOptions, Executor, ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Creates an {@link Executor} by wrapping a transport function with an\n * optional middleware chain.\n *\n * Middlewares are applied in declaration order — the first middleware is the\n * outermost wrapper and runs first on each request.\n *\n * @param execute - The underlying transport function (fetch, axios, etc.).\n * @param middlewares - Ordered list of middlewares to apply.\n *\n * @example\n * ```ts\n * const executor = createExecutor(\n * async ({ method, url, body }) => {\n * const res = await fetch(url, { method, body: JSON.stringify(body) });\n * return res.json();\n * },\n * [withTimeout(5000), withRetry(3), withLogger()],\n * );\n * ```\n */\nexport function createExecutor(\n execute: (options: ExecuteOptions) => Promise<unknown>,\n middlewares: ExecutorMiddleware[] = [],\n): Executor {\n const chain = middlewares.reduceRight<\n (options: ExecuteOptions) => Promise<unknown>\n >((next, mw) => (opts) => mw(opts, next), execute);\n return { execute: chain };\n}\n\n/**\n * Creates an {@link Executor} that selects the underlying transport at\n * request time based on the result of `resolver`.\n *\n * Use this to unify SSR and CSR behind a single API client — the resolver\n * picks the right executor per request, so `createApi` is called once and\n * works in both environments without duplicate `*ServerApi` instances.\n *\n * The resolver receives the full {@link ExecuteOptions} so it can branch on\n * environment, URL prefix, auth context, or any runtime condition.\n *\n * @param resolver - Called on every request; returns the executor to delegate to.\n *\n * @example\n * ```ts\n * // SSR vs CSR — pick transport based on environment\n * const apiExecutor = dispatchExecutor(() =>\n * typeof window === 'undefined' ? serverExecutor : clientExecutor,\n * );\n *\n * // Route by URL prefix — internal routes use a different transport\n * const apiExecutor = dispatchExecutor((opts) =>\n * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,\n * );\n * ```\n */\nexport function dispatchExecutor(\n resolver: (opts: ExecuteOptions) => Executor,\n): Executor {\n return {\n execute: (opts) => resolver(opts).execute(opts),\n };\n}\n","import type {\n HttpMethod,\n RequestShape,\n Validator,\n ValidatorOutput,\n} from \"./types.js\";\n\n/**\n * Extracts `:param` segment names from a path template string as a union of\n * string literals.\n *\n * @example\n * ```ts\n * type P = PathParams<'/:userId/posts/:postId'>; // 'userId' | 'postId'\n * ```\n */\nexport type PathParams<TPath extends string> =\n TPath extends `${string}:${infer Param}/${infer Rest}`\n ? Param | PathParams<Rest>\n : TPath extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * When `TPath` contains dynamic segments (`:param`), requires `request.path`\n * to include all extracted param names. No constraint for static paths.\n */\ntype PathConstraint<TPath extends string> = [PathParams<TPath>] extends [never]\n ? {}\n : { path: Record<PathParams<TPath>, unknown> };\n\n/**\n * Type-safe endpoint definition helper.\n *\n * Use this instead of a plain object literal to get full type inference on\n * `adapter` without requiring explicit annotations or `as any` casts.\n * The four overloads cover every combination of optional `request` validator\n * and optional `adapter` function while keeping all return-type fields\n * required so that TypeScript can narrow them downstream.\n *\n * When `path` contains dynamic segments (e.g. `'/:id'`), TypeScript enforces\n * that `request` includes a matching `path` field with those param names.\n * A mismatch or missing key is a compile-time error.\n *\n * @example\n * ```ts\n * // ✅ path has ':id' → request.path.id is required\n * const getDetail = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ path: z.object({ id: z.number() }) }),\n * response: TodoSchema,\n * adapter: toTodoItem,\n * });\n *\n * // ❌ compile error — 'id' is missing from request.path\n * const broken = endpoint({\n * method: 'GET',\n * path: '/:id',\n * request: z.object({ query: z.object({ foo: z.string() }) }),\n * response: TodoSchema,\n * });\n * ```\n */\n// request O + adapter O\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n TOut,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request O + adapter X\nexport function endpoint<\n TPath extends string,\n TRequest extends RequestShape & PathConstraint<TPath>,\n TResponse extends Validator<unknown>,\n>(spec: {\n method: HttpMethod;\n path: TPath;\n request: Validator<TRequest>;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n request: Validator<TRequest>;\n response: TResponse;\n};\n\n// request X + adapter O\nexport function endpoint<TResponse extends Validator<unknown>, TOut>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n adapter: (raw: ValidatorOutput<TResponse>) => TOut;\n};\n\n// request X + adapter X\nexport function endpoint<TResponse extends Validator<unknown>>(spec: {\n method: HttpMethod;\n path: string;\n response: TResponse;\n}): {\n method: HttpMethod;\n path: string;\n response: TResponse;\n};\n\nexport function endpoint(spec: unknown): unknown {\n return spec;\n}\n","import type { RouterDef, RouterEndpoints } from \"./types.js\";\n\n/**\n * Groups a set of endpoint specs (and optional nested routers) under a shared\n * URL prefix.\n *\n * The returned {@link RouterDef} can be passed directly to {@link createApi}\n * to produce a fully-typed API client. Nesting another {@link RouterDef} as a\n * value creates a sub-client whose prefix is the concatenation of both prefixes.\n *\n * @param prefix - Base path prepended to every endpoint's `path` (e.g. `'/users'`).\n * @param endpoints - Record of named {@link EndpointSpec}s or nested {@link RouterDef}s.\n *\n * @example\n * ```ts\n * // Flat router\n * export const todoRouter = defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * create: endpoint({ method: 'POST', path: '/', response: TodoSchema }),\n * });\n *\n * // Nested router — api.users.todos.getList() resolves to GET /users/todos/\n * export const userRouter = defineRouter('/users', {\n * getList: endpoint({ method: 'GET', path: '/', response: UserListSchema }),\n * todos: defineRouter('/todos', {\n * getList: endpoint({ method: 'GET', path: '/', response: TodoListSchema }),\n * getDetail: endpoint({ method: 'GET', path: '/:id', response: TodoSchema }),\n * }),\n * });\n * ```\n */\nexport function defineRouter<TEndpoints extends RouterEndpoints>(\n prefix: string,\n endpoints: TEndpoints,\n): RouterDef<TEndpoints> {\n return { prefix, endpoints };\n}\n","import type { ExecutorMiddleware } from \"./types.js\";\n\n/**\n * Identity helper that returns the middleware as-is.\n *\n * Wrap your middleware function with this to get full type inference on `opts`\n * and `next` without having to annotate the type manually.\n *\n * @example\n * ```ts\n * const withCorrelationId = defineMiddleware((opts, next) =>\n * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })\n * );\n * ```\n */\nexport function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware {\n return fn;\n}\n\n/**\n * Retries a failed request up to `count` additional times.\n *\n * By default all errors trigger a retry. Pass `shouldRetry` to skip retries\n * for non-transient errors (e.g. 4xx responses).\n *\n * @param count - Number of retries (not counting the initial attempt).\n * @param options.shouldRetry - Return `false` to stop retrying early.\n *\n * @example\n * ```ts\n * withRetry(3, {\n * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,\n * })\n * ```\n */\nexport function withRetry(\n count: number,\n options?: { shouldRetry?: (error: unknown, attempt: number) => boolean },\n): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n let lastError: unknown;\n for (let attempt = 0; attempt <= count; attempt++) {\n try {\n return await next(opts);\n } catch (err) {\n lastError = err;\n if (attempt === count) break;\n if (options?.shouldRetry && !options.shouldRetry(err, attempt)) break;\n }\n }\n throw lastError;\n });\n}\n\n/**\n * Aborts a request if it does not complete within `ms` milliseconds.\n *\n * Merges the timeout signal with any existing `AbortSignal` on the request,\n * so whichever fires first wins.\n *\n * @param ms - Timeout in milliseconds.\n */\nexport function withTimeout(ms: number): ExecutorMiddleware {\n return defineMiddleware(async (opts, next) => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n const signal = opts.signal\n ? anySignal([opts.signal, controller.signal])\n : controller.signal;\n\n try {\n return await next({ ...opts, signal });\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\n/**\n * Logs each request and its outcome (success duration or error).\n *\n * @param options.log - Custom logging function. Defaults to `console.log`.\n *\n * @example\n * ```ts\n * withLogger({ log: (msg, data) => logger.debug(msg, data) })\n * ```\n */\nexport function withLogger(options?: {\n log?: (message: string, data?: unknown) => void;\n}): ExecutorMiddleware {\n const log = options?.log ?? ((msg, data) => console.log(msg, data));\n return defineMiddleware(async (opts, next) => {\n const start = Date.now();\n log(`[routar] ${opts.method} ${opts.url}`, {\n params: opts.params,\n body: opts.body,\n });\n try {\n const result = await next(opts);\n log(`[routar] ${opts.method} ${opts.url} — ${Date.now() - start}ms`);\n return result;\n } catch (err) {\n log(\n `[routar] ${opts.method} ${opts.url} — error after ${Date.now() - start}ms`,\n err,\n );\n throw err;\n }\n });\n}\n\n/** Combines multiple AbortSignals into one that aborts when any of them fire. */\nfunction anySignal(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), { once: true });\n }\n return controller.signal;\n}\n","export function serializeParams(\n params: Record<string, unknown>,\n): URLSearchParams {\n const result = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item != null) result.append(key, String(item));\n }\n } else {\n result.append(key, String(value));\n }\n }\n return result;\n}\n"]}
package/package.json CHANGED
@@ -1,8 +1,19 @@
1
1
  {
2
2
  "name": "@routar/core",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Schema-first HTTP API client — endpoint definitions, typed router, API client factory, and middleware system",
5
- "keywords": ["api", "http", "typescript", "zod", "schema", "type-safe", "fetch", "axios", "middleware", "rest"],
5
+ "keywords": [
6
+ "api",
7
+ "http",
8
+ "typescript",
9
+ "zod",
10
+ "schema",
11
+ "type-safe",
12
+ "fetch",
13
+ "axios",
14
+ "middleware",
15
+ "rest"
16
+ ],
6
17
  "author": "Kyungbae Min",
7
18
  "license": "MIT",
8
19
  "repository": {
@@ -22,10 +33,16 @@
22
33
  "main": "./dist/index.cjs",
23
34
  "module": "./dist/index.js",
24
35
  "types": "./dist/index.d.ts",
25
- "files": ["dist", "README.md", "LICENSE"],
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
26
41
  "scripts": {
27
42
  "build": "tsup",
28
43
  "dev": "tsup --watch"
29
44
  },
30
- "publishConfig": { "access": "public" }
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
31
48
  }