@fgrzl/fetch 1.1.0 → 1.3.0-alpha.3

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
@@ -28,6 +28,26 @@ npm install @fgrzl/fetch
28
28
  ```ts
29
29
  import api from "@fgrzl/fetch";
30
30
 
31
+ // It just works! 🎉
32
+ const users = await api.get("https://api.example.com/api/users");
33
+ const newUser = await api.post("https://api.example.com/api/users", {
34
+ name: "John",
35
+ });
36
+
37
+ // Built-in error handling
38
+ if (users.ok) {
39
+ console.log("Users:", users.data);
40
+ } else {
41
+ console.error("Error:", users.error?.message);
42
+ }
43
+ ```
44
+
45
+ ```ts
46
+ import api from "@fgrzl/fetch";
47
+
48
+ // Set the base url to keep things simple
49
+ api.setBaseUrl("https://api.example.com");
50
+
31
51
  // It just works! 🎉
32
52
  const users = await api.get("/api/users");
33
53
  const newUser = await api.post("/api/users", { name: "John" });
@@ -48,7 +68,28 @@ import { FetchClient, useAuthentication } from "@fgrzl/fetch";
48
68
  const authClient = useAuthentication(new FetchClient(), {
49
69
  tokenProvider: () => localStorage.getItem("token") || "",
50
70
  });
71
+ ```
72
+
73
+ **Working with APIs?** Use a base URL:
51
74
 
