@routar/core 1.2.1 → 1.3.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/dist/index.d.cts CHANGED
@@ -6,7 +6,7 @@ interface ExecuteOptions {
6
6
  params?: Record<string, unknown>;
7
7
  body?: unknown;
8
8
  /**
9
- * Per-request headers injected by middleware (e.g. `defineMiddleware`).
9
+ * Per-request headers injected by a plugin's `onRequest` hook.
10
10
  * Headers cannot be set from `createApi` call sites directly — use middleware
11
11
  * to add dynamic headers such as `Authorization` or `X-Request-Id`.
12
12
  */
@@ -22,21 +22,51 @@ interface Executor {
22
22
  execute(options: ExecuteOptions): Promise<unknown>;
23
23
  }
24
24
  /**
25
- * Middleware function for an {@link Executor}.
25
+ * A named, composable unit of executor behavior.
26
26
  *
27
- * Receives the current {@link ExecuteOptions} and a `next` function to call
28
- * the next middleware (or the underlying transport). Must return the response
29
- * promise.
27
+ * Each lifecycle hook is optional implement only what you need.
28
+ * Related concerns (e.g. auth header injection + 401 refresh) can be
29
+ * bundled into a single plugin.
30
30
  *
31
- * @example
31
+ * @example Auth plugin
32
32
  * ```ts
33
- * const myMiddleware: ExecutorMiddleware = async (opts, next) => {
34
- * console.log(opts.method, opts.url);
35
- * return next(opts);
33
+ * const authPlugin: ExecutorPlugin = {
34
+ * name: 'auth',
35
+ * onRequest: async (opts) => ({
36
+ * ...opts,
37
+ * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
38
+ * }),
39
+ * onError: async (err) => {
40
+ * if (isUnauthorized(err)) await refreshToken();
41
+ * throw err;
42
+ * },
36
43
  * };
37
44
  * ```
38
45
  */
39
- type ExecutorMiddleware = (options: ExecuteOptions, next: (options: ExecuteOptions) => Promise<unknown>) => Promise<unknown>;
46
+ interface ExecutorPlugin {
47
+ /** Optional name — used for introspection and `eject`. */
48
+ name?: string;
49
+ /** Runs before the request is sent. Return modified opts to transform the request. */
50
+ onRequest?: (opts: ExecuteOptions) => ExecuteOptions | Promise<ExecuteOptions>;
51
+ /** Runs after a successful response. Return a modified value to transform the response. */
52
+ onResponse?: (response: unknown, opts: ExecuteOptions) => unknown | Promise<unknown>;
53
+ /** Runs when the request throws. Must re-throw (or throw a different error). */
54
+ onError?: (error: unknown, opts: ExecuteOptions) => never | Promise<never>;
55
+ }
56
+ /**
57
+ * Options for {@link createExecutor}.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const executor = createExecutor(transport, {
62
+ * plugins: [authPlugin, logger()],
63
+ * });
64
+ * ```
65
+ */
66
+ interface CreateExecutorOptions {
67
+ /** Plugins applied in declaration order (first plugin is outermost). */
68
+ plugins?: ExecutorPlugin[];
69
+ }
40
70
  /**
41
71
  * Any object with a `parse` method — compatible with Zod, Valibot, Yup, etc.
42
72
  *
@@ -229,55 +259,43 @@ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executo
229
259
  declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
230
260
 
231
261
  /**
232
- * Creates an {@link Executor} by wrapping a transport function with an
233
- * optional middleware chain.
262
+ * Creates an {@link Executor} by wrapping a transport function with plugins.
234
263
  *
235
- * Middlewares are applied in declaration order — the first middleware is the
236
- * outermost wrapper and runs first on each request.
264
+ * Plugins run in declaration order (first plugin is outermost).
237
265
  *
238
- * @param execute - The underlying transport function (fetch, axios, etc.).
239
- * @param middlewares - Ordered list of middlewares to apply.
266
+ * For `retry` and `timeout`, use the options on {@link createFetchExecutor}
267
+ * or configure them via the underlying HTTP client (axios, ky).
240
268
  *
241
269
  * @example
242
270
  * ```ts
243
- * const executor = createExecutor(
244
- * async ({ method, url, body }) => {
245
- * const res = await fetch(url, { method, body: JSON.stringify(body) });
246
- * return res.json();
247
- * },
248
- * [withTimeout(5000), withRetry(3), withLogger()],
249
- * );
271
+ * const executor = createExecutor(transport, {
272
+ * plugins: [authPlugin, logger()],
273
+ * });
250
274
  * ```
251
275
  */
252
- declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
276
+ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, options?: CreateExecutorOptions): Executor;
253
277
  /**
254
278
  * Creates an {@link Executor} that selects the underlying transport at
255
279
  * request time based on the result of `resolver`.
256
280
  *
257
- * Use this to unify SSR and CSR behind a single API client — the resolver
258
- * picks the right executor per request, so `createApi` is called once and
259
- * works in both environments without duplicate `*ServerApi` instances.
260
- *
261
- * The resolver receives the full {@link ExecuteOptions} so it can branch on
262
- * environment, URL prefix, auth context, or any runtime condition.
263
- *
264
- * @param resolver - Called on every request; returns the executor to delegate to.
265
- *
266
281
  * @example
267
282
  * ```ts
268
- * // SSR vs CSR — pick transport based on environment
269
283
  * const apiExecutor = dispatchExecutor(() =>
270
284
  * typeof window === 'undefined' ? serverExecutor : clientExecutor,
271
285
  * );
272
- *
273
- * // Route by URL prefix — internal routes use a different transport
274
- * const apiExecutor = dispatchExecutor((opts) =>
275
- * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
276
- * );
277
286
  * ```
278
287
  */
