@marianmeres/http-utils 2.2.0 → 2.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/AGENTS.md CHANGED
@@ -103,6 +103,13 @@ HTTP_ERROR.BadGateway // 502
103
103
  HTTP_ERROR.ServiceUnavailable // 503
104
104
  ```
105
105
 
106
+ ### Helper Functions
107
+
108
+ ```typescript
109
+ // Marks options object for options-based API (vs legacy positional API)
110
+ function opts<T extends GetOptions | DataOptions>(options: T): T
111
+ ```
112
+
106
113
  ### Utility Functions
107
114
 
108
115
  ```typescript
@@ -168,12 +175,18 @@ deno task publish # Publish to JSR and NPM
168
175
  ### Basic Usage
169
176
 
170
177
  ```typescript
178
+ import { createHttpApi, opts } from "@marianmeres/http-utils";
179
+
171
180
  const api = createHttpApi("https://api.example.com", {
172
181
  headers: { "Authorization": "Bearer token" }
173
182
  });
174
183
 
184
+ // Legacy API (default - object is request body)
175
185
  const data = await api.get("/users");
176
- await api.post("/users", { data: { name: "John" } });
186
+ await api.post("/users", { name: "John" });
187
+
188
+ // Options API (requires opts() wrapper)
189
+ await api.post("/users", opts({ data: { name: "John" }, params: { token: "abc" } }));
177
190
  ```
178
191
 
179
192
  ### Error Handling
package/API.md CHANGED
@@ -5,6 +5,7 @@ Complete API reference for `@marianmeres/http-utils`.
5
5
  ## Table of Contents
6
6
 