75
+ ```ts
76
+ import { FetchClient } from "@fgrzl/fetch";
77
+
78
+ const apiClient = new FetchClient({
79
+ baseUrl: "https://api.example.com",
80
+ });
81
+
82
+ // All relative URLs are prefixed with the base URL
83
+ await apiClient.get("/users"); // → GET https://api.example.com/users
84
+ await apiClient.post("/users", data); // → POST https://api.example.com/users
85
+
86
+ // Absolute URLs work as expected
87
+ await apiClient.get("https://other-api.com/data"); // → GET https://other-api.com/data
88
+ ```
89
+
90
+ **Advanced usage** with middleware stacks:
91
+
92
+ ```ts
52
93
  // Smart defaults - just works
53
94
  useCSRF(client);
54
95
  useAuthorization(client); // Redirects to /login with return URL
package/dist/cjs/index.js CHANGED
@@ -24,6 +24,8 @@ __export(index_exports, {
24
24
  FetchError: () => FetchError,
25
25
  HttpError: () => HttpError,
26
26
  NetworkError: () => NetworkError,
27
+ appendQueryParams: () => appendQueryParams,
28
+ buildQueryParams: () => buildQueryParams,
27
29
  createAuthenticationMiddleware: () => createAuthenticationMiddleware,
28
30
  createAuthorizationMiddleware: () => createAuthorizationMiddleware,
29
31
  createCacheMiddleware: () => createCacheMiddleware,
@@ -49,16 +51,46 @@ var FetchClient = class {
49
51
  constructor(config = {}) {
50
52
  this.middlewares = [];
51
53
  this.credentials = config.credentials ?? "same-origin";
54
+ this.baseUrl = config.baseUrl;
52
55
  }
53
56
  use(middleware) {
54
57
  this.middlewares.push(middleware);
55
58
  return this;
56
59
  }
60
+ /**
61
+ * Set or update the base URL for this client instance.
62
+ *
63
+ * When a base URL is set, relative URLs will be resolved against it.
64
+ * Absolute URLs will continue to work unchanged.
65
+ *
66
+ * @param baseUrl - The base URL to set, or undefined to clear it
67
+ * @returns The client instance for method chaining
68
+ *
69
+ * @example Set base URL:
70
+ * ```typescript
71
+ * const client = new FetchClient();
72
+ * client.setBaseUrl('https://api.example.com');
73
+ *
74
+ * // Now relative URLs work
75
+ * await client.get('/users'); // → GET https://api.example.com/users
76
+ * ```
77
+ *
78
+ * @example Chain with middleware:
79
+ * ```typescript
80
+ * const client = useProductionStack(new FetchClient())
81
+ * .setBaseUrl(process.env.API_BASE_URL);
82
+ * ```
83
+ */
84
+ setBaseUrl(baseUrl) {
85
+ this.baseUrl = baseUrl;
86
+ return this;
87
+ }
57
88
  async request(url, init = {}) {
89
+ const resolvedUrl = this.resolveUrl(url);
58
90
  let index = 0;
59
91
  const execute = async (request) => {
60
- const currentRequest = request || { ...init, url };
61
- const currentUrl = currentRequest.url || url;
92
+ const currentRequest = request || { ...init, url: resolvedUrl };
93
+ const currentUrl = currentRequest.url || resolvedUrl;
62
94
  if (index >= this.middlewares.length) {
63
95
  const { url: _, ...requestInit } = currentRequest;
64
96
  return this.coreFetch(requestInit, currentUrl);
@@ -142,20 +174,48 @@ var FetchClient = class {
142
174
  if (!params) {
143
175
  return url;
144
176
  }
145
- const urlObj = new URL(
146
- url,
147
- url.startsWith("http") ? void 0 : "http://localhost"
148
- );
177
+ const resolvedUrl = this.resolveUrl(url);
178
+ if (!resolvedUrl.startsWith("http://") && !resolvedUrl.startsWith("https://") && !resolvedUrl.startsWith("//")) {
179
+ const searchParams = new URLSearchParams();
180
+ Object.entries(params).forEach(([key, value]) => {
181
+ if (value !== void 0 && value !== null) {
182
+ searchParams.set(key, String(value));
183
+ }
184
+ });
185
+ const queryString = searchParams.toString();
186
+ return queryString ? `${resolvedUrl}?${queryString}` : resolvedUrl;
187
+ }
188
+ const urlObj = new URL(resolvedUrl);
149
189
  Object.entries(params).forEach(([key, value]) => {
150
190
  if (value !== void 0 && value !== null) {
151
191
  urlObj.searchParams.set(key, String(value));
152
192
  }
153
193
  });
154
- if (!url.startsWith("http")) {
155
- return urlObj.pathname + urlObj.search;
156
- }
157
194
  return urlObj.toString();
158
195
  }
196
+ /**
197
+ * Resolves a URL with the base URL if it's relative and base URL is configured
198
+ * @param url - The URL to resolve
199
+ * @returns The resolved URL
200
+ * @private
201
+ */
202
+ resolveUrl(url) {
203
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) {
204
+ return url;
205
+ }
206
+ if (!this.baseUrl) {
207
+ return url;
208
+ }
209
+ try {
210
+ const baseUrl = new URL(this.baseUrl);
211
+ const resolvedUrl = new URL(url, baseUrl);
212
+ return resolvedUrl.toString();
213
+ } catch {
214
+ throw new Error(
215
+ `Invalid URL: Unable to resolve "${url}" with baseUrl "${this.baseUrl}"`
216
+ );
217
+ }
218
+ }
159
219
  // 🎯 PIT OF SUCCESS: Convenience methods with smart defaults
160
220
  /**
161
221
  * HEAD request with query parameter support.
@@ -1068,6 +1128,40 @@ var NetworkError = class extends FetchError {
1068
1128
  }
1069
1129
  };
1070
1130
 
1131
+ // src/client/query.ts
1132
+ function buildQueryParams(query) {
1133
+ const params = new URLSearchParams();
1134
+ for (const [key, value] of Object.entries(query)) {
1135
+ if (value !== void 0) {
1136
+ if (Array.isArray(value)) {
1137
+ value.forEach((item) => {
1138
+ if (item !== void 0) {
1139
+ params.append(key, String(item));
1140
+ }
1141
+ });
1142
+ } else {
1143
+ params.set(key, String(value));
1144
+ }
1145
+ }
1146
+ }
1147
+ return params.toString();
1148
+ }
1149
+ function appendQueryParams(baseUrl, query) {
1150
+ const queryString = buildQueryParams(query);
1151
+ if (!queryString) {
1152
+ return baseUrl;
1153
+ }
1154
+ const fragmentIndex = baseUrl.indexOf("#");
1155
+ if (fragmentIndex !== -1) {
1156
+ const urlPart = baseUrl.substring(0, fragmentIndex);
1157
+ const fragmentPart = baseUrl.substring(fragmentIndex);
1158
+ const separator2 = urlPart.includes("?") ? "&" : "?";
1159
+ return `${urlPart}${separator2}${queryString}${fragmentPart}`;
1160
+ }
1161
+ const separator = baseUrl.includes("?") ? "&" : "?";
1162
+ return `${baseUrl}${separator}${queryString}`;
1163
+ }
1164
+
1071
1165
  // src/index.ts
1072
1166
  var api = useProductionStack(
1073
1167
  new FetchClient({
@@ -1103,6 +1197,8 @@ var index_default = api;
1103
1197
  FetchError,
1104
1198
  HttpError,
1105
1199
  NetworkError,
1200
+ appendQueryParams,
1201
+ buildQueryParams,
1106
1202
  createAuthenticationMiddleware,
1107
1203
  createAuthorizationMiddleware,
1108
1204
  createCacheMiddleware,