279
288
  declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
280
289
 
290
+ type FetchRetryOption = number | {
291
+ count: number;
292
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
293
+ };
294
+ interface FetchExecutorOptions extends CreateExecutorOptions {
295
+ defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
296
+ retry?: FetchRetryOption;
297
+ timeout?: number;
298
+ }
281
299
  /**
282
300
  * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.
283
301
  *
@@ -293,7 +311,13 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
293
311
  * @param baseURL - Absolute base URL prepended to every endpoint path.
294
312
  * @param options.defaultHeaders - Async factory called on every request to
295
313
  * produce headers (e.g. reading cookies in a Next.js server component).
296
- * @param options.middlewares - Middleware chain applied before the fetch call.
314
+ * @param options.plugins - Plugins applied around the fetch call. Each retry
315
+ * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)
316
+ * and `onError` hooks. Token-refresh-on-401 patterns work by updating an
317
+ * external token store in `onError` and letting `onRequest` pick it up on
318
+ * the next attempt.
319
+ * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.
320
+ * @param options.timeout - Per-attempt timeout in milliseconds.
297
321
  *
298
322
  * @example Minimal — no options needed
299
323
  * ```ts
@@ -310,22 +334,15 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
310
334
  * });
311
335
  * ```
312
336
  *
313
- * @example Next.js App Router — forward cookies from the incoming request
337
+ * @example With retry and timeout
314
338
  * ```ts
315
339
  * const executor = createFetchExecutor('https://api.example.com', {
316
- * defaultHeaders: async () => {
317
- * const { cookies } = await import('next/headers');
318
- * const token = (await cookies()).get('access_token')?.value;
319
- * return token ? { Authorization: `Bearer ${token}` } : {};
320
- * },
321
- * middlewares: [withTimeout(8_000), withRetry(2)],
340
+ * retry: 2,
341
+ * timeout: 8_000,
322
342
  * });
323
343
  * ```
324
344
  */
325
- declare function createFetchExecutor(baseURL: string, options?: {
326
- defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
327
- middlewares?: ExecutorMiddleware[];
328
- }): Executor;
345
+ declare function createFetchExecutor(baseURL: string, options?: FetchExecutorOptions): Executor;
329
346
  /**
330
347
  * Thrown by {@link createFetchExecutor} when the server returns a non-2xx
331
348
  * status code.
@@ -342,19 +359,10 @@ declare function createFetchExecutor(baseURL: string, options?: {
342
359
  * ```
343
360
  */