7
7
  - [createHttpApi](#createhttpapi)
8
+ - [opts](#opts)
8
9
  - [HttpApi Class](#httpapi-class)
9
10
  - [Types](#types)
10
11
  - [HTTP Errors](#http-errors)
@@ -78,6 +79,47 @@ Priority order: per-request > per-instance > global > built-in fallback.
78
79
 
79
80
  ---
80
81
 
82
+ ## opts
83
+
84
+ Marks an options object for the options-based API. Without this wrapper, arguments are treated as legacy positional parameters.
85
+
86
+ ```ts
87
+ function opts<T extends GetOptions | DataOptions>(options: T): T
88
+ ```
89
+
90
+ ### Why `opts()`?
91
+
92
+ The library supports two API styles:
93
+ - **Legacy API**: Positional parameters (backward compatible)
94
+ - **Options API**: Single options object with named properties
95
+
96
+ The `opts()` wrapper explicitly indicates which style you're using, preventing ambiguity when your request data might look like an options object.
97
+
98
+ ### Example
99
+
100
+ ```ts
101
+ import { createHttpApi, opts } from "@marianmeres/http-utils";
102
+
103
+ const api = createHttpApi("https://api.example.com");
104
+
105
+ // Without opts() - legacy behavior: entire object is sent as request body
106
+ await api.post("/users", { data: { name: "John" } });
107
+ // Sends: { "data": { "name": "John" } }
108
+
109
+ // With opts() - options API: data is extracted and sent as body
110
+ await api.post("/users", opts({ data: { name: "John" } }));
111
+ // Sends: { "name": "John" }
112
+
113
+ // GET with options
114
+ const respHeaders = {};
115
+ await api.get("/users", opts({
116
+ params: { headers: { "X-Custom": "value" } },
117
+ respHeaders
118
+ }));
119
+ ```
120
+
121
+ ---
122
+
81
123
  ## HttpApi Class
82
124
 
83
125
  HTTP API client class. Usually created via `createHttpApi()`.
@@ -88,12 +130,12 @@ HTTP API client class. Usually created via `createHttpApi()`.
88
130
 
89
131
  Performs a GET request.
90
132
 
91
- **New Options API (recommended):**
133
+ **Options API (with `opts()` wrapper):**
92
134
  ```ts
93
135
  async get<T = unknown>(path: string, options?: GetOptions): Promise<T>
94
136
  ```
95
137
 
96
- **Legacy API:**
138
+ **Legacy API (default behavior):**
97
139
  ```ts
98
140
  async get<T = unknown>(
99
141
  path: string,
@@ -105,17 +147,17 @@ async get<T = unknown>(
105
147
 
106
148
  **Example:**
107
149
  ```ts
108
- // New API with type parameter
150
+ // Options API with type parameter (requires opts() wrapper)
109
151
  interface User { id: number; name: string; }
110
- const user = await api.get<User>("/users/1", {
152
+ const user = await api.get<User>("/users/1", opts({
111
153
  params: { headers: { "X-Custom": "value" } },
112
154
  respHeaders: {}
113
- });
155
+ }));
114
156
 
115
157
  // Without type parameter (returns unknown)
116
158
  const data = await api.get("/users");
117
159
 
118
- // Legacy API
160
+ // Legacy API (no opts() needed)
119
161
  const data = await api.get("/users", { headers: { "X-Custom": "value" } });
120
162
  ```
121
163
 
@@ -123,12 +165,12 @@ const data = await api.get("/users", { headers: { "X-Custom": "value" } });
123
165
 
124
166
  Performs a POST request.
125
167
 
126
- **New Options API (recommended):**
168
+ **Options API (with `opts()` wrapper):**
127
169
  ```ts
128
170
  async post<T = unknown>(path: string, options?: DataOptions): Promise<T>
129
171
  ```
130
172
 
131
- **Legacy API:**
173
+ **Legacy API (default behavior):**
132
174
  ```ts
133
175
  async post<T = unknown>(
134
176
  path: string,
@@ -141,14 +183,14 @@ async post<T = unknown>(
141
183
 
142
184
  **Example:**
143
185
  ```ts
144
- // New API with type parameter
186
+ // Options API with type parameter (requires opts() wrapper)
145
187
  interface User { id: number; name: string; }
146
- const user = await api.post<User>("/users", {
188
+ const user = await api.post<User>("/users", opts({
147
189
  data: { name: "John" },
148
190
  params: { headers: { "X-Custom": "value" } }
149
- });
191
+ }));
150
192
 
151
- // Legacy API
193
+ // Legacy API (no opts() needed)
152
194
  const result = await api.post("/users", { name: "John" });
153
195
  ```
154
196
 
package/README.md CHANGED
@@ -26,38 +26,38 @@ npm install @marianmeres/http-utils
26
26
  ```
27
27
 
28
28
  ```ts
29
- import { createHttpApi, HTTP_ERROR } from "@marianmeres/http-utils";
29
+ import { createHttpApi, opts, HTTP_ERROR } from "@marianmeres/http-utils";
30
30
  ```
31
31
 
32
32
  ## Quick Start
33
33
 
34
34
  ```ts
35
- import { createHttpApi, HTTP_ERROR, NotFound } from "@marianmeres/http-utils";
35
+ import { createHttpApi, opts, HTTP_ERROR, NotFound } from "@marianmeres/http-utils";
36
36
 
37
37
  // Create an API client with base URL
38
38
  const api = createHttpApi("https://api.example.com", {
39
39
  headers: { "Authorization": "Bearer your-token" }
40
40
  });
41
41
 
42
- // GET request (new options API - recommended)
43
- const users = await api.get("/users", {
42
+ // GET request (options API with opts() wrapper)
43
+ const users = await api.get("/users", opts({
44
44
  params: { headers: { "X-Custom": "value" } }
45
- });
45
+ }));
46
46
 
47
- // POST request (new options API - recommended)
48
- const newUser = await api.post("/users", {
47
+ // POST request (options API with opts() wrapper)
48
+ const newUser = await api.post("/users", opts({
49
49
  data: { name: "John Doe" },
50
50
  params: { headers: { "X-Custom": "value" } }
51
- });
51
+ }));
52
52
 
53
- // Legacy API still works
53
+ // Legacy API (default behavior without opts())
54
54
  const legacyUsers = await api.get("/users", { headers: { "X-Custom": "value" } });
55
55
  const legacyUser = await api.post("/users", { name: "John Doe" });
56
56
 
57
57
  // With type parameters for typed responses
58
58
  interface User { id: number; name: string; }
59
59
  const user = await api.get<User>("/users/1");
60
- const created = await api.post<User>("/users", { data: { name: "Jane" } });
60
+ const created = await api.post<User>("/users", opts({ data: { name: "Jane" } }));
61
61
 
62
62
  // Error handling
63
63
  try {
@@ -89,23 +89,37 @@ const api = createHttpApi("https://api.example.com", {
89
89
  ### HTTP Methods
90
90
 
91
91
  ```ts
92
- // GET (new options API)
93
- const data = await api.get("/users", {
92
+ // GET (options API with opts() wrapper)
93
+ const data = await api.get("/users", opts({
94
94
  params: { headers: { "X-Custom": "value" } },
95
95
  respHeaders: {}
96
- });
96
+ }));
97
97
 
98
- // POST/PUT/PATCH/DELETE (new options API)
99
- await api.post("/users", {
98
+ // POST/PUT/PATCH/DELETE (options API with opts() wrapper)
99
+ await api.post("/users", opts({
100
100
  data: { name: "John" },
101
101
  params: { token: "bearer-token" }
102
- });
102
+ }));
103
103
 
104
- // Legacy API still supported
104
+ // Legacy API (default behavior without opts())
105
105
  const data = await api.get("/users", { headers: { "X-Custom": "value" } });
106
106
  await api.post("/users", { name: "John" });
107
107
  ```
108
108
 
109
+ ### The `opts()` Helper
110
+
111
+ The `opts()` function explicitly marks an options object for the options-based API. Without it, arguments are treated as legacy positional parameters.
112
+
113
+ ```ts
114
+ // Without opts() - legacy behavior: object is sent as request body
115
+ await api.post("/users", { data: { name: "John" } }); // Sends: { data: { name: "John" } }
116
+
117
+ // With opts() - options API: data is extracted and sent as body
118
+ await api.post("/users", opts({ data: { name: "John" } })); // Sends: { name: "John" }
119
+ ```
120
+
121
+ This makes the API unambiguous and prevents accidental misinterpretation of request data.
122
+
109
123
  ### Error Handling
110
124
 
111
125
  ```ts
package/dist/api.d.ts CHANGED
@@ -71,6 +71,20 @@ export interface DataOptions {
71
71
  /** Custom error message extractor for this request. */
72
72
  errorExtractor?: ErrorMessageExtractor | null;
73
73
  }
74
+ /**
75
+ * Marks an options object for the new options API.
76
+ * Use this to explicitly indicate you're using the options-based API.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // GET with options
81
+ * await api.get('/users', opts({ params: { token: 'abc' } }));
82
+ *
83
+ * // POST with options
84
+ * await api.post('/users', opts({ data: { name: 'John' }, params: { token: 'abc' } }));
85
+ * ```
86
+ */
87
+ export declare function opts<T extends GetOptions | DataOptions>(options: T): T;
74
88
  /**
75
89
  * HTTP API client with convenient defaults and error handling.
76
90
  */
package/dist/api.js CHANGED
@@ -32,6 +32,68 @@ function deepMerge(target, source) {
32
32
  function isObject(item) {
33
33
  return item !== null && typeof item === 'object' && !Array.isArray(item);
34
34
  }
35
+ /** Symbol marker for explicit options API detection. */
36
+ const OPTIONS_MARKER = Symbol('options');
37
+ /**
38
+ * Marks an options object for the new options API.
39
+ * Use this to explicitly indicate you're using the options-based API.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * // GET with options
44
+ * await api.get('/users', opts({ params: { token: 'abc' } }));
45
+ *
46
+ * // POST with options
47
+ * await api.post('/users', opts({ data: { name: 'John' }, params: { token: 'abc' } }));
48
+ * ```
49
+ */
50
+ export function opts(options) {
51
+ return Object.assign(options, { [OPTIONS_MARKER]: true });
52
+ }
53
+ /**
54
+ * Parses GET method arguments, detecting new options API via OPTIONS_MARKER.
55
+ */
56
+ function parseGetOptions(paramsOrOptions, legacyRespHeaders, legacyErrorExtractor) {
57
+ if (paramsOrOptions && OPTIONS_MARKER in paramsOrOptions) {
58
+ // New options API (explicit via opts() wrapper)
59
+ const o = paramsOrOptions;
60
+ return {
61
+ params: o.params,
62
+ respHeaders: o.respHeaders ?? null,
63
+ errorExtractor: o.errorExtractor ?? null,
64
+ };
65
+ }
66
+ // Legacy positional API
67
+ return {
68
+ params: paramsOrOptions,
69
+ respHeaders: legacyRespHeaders ?? null,
70
+ errorExtractor: legacyErrorExtractor ?? null,
71
+ };
72
+ }
73
+ /**
74
+ * Parses body method arguments (POST/PUT/PATCH/DELETE), detecting new options API via OPTIONS_MARKER.
75
+ */
76
+ function parseDataOptions(dataOrOptions, legacyParams, legacyRespHeaders, legacyErrorExtractor) {
77
+ if (dataOrOptions &&
78
+ typeof dataOrOptions === 'object' &&
79
+ OPTIONS_MARKER in dataOrOptions) {
80
+ // New options API (explicit via opts() wrapper)
81
+ const o = dataOrOptions;
82
+ return {
83
+ data: o.data ?? null,
84
+ params: o.params,
85
+ respHeaders: o.respHeaders ?? null,
86
+ errorExtractor: o.errorExtractor ?? null,
87
+ };
88
+ }
89
+ // Legacy positional API
90
+ return {
91
+ data: dataOrOptions ?? null,
92
+ params: legacyParams,
93
+ respHeaders: legacyRespHeaders ?? null,
94
+ errorExtractor: legacyErrorExtractor ?? null,
95
+ };
96
+ }
35
97
  const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal, credentials, }) => {
36
98
  const normalizedHeaders = Object.entries(headers || {}).reduce((m, [k, v]) => ({ ...m, [k.toLowerCase()]: v }), {});
37
99
  const opts = {
@@ -142,136 +204,29 @@ export class HttpApi {
142
204
  return /^https?:/.test(path) ? path : base + path;
143
205
  }
144
206
  async get(path, paramsOrOptions, respHeaders, errorMessageExtractor, _dumpParams = false) {
145
- // Detect which API is being used
146
- let params;
147
- let headers = null;
148
- let extractor = null;
149
- if (paramsOrOptions && ('respHeaders' in paramsOrOptions || 'errorExtractor' in paramsOrOptions)) {
150
- // New options API
151
- const opts = paramsOrOptions;
152
- params = opts.params;
153
- headers = opts.respHeaders ?? null;
154
- extractor = opts.errorExtractor ?? null;
155
- }
156
- else {
157
- // Legacy positional API
158
- params = paramsOrOptions;
159
- headers = respHeaders ?? null;
160
- extractor = errorMessageExtractor ?? null;
161
- }
207
+ const { params, respHeaders: headers, errorExtractor } = parseGetOptions(paramsOrOptions, respHeaders, errorMessageExtractor);
162
208
  path = this.#buildPath(path, this.#base);
163
- return _fetch(this.#merge(await this.#getDefs(), { ...params, method: 'GET', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
209
+ return _fetch(this.#merge(await this.#getDefs(), { ...params, method: 'GET', path }), headers, errorExtractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
164
210
  }
165
211
  async post(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
166
- // Detect which API is being used
167
- let data = null;
168
- let fetchParams;
169
- let headers = null;
170
- let extractor = null;
171
- if (dataOrOptions &&
172
- typeof dataOrOptions === 'object' &&
173
- !(dataOrOptions instanceof FormData) &&
174
- ('data' in dataOrOptions ||
175
- 'params' in dataOrOptions ||
176
- 'respHeaders' in dataOrOptions ||
177
- 'errorExtractor' in dataOrOptions)) {
178
- // New options API
179
- const opts = dataOrOptions;
180
- data = opts.data ?? null;
181
- fetchParams = opts.params;
182
- headers = opts.respHeaders ?? null;
183
- extractor = opts.errorExtractor ?? null;
184
- }
185
- else {
186
- // Legacy positional API
187
- data = dataOrOptions ?? null;
188
- fetchParams = params;
189
- headers = respHeaders ?? null;
190
- extractor = errorMessageExtractor ?? null;
191
- }
212
+ const { data, params: fetchParams, respHeaders: headers, errorExtractor } = parseDataOptions(dataOrOptions, params, respHeaders, errorMessageExtractor);
192
213
  path = this.#buildPath(path, this.#base);
193
- return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'POST', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
214
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'POST', path }), headers, errorExtractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
194
215
  }
195
216
  async put(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
196
- let data = null;
197
- let fetchParams;
198
- let headers = null;
199
- let extractor = null;
200
- if (dataOrOptions &&
201
- typeof dataOrOptions === 'object' &&
202
- !(dataOrOptions instanceof FormData) &&
203
- ('data' in dataOrOptions ||
204
- 'params' in dataOrOptions ||
205
- 'respHeaders' in dataOrOptions ||
206
- 'errorExtractor' in dataOrOptions)) {
207
- const opts = dataOrOptions;
208
- data = opts.data ?? null;
209
- fetchParams = opts.params;
210
- headers = opts.respHeaders ?? null;
211
- extractor = opts.errorExtractor ?? null;
212
- }
213
- else {
214
- data = dataOrOptions ?? null;
215
- fetchParams = params;
216
- headers = respHeaders ?? null;
217
- extractor = errorMessageExtractor ?? null;
218
- }
217
+ const { data, params: fetchParams, respHeaders: headers, errorExtractor } = parseDataOptions(dataOrOptions, params, respHeaders, errorMessageExtractor);
219
218
  path = this.#buildPath(path, this.#base);
220
- return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PUT', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
219
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PUT', path }), headers, errorExtractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
221
220
  }
222
221
  async patch(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
223
- let data = null;
224
- let fetchParams;
225
- let headers = null;
226
- let extractor = null;
227
- if (dataOrOptions &&
228
- typeof dataOrOptions === 'object' &&
229
- !(dataOrOptions instanceof FormData) &&
230
- ('data' in dataOrOptions ||
231
- 'params' in dataOrOptions ||
232
- 'respHeaders' in dataOrOptions ||
233
- 'errorExtractor' in dataOrOptions)) {
234
- const opts = dataOrOptions;
235
- data = opts.data ?? null;
236
- fetchParams = opts.params;
237
- headers = opts.respHeaders ?? null;
238
- extractor = opts.errorExtractor ?? null;
239
- }
240
- else {
241
- data = dataOrOptions ?? null;
242
- fetchParams = params;
243
- headers = respHeaders ?? null;
244
- extractor = errorMessageExtractor ?? null;
245
- }
222
+ const { data, params: fetchParams, respHeaders: headers, errorExtractor } = parseDataOptions(dataOrOptions, params, respHeaders, errorMessageExtractor);
246
223
  path = this.#buildPath(path, this.#base);
247
- return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PATCH', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
224
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PATCH', path }), headers, errorExtractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
248
225
  }
249
226
  async del(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
250
- let data = null;
251
- let fetchParams;
252
- let headers = null;
253
- let extractor = null;
254
- if (dataOrOptions &&
255
- typeof dataOrOptions === 'object' &&
256
- !(dataOrOptions instanceof FormData) &&
257
- ('data' in dataOrOptions ||
258
- 'params' in dataOrOptions ||
259
- 'respHeaders' in dataOrOptions ||
260
- 'errorExtractor' in dataOrOptions)) {
261
- const opts = dataOrOptions;
262
- data = opts.data ?? null;
263
- fetchParams = opts.params;
264
- headers = opts.respHeaders ?? null;
265
- extractor = opts.errorExtractor ?? null;
266
- }
267
- else {
268
- data = dataOrOptions ?? null;
269
- fetchParams = params;
270
- headers = respHeaders ?? null;
271
- extractor = errorMessageExtractor ?? null;
272
- }
227
+ const { data, params: fetchParams, respHeaders: headers, errorExtractor } = parseDataOptions(dataOrOptions, params, respHeaders, errorMessageExtractor);
273
228
  path = this.#buildPath(path, this.#base);
274
- return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'DELETE', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
229
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'DELETE', path }), headers, errorExtractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
275
230
  }
276
231
  /**
277
232
  * Helper method to build the full URL from a path.
package/dist/mod.d.ts CHANGED
@@ -21,6 +21,6 @@
21
21
  * }
22
22
  * ```
23
23
  */
24
- export { HttpApi, createHttpApi, type DataOptions, type GetOptions, type FetchParams, type ErrorMessageExtractor, type ResponseHeaders, type RequestData, } from "./api.js";
24
+ export { HttpApi, createHttpApi, opts, type DataOptions, type GetOptions, type FetchParams, type ErrorMessageExtractor, type ResponseHeaders, type RequestData, } from "./api.js";
25
25
  export { HTTP_ERROR, createHttpError, getErrorMessage } from "./error.js";
26
26
  export { HTTP_STATUS } from "./status.js";
package/dist/mod.js CHANGED
@@ -21,6 +21,6 @@
21
21
  * }
22
22
  * ```
23
23
  */
24
- export { HttpApi, createHttpApi, } from "./api.js";
24
+ export { HttpApi, createHttpApi, opts, } from "./api.js";
25
25
  export { HTTP_ERROR, createHttpError, getErrorMessage } from "./error.js";
26
26
  export { HTTP_STATUS } from "./status.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/http-utils",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",