344
361
  declare class HttpError extends Error {
345
- /** HTTP status code (e.g. 404, 500). */
346
362
  readonly status: number;
347
- /** HTTP status text (e.g. "Not Found"). */
348
363
  readonly statusText: string;
349
- /** Parsed response body, or `null` if the body was empty or not JSON. */
350
364
  readonly body: unknown;
351
- constructor(
352
- /** HTTP status code (e.g. 404, 500). */
353
- status: number,
354
- /** HTTP status text (e.g. "Not Found"). */
355
- statusText: string,
356
- /** Parsed response body, or `null` if the body was empty or not JSON. */
357
- body?: unknown);
365
+ constructor(status: number, statusText: string, body?: unknown);
358
366
  }
359
367
 
360
368
  /**
@@ -522,71 +530,29 @@ declare function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints>
522
530
  declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
523
531
 
524
532
  /**
525
- * Thrown by {@link withTimeout} when a request exceeds the configured duration.
526
- * Distinguishable from a user-initiated {@link AbortSignal} cancellation.
533
+ * Thrown by the built-in `timeout` option when a request exceeds the
534
+ * configured duration. Distinguishable from a user-initiated
535
+ * {@link AbortSignal} cancellation.
527
536
  */
528
537
  declare class TimeoutError extends Error {
529
538
  readonly ms: number;
530
539
  constructor(ms: number);
531
540
  }
532
541
  /**
533
- * Identity helper that returns the middleware as-is.
534
- *
535
- * Wrap your middleware function with this to get full type inference on `opts`
536
- * and `next` without having to annotate the type manually.
537
- *
538
- * @example
539
- * ```ts
540
- * const withCorrelationId = defineMiddleware((opts, next) =>
541
- * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })
542
- * );
543
- * ```
544
- */
545
- declare function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware;
546
- /**
547
- * Retries a failed request up to `count` additional times.
548
- *
549
- * By default all errors trigger a retry. Pass `shouldRetry` to skip retries
550
- * for non-transient errors (e.g. 4xx responses).
551
- *
552
- * @param count - Number of retries (not counting the initial attempt).
553
- * @param options.shouldRetry - Return `false` to stop retrying early.
554
- * Receives the error and a zero-based `attempt` index (0 = first failure,
555
- * 1 = second failure, …) so you can limit retries by count or error type.
556
- *
557
- * @example
558
- * ```ts
559
- * withRetry(3, {
560
- * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,
561
- * })
562
- * ```
563
- */
564
- declare function withRetry(count: number, options?: {
565
- shouldRetry?: (error: unknown, attempt: number) => boolean;
566
- }): ExecutorMiddleware;
567
- /**
568
- * Aborts a request if it does not complete within `ms` milliseconds.
569
- *
570
- * Merges the timeout signal with any existing `AbortSignal` on the request,
571
- * so whichever fires first wins.
572
- *
573
- * @param ms - Timeout in milliseconds.
542
+ * Identity helper that returns the plugin as-is, providing full type inference.
574
543
  *
575
544
  * @example
576
545
  * ```ts
577
- * const executor = createFetchExecutor('https://api.example.com', {
578
- * middlewares: [withTimeout(5_000)],
546
+ * const authPlugin = definePlugin({
547
+ * name: 'auth',
548
+ * onRequest: async (opts) => ({
549
+ * ...opts,
550
+ * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
551
+ * }),
579
552
  * });
580
- *
581
- * // Combine with retry — timeout applies per attempt
582
- * const executor = createExecutor(transport, [
583
- * withTimeout(5_000),
584
- * withRetry(3, { shouldRetry: (err) => !(err instanceof HttpError && err.status < 500) }),
585
- * withLogger(),
586
- * ]);
587
553
  * ```
588
554
  */
589
- declare function withTimeout(ms: number): ExecutorMiddleware;
555
+ declare function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin;
590
556
  /**
591
557
  * Logs each request and its outcome (success duration or error).
592
558
  *
@@ -594,12 +560,14 @@ declare function withTimeout(ms: number): ExecutorMiddleware;
594
560
  *
595
561
  * @example
596
562
  * ```ts
597
- * withLogger({ log: (msg, data) => logger.debug(msg, data) })
563
+ * createExecutor(transport, {
564
+ * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],
565
+ * })
598
566
  * ```
599
567
  */
600
- declare function withLogger(options?: {
568
+ declare function logger(options?: {
601
569
  log?: (message: string, data?: unknown) => void;
602
- }): ExecutorMiddleware;
570
+ }): ExecutorPlugin;
603
571
 
604
572
  declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
605
573
 
@@ -618,4 +586,4 @@ declare class ValidationError extends Error {
618
586
  constructor(message: string, cause?: unknown);
619
587
  }
620
588
 
621
- export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
589
+ export { type ApiTypes, type CreateApiOptions, type CreateExecutorOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorPlugin, type FetchExecutorOptions, type FetchRetryOption, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ interface ExecuteOptions {
6
6
  params?: Record<string, unknown>;
7
7
  body?: unknown;
8
8
  /**
9
- * Per-request headers injected by middleware (e.g. `defineMiddleware`).
9
+ * Per-request headers injected by a plugin's `onRequest` hook.
10
10
  * Headers cannot be set from `createApi` call sites directly — use middleware
11
11
  * to add dynamic headers such as `Authorization` or `X-Request-Id`.
12
12
  */
@@ -22,21 +22,51 @@ interface Executor {
22
22
  execute(options: ExecuteOptions): Promise<unknown>;
23
23
  }
24
24
  /**
25
- * Middleware function for an {@link Executor}.
25
+ * A named, composable unit of executor behavior.
26
26
  *
27
- * Receives the current {@link ExecuteOptions} and a `next` function to call
28
- * the next middleware (or the underlying transport). Must return the response
29
- * promise.
27
+ * Each lifecycle hook is optional implement only what you need.
28
+ * Related concerns (e.g. auth header injection + 401 refresh) can be
29
+ * bundled into a single plugin.
30
30
  *
31
- * @example
31
+ * @example Auth plugin
32
32
  * ```ts
33
- * const myMiddleware: ExecutorMiddleware = async (opts, next) => {
34
- * console.log(opts.method, opts.url);
35
- * return next(opts);
33
+ * const authPlugin: ExecutorPlugin = {
34
+ * name: 'auth',
35
+ * onRequest: async (opts) => ({
36
+ * ...opts,
37
+ * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
38
+ * }),
39
+ * onError: async (err) => {
40
+ * if (isUnauthorized(err)) await refreshToken();
41
+ * throw err;
42
+ * },
36
43
  * };
37
44
  * ```
38
45
  */
39
- type ExecutorMiddleware = (options: ExecuteOptions, next: (options: ExecuteOptions) => Promise<unknown>) => Promise<unknown>;
46
+ interface ExecutorPlugin {
47
+ /** Optional name — used for introspection and `eject`. */
48
+ name?: string;
49
+ /** Runs before the request is sent. Return modified opts to transform the request. */
50
+ onRequest?: (opts: ExecuteOptions) => ExecuteOptions | Promise<ExecuteOptions>;
51
+ /** Runs after a successful response. Return a modified value to transform the response. */
52
+ onResponse?: (response: unknown, opts: ExecuteOptions) => unknown | Promise<unknown>;
53
+ /** Runs when the request throws. Must re-throw (or throw a different error). */
54
+ onError?: (error: unknown, opts: ExecuteOptions) => never | Promise<never>;
55
+ }
56
+ /**
57
+ * Options for {@link createExecutor}.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const executor = createExecutor(transport, {
62
+ * plugins: [authPlugin, logger()],
63
+ * });
64
+ * ```
65
+ */
66
+ interface CreateExecutorOptions {
67
+ /** Plugins applied in declaration order (first plugin is outermost). */
68
+ plugins?: ExecutorPlugin[];
69
+ }
40
70
  /**
41
71
  * Any object with a `parse` method — compatible with Zod, Valibot, Yup, etc.
42
72
  *
@@ -229,55 +259,43 @@ declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executo
229
259
  declare function createApi<TEndpoints extends RouterEndpoints>(executor: Executor, endpoints: TEndpoints, options?: CreateApiOptions): ApiClient<TEndpoints>;
230
260
 
231
261
  /**
232
- * Creates an {@link Executor} by wrapping a transport function with an
233
- * optional middleware chain.
262
+ * Creates an {@link Executor} by wrapping a transport function with plugins.
234
263
  *
235
- * Middlewares are applied in declaration order — the first middleware is the
236
- * outermost wrapper and runs first on each request.
264
+ * Plugins run in declaration order (first plugin is outermost).
237
265
  *
238
- * @param execute - The underlying transport function (fetch, axios, etc.).
239
- * @param middlewares - Ordered list of middlewares to apply.
266
+ * For `retry` and `timeout`, use the options on {@link createFetchExecutor}
267
+ * or configure them via the underlying HTTP client (axios, ky).
240
268
  *
241
269
  * @example
242
270
  * ```ts
243
- * const executor = createExecutor(
244
- * async ({ method, url, body }) => {
245
- * const res = await fetch(url, { method, body: JSON.stringify(body) });
246
- * return res.json();
247
- * },
248
- * [withTimeout(5000), withRetry(3), withLogger()],
249
- * );
271
+ * const executor = createExecutor(transport, {
272
+ * plugins: [authPlugin, logger()],
273
+ * });
250
274
  * ```
251
275
  */
252
- declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, middlewares?: ExecutorMiddleware[]): Executor;
276
+ declare function createExecutor(execute: (options: ExecuteOptions) => Promise<unknown>, options?: CreateExecutorOptions): Executor;
253
277
  /**
254
278
  * Creates an {@link Executor} that selects the underlying transport at
255
279
  * request time based on the result of `resolver`.
256
280
  *
257
- * Use this to unify SSR and CSR behind a single API client — the resolver
258
- * picks the right executor per request, so `createApi` is called once and
259
- * works in both environments without duplicate `*ServerApi` instances.
260
- *
261
- * The resolver receives the full {@link ExecuteOptions} so it can branch on
262
- * environment, URL prefix, auth context, or any runtime condition.
263
- *
264
- * @param resolver - Called on every request; returns the executor to delegate to.
265
- *
266
281
  * @example
267
282
  * ```ts
268
- * // SSR vs CSR — pick transport based on environment
269
283
  * const apiExecutor = dispatchExecutor(() =>
270
284
  * typeof window === 'undefined' ? serverExecutor : clientExecutor,
271
285
  * );
272
- *
273
- * // Route by URL prefix — internal routes use a different transport
274
- * const apiExecutor = dispatchExecutor((opts) =>
275
- * opts.url.startsWith('/internal') ? internalExecutor : publicExecutor,
276
- * );
277
286
  * ```
278
287
  */
279
288
  declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor): Executor;
280
289
 
290
+ type FetchRetryOption = number | {
291
+ count: number;
292
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
293
+ };
294
+ interface FetchExecutorOptions extends CreateExecutorOptions {
295
+ defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
296
+ retry?: FetchRetryOption;
297
+ timeout?: number;
298
+ }
281
299
  /**
282
300
  * Creates an {@link Executor} backed by the browser / Node.js `fetch` API.
283
301
  *
@@ -293,7 +311,13 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
293
311
  * @param baseURL - Absolute base URL prepended to every endpoint path.
294
312
  * @param options.defaultHeaders - Async factory called on every request to
295
313
  * produce headers (e.g. reading cookies in a Next.js server component).
296
- * @param options.middlewares - Middleware chain applied before the fetch call.
314
+ * @param options.plugins - Plugins applied around the fetch call. Each retry
315
+ * attempt re-runs `onRequest` hooks (so headers are refreshed per attempt)
316
+ * and `onError` hooks. Token-refresh-on-401 patterns work by updating an
317
+ * external token store in `onError` and letting `onRequest` pick it up on
318
+ * the next attempt.
319
+ * @param options.retry - Number of retries, or `{ count, shouldRetry? }`.
320
+ * @param options.timeout - Per-attempt timeout in milliseconds.
297
321
  *
298
322
  * @example Minimal — no options needed
299
323
  * ```ts
@@ -310,22 +334,15 @@ declare function dispatchExecutor(resolver: (opts: ExecuteOptions) => Executor):
310
334
  * });
311
335
  * ```
312
336
  *
313
- * @example Next.js App Router — forward cookies from the incoming request
337
+ * @example With retry and timeout
314
338
  * ```ts
315
339
  * const executor = createFetchExecutor('https://api.example.com', {
316
- * defaultHeaders: async () => {
317
- * const { cookies } = await import('next/headers');
318
- * const token = (await cookies()).get('access_token')?.value;
319
- * return token ? { Authorization: `Bearer ${token}` } : {};
320
- * },
321
- * middlewares: [withTimeout(8_000), withRetry(2)],
340
+ * retry: 2,
341
+ * timeout: 8_000,
322
342
  * });
323
343
  * ```
324
344
  */
325
- declare function createFetchExecutor(baseURL: string, options?: {
326
- defaultHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
327
- middlewares?: ExecutorMiddleware[];
328
- }): Executor;
345
+ declare function createFetchExecutor(baseURL: string, options?: FetchExecutorOptions): Executor;
329
346
  /**
330
347
  * Thrown by {@link createFetchExecutor} when the server returns a non-2xx
331
348
  * status code.
@@ -342,19 +359,10 @@ declare function createFetchExecutor(baseURL: string, options?: {
342
359
  * ```
343
360
  */
344
361
  declare class HttpError extends Error {
345
- /** HTTP status code (e.g. 404, 500). */
346
362
  readonly status: number;
347
- /** HTTP status text (e.g. "Not Found"). */
348
363
  readonly statusText: string;
349
- /** Parsed response body, or `null` if the body was empty or not JSON. */
350
364
  readonly body: unknown;
351
- constructor(
352
- /** HTTP status code (e.g. 404, 500). */
353
- status: number,
354
- /** HTTP status text (e.g. "Not Found"). */
355
- statusText: string,
356
- /** Parsed response body, or `null` if the body was empty or not JSON. */
357
- body?: unknown);
365
+ constructor(status: number, statusText: string, body?: unknown);
358
366
  }
359
367
 
360
368
  /**
@@ -522,71 +530,29 @@ declare function isRouterDef(entry: object): entry is RouterDef<RouterEndpoints>
522
530
  declare function defineRouter<TEndpoints extends RouterEndpoints>(prefix: string, endpoints: TEndpoints): RouterDef<TEndpoints>;
523
531
 
524
532
  /**
525
- * Thrown by {@link withTimeout} when a request exceeds the configured duration.
526
- * Distinguishable from a user-initiated {@link AbortSignal} cancellation.
533
+ * Thrown by the built-in `timeout` option when a request exceeds the
534
+ * configured duration. Distinguishable from a user-initiated
535
+ * {@link AbortSignal} cancellation.
527
536
  */
528
537
  declare class TimeoutError extends Error {
529
538
  readonly ms: number;
530
539
  constructor(ms: number);
531
540
  }
532
541
  /**
533
- * Identity helper that returns the middleware as-is.
534
- *
535
- * Wrap your middleware function with this to get full type inference on `opts`
536
- * and `next` without having to annotate the type manually.
537
- *
538
- * @example
539
- * ```ts
540
- * const withCorrelationId = defineMiddleware((opts, next) =>
541
- * next({ ...opts, headers: { ...opts.headers, 'X-Request-Id': crypto.randomUUID() } })
542
- * );
543
- * ```
544
- */
545
- declare function defineMiddleware(fn: ExecutorMiddleware): ExecutorMiddleware;
546
- /**
547
- * Retries a failed request up to `count` additional times.
548
- *
549
- * By default all errors trigger a retry. Pass `shouldRetry` to skip retries
550
- * for non-transient errors (e.g. 4xx responses).
551
- *
552
- * @param count - Number of retries (not counting the initial attempt).
553
- * @param options.shouldRetry - Return `false` to stop retrying early.
554
- * Receives the error and a zero-based `attempt` index (0 = first failure,
555
- * 1 = second failure, …) so you can limit retries by count or error type.
556
- *
557
- * @example
558
- * ```ts
559
- * withRetry(3, {
560
- * shouldRetry: (err) => err instanceof HttpError && err.status >= 500,
561
- * })
562
- * ```
563
- */
564
- declare function withRetry(count: number, options?: {
565
- shouldRetry?: (error: unknown, attempt: number) => boolean;
566
- }): ExecutorMiddleware;
567
- /**
568
- * Aborts a request if it does not complete within `ms` milliseconds.
569
- *
570
- * Merges the timeout signal with any existing `AbortSignal` on the request,
571
- * so whichever fires first wins.
572
- *
573
- * @param ms - Timeout in milliseconds.
542
+ * Identity helper that returns the plugin as-is, providing full type inference.
574
543
  *
575
544
  * @example
576
545
  * ```ts
577
- * const executor = createFetchExecutor('https://api.example.com', {
578
- * middlewares: [withTimeout(5_000)],
546
+ * const authPlugin = definePlugin({
547
+ * name: 'auth',
548
+ * onRequest: async (opts) => ({
549
+ * ...opts,
550
+ * headers: { ...opts.headers, Authorization: `Bearer ${await getToken()}` },
551
+ * }),
579
552
  * });
580
- *
581
- * // Combine with retry — timeout applies per attempt
582
- * const executor = createExecutor(transport, [
583
- * withTimeout(5_000),
584
- * withRetry(3, { shouldRetry: (err) => !(err instanceof HttpError && err.status < 500) }),
585
- * withLogger(),
586
- * ]);
587
553
  * ```
588
554
  */
589
- declare function withTimeout(ms: number): ExecutorMiddleware;
555
+ declare function definePlugin(plugin: ExecutorPlugin): ExecutorPlugin;
590
556
  /**
591
557
  * Logs each request and its outcome (success duration or error).
592
558
  *
@@ -594,12 +560,14 @@ declare function withTimeout(ms: number): ExecutorMiddleware;
594
560
  *
595
561
  * @example
596
562
  * ```ts
597
- * withLogger({ log: (msg, data) => logger.debug(msg, data) })
563
+ * createExecutor(transport, {
564
+ * plugins: [logger({ log: (msg, data) => myLogger.debug(msg, data) })],
565
+ * })
598
566
  * ```
599
567
  */
600
- declare function withLogger(options?: {
568
+ declare function logger(options?: {
601
569
  log?: (message: string, data?: unknown) => void;
602
- }): ExecutorMiddleware;
570
+ }): ExecutorPlugin;
603
571
 
604
572
  declare function serializeParams(params: Record<string, unknown>): URLSearchParams;
605
573
 
@@ -618,4 +586,4 @@ declare class ValidationError extends Error {
618
586
  constructor(message: string, cause?: unknown);
619
587
  }
620
588
 
621
- export { type ApiTypes, type CreateApiOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorMiddleware, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, defineMiddleware, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, resolvePath, serializeParams, withLogger, withRetry, withTimeout };
589
+ export { type ApiTypes, type CreateApiOptions, type CreateExecutorOptions, type EndpointSpec, type ExecuteOptions, type Executor, type ExecutorPlugin, type FetchExecutorOptions, type FetchRetryOption, HttpError, type HttpMethod, type InferResponse, type PathParams, type RequestShape, type RouterDef, type RouterEndpoints, type RouterEntry, TimeoutError, ValidationError, type Validator, type ValidatorOutput, createApi, createExecutor, createFetchExecutor, definePlugin, defineRouter, dispatchExecutor, endpoint, isRouterDef, joinPaths, logger, resolvePath, serializeParams };