make-service 0.1.0 → 1.0.0-next.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,6 +8,7 @@ It adds a set of little features and allows you to parse responses with [zod](ht
8
8
  - 🤩 Type-safe return of `response.json()` and `response.text()`. Defaults to `unknown` instead of `any`.
9
9
  - 🚦 Easily setup an API with a `baseURL` and common `headers` for every request.
10
10
  - 🏗️ Compose URL from the base by just calling the endpoints and an object-like `query`.
11
+ - 🐾 Replaces URL wildcards with an object of `params`.
11
12
  - 🧙‍♀️ Automatically stringifies the `body` of a request so you can give it a JSON-like structure.
12
13
  - 🐛 Accepts a `trace` function for debugging.
13
14
 
@@ -76,7 +77,7 @@ Its [`typedResponse`](#typedresponse) can also be parsed with a zod schema. Here
76
77
  ```ts
77
78
  const response = await api.get("/users", {
78
79
  query: { search: "John" },
79
- trace: (input, requestInit) => console.log(input, requestInit),
80
+ trace: (url, requestInit) => console.log(url, requestInit),
80
81
  })
81
82
  const json = await response.json(
82
83
  z.object({
@@ -104,6 +105,38 @@ await api.head("/users")
104
105
  await api.options("/users")
105
106
  ```
106
107
 
108
+ This function can also correctly merge any sort of `URL`, `URLSearchParams`, and `Headers`.
109
+
110
+ ```ts
111
+ import { makeService } from 'make-service'
112
+
113
+ const api = makeService(new URL("https://example.com/api"), new Headers({
114
+ authorization: "Bearer 123"
115
+ }))
116
+
117
+ const response = await api.get("/users?admin=true", {
118
+ headers: [['accept', 'application/json']],
119
+ query: { page: "2" },
120
+ })
121
+
122
+ // It will call "https://example.com/api/users?admin=true&page=2"
123
+ // with headers: { authorization: "Bearer 123", accept: "application/json" }
124
+ ```
125
+
126
+ In case you want to delete a header previously set you can pass `undefined` or `'undefined'` as its value:
127
+ ```ts
128
+ const api = makeService("https://example.com/api", { authorization: "Bearer 123" })
129
+ const response = await api.get("/users", {
130
+ headers: new Headers({ authorization: 'undefined', "Content-Type": undefined }),
131
+ })
132
+ // headers will be empty.
133
+ ```
134
+ Note: Don't forget headers are case insensitive.
135
+ ```ts
136
+ const headers = new Headers({ 'Content-Type': 'application/json' })
137
+ Object.fromEntries(headers) // equals to: { 'content-type': 'application/json' }
138
+ ```
139
+
107
140
  ## enhancedFetch
108
141
 
109
142
  A wrapper around the `fetch` API.
@@ -121,22 +154,23 @@ const json = await response.json()
121
154
  // You can pass it a generic or schema to type the result
122
155
  ```
123
156
 
124
- This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](/src/make-service.ts) -, and it also accepts an object-like [`query`](/src/make-service.ts) and a `trace` function that will be called with the `input` and `requestInit` arguments.
157
+ This function accepts the same arguments as the `fetch` API - with exception of [JSON-like body](/src/make-service.ts) -, and it also accepts an object of `params` to replace URL wildcards, an object-like [`query`](/src/make-service.ts) and a `trace` function that will be called with the `url` and `requestInit` arguments.
125
158
 
126
159
  This slightly different `RequestInit` is typed as `EnhancedRequestInit`.
127
160
 
128
161
  ```ts
129
162
  import { enhancedFetch } from 'make-service'
130
163
 
131
- await enhancedFetch("https://example.com/api/users", {
164
+ await enhancedFetch("https://example.com/api/users/:role", {
132
165
  method: 'POST',
133
166
  body: { some: { object: { as: { body } } } },
134
167
  query: { page: "1" },
135
- trace: (input, requestInit) => console.log(input, requestInit)
168
+ params: { role: "admin" },
169
+ trace: (url, requestInit) => console.log(url, requestInit)
136
170
  })
137
171
 
138
172
  // The trace function will be called with the following arguments:
139
- // "https://example.com/api/users?page=1"
173
+ // "https://example.com/api/users/admin?page=1"
140
174
  // {
141
175
  // method: 'POST',
142
176
  // body: '{"some":{"object":{"as":{"body":{}}}}}',
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare const HTTP_METHODS: readonly ["get", "post", "put", "delete", "patch", "options", "head"];
1
+ declare const HTTP_METHODS: readonly ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
2
2
 
3
3
  /**
4
4
  * It returns the JSON object or throws an error if the response is not ok.
@@ -24,14 +24,24 @@ type TypedResponse = Omit<Response, 'json' | 'text'> & {
24
24
  text: TypedResponseText;
25
25
  };
26
26
  type EnhancedRequestInit = Omit<RequestInit, 'body'> & {
27
- body?: JSONValue;
27
+ body?: JSONValue | BodyInit | null;
28
28
  query?: SearchParams;
29
+ params?: Record<string, string>;
29
30
  trace?: (...args: Parameters<typeof fetch>) => void;
30
31
  };
31
32
  type ServiceRequestInit = Omit<EnhancedRequestInit, 'method'>;
32
33
  type HTTPMethod = (typeof HTTP_METHODS)[number];
33
34
  type TypedResponseJson = ReturnType<typeof getJson>;
34
35
  type TypedResponseText = ReturnType<typeof getText>;
36
+ type Prettify<T> = {
37
+ [K in keyof T]: T[K];
38
+ } & {};
39
+ type NoEmpty<T> = keyof T extends never ? never : T;
40
+ type PathParams<T extends string> = NoEmpty<T extends `${infer _}:${infer Param}/${infer Rest}` ? Prettify<{
41
+ [K in Param]: string;
42
+ } & PathParams<Rest>> : T extends `${infer _}:${infer Param}` ? {
43
+ [K in Param]: string;
44
+ } : {}>;
35
45
 
36
46
  /**
37
47
  * It merges multiple HeadersInit objects into a single Headers object
@@ -40,11 +50,11 @@ type TypedResponseText = ReturnType<typeof getText>;
40
50
  */
41
51
  declare function mergeHeaders(...entries: (HeadersInit | [string, undefined][] | Record<string, undefined>)[]): Headers;
42
52
  /**
43
- * @param input a string or URL to which the query parameters will be added
53
+ * @param url a string or URL to which the query parameters will be added
44
54
  * @param searchParams the query parameters
45
- * @returns the input with the query parameters added with the same type as the input
55
+ * @returns the url with the query parameters added with the same type as the url
46
56
  */
47
- declare function addQueryToInput(input: string | URL, searchParams?: SearchParams): string | URL;
57
+ declare function addQueryToUrl(url: string | URL, searchParams?: SearchParams): string | URL;
48
58
  /**
49
59
  * @param baseURL the base path to the API
50
60
  * @returns a function that receives a path and an object of query parameters and returns a URL
@@ -67,12 +77,12 @@ declare function makeGetApiUrl(baseURL: string | URL): (path: string, searchPara
67
77
  declare function typedResponse(response: Response): TypedResponse;
68
78
  /**
69
79
  * @param body the JSON-like body of the request
70
- * @returns the body stringified if it is not a string
80
+ * @returns the body is stringified if it is not a string and it is a JSON-like object. It also accepts other types of BodyInit such as Blob, ReadableStream, etc.
71
81
  */
72
- declare function ensureStringBody(body?: JSONValue): string | undefined;
82
+ declare function ensureStringBody<B extends JSONValue | BodyInit | null>(body?: B): B extends JSONValue ? string : B;
73
83
  /**
74
84
  *
75
- * @param input a string or URL to be fetched
85
+ * @param url a string or URL to be fetched
76
86
  * @param requestInit the requestInit to be passed to the fetch request. It is the same as the `RequestInit` type, but it also accepts a JSON-like `body` and an object-like `query` parameter.
77
87
  * @param requestInit.body the body of the request. It will be automatically stringified so you can send a JSON-like object
78
88
  * @param requestInit.query the query parameters to be added to the URL
@@ -84,7 +94,7 @@ declare function ensureStringBody(body?: JSONValue): string | undefined;
84
94
  * const untyped = await response.json();
85
95
  * // ^? unknown
86
96
  */
87
- declare function enhancedFetch(input: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
97
+ declare function enhancedFetch(url: string | URL, requestInit?: EnhancedRequestInit): Promise<TypedResponse>;
88
98
  /**
89
99
  *
90
100
  * @param baseURL the base URL to the API
@@ -96,14 +106,6 @@ declare function enhancedFetch(input: string | URL, requestInit?: EnhancedReques
96
106
  * const users = await response.json(userSchema);
97
107
  * // ^? User[]
98
108
  */
99
- declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit): {
100
- get: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
101
- post: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
102
- put: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
103
- delete: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
104
- patch: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
105
- options: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
106
- head: (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>;
107
- };
109
+ declare function makeService(baseURL: string | URL, baseHeaders?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>)): Record<"delete" | "get" | "post" | "put" | "patch" | "options" | "head", (path: string, requestInit?: ServiceRequestInit) => Promise<TypedResponse>>;
108
110
 
109
- export { EnhancedRequestInit, HTTPMethod, JSONValue, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToInput, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
111
+ export { EnhancedRequestInit, HTTPMethod, JSONValue, PathParams, Schema, SearchParams, ServiceRequestInit, TypedResponse, TypedResponseJson, TypedResponseText, addQueryToUrl, enhancedFetch, ensureStringBody, makeGetApiUrl, makeService, mergeHeaders, typedResponse };
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
- addQueryToInput: () => addQueryToInput,
23
+ addQueryToUrl: () => addQueryToUrl,
24
24
  enhancedFetch: () => enhancedFetch,
25
25
  ensureStringBody: () => ensureStringBody,
26
26
  makeGetApiUrl: () => makeGetApiUrl,
@@ -32,21 +32,18 @@ module.exports = __toCommonJS(src_exports);
32
32
 
33
33
  // src/constants.ts
34
34
  var HTTP_METHODS = [
35
- "get",
36
- "post",
37
- "put",
38
- "delete",
39
- "patch",
40
- "options",
41
- "head"
35
+ "GET",
36
+ "POST",
37
+ "PUT",
38
+ "DELETE",
39
+ "PATCH",
40
+ "OPTIONS",
41
+ "HEAD"
42
42
  ];
43
43
 
44
44
  // src/internals.ts
45
45
  function getJson(response) {
46
46
  return async (schema) => {
47
- if (!response.ok) {
48
- throw new Error(await response.text());
49
- }
50
47
  const json = await response.json();
51
48
  return schema ? schema.parse(json) : json;
52
49
  };
@@ -57,8 +54,17 @@ function getText(response) {
57
54
  return schema ? schema.parse(text) : text;
58
55
  };
59
56
  }
60
- function isHTTPMethod(method) {
61
- return HTTP_METHODS.includes(method);
57
+ function replaceUrlParams(url, params) {
58
+ if (!params)
59
+ return url;
60
+ let urlString = String(url);
61
+ Object.entries(params).forEach(([key, value]) => {
62
+ urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
63
+ });
64
+ return url instanceof URL ? new URL(urlString) : urlString;
65
+ }
66
+ function typeOf(t) {
67
+ return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
62
68
  }
63
69
 
64
70
  // src/make-service.ts
@@ -76,27 +82,27 @@ function mergeHeaders(...entries) {
76
82
  }
77
83
  return new Headers(Array.from(result.entries()));
78
84
  }
79
- function addQueryToInput(input, searchParams) {
85
+ function addQueryToUrl(url, searchParams) {
80
86
  if (!searchParams)
81
- return input;
82
- if (typeof input === "string") {
83
- const separator = input.includes("?") ? "&" : "?";
84
- return `${input}${separator}${new URLSearchParams(searchParams)}`;
87
+ return url;
88
+ if (typeof url === "string") {
89
+ const separator = url.includes("?") ? "&" : "?";
90
+ return `${url}${separator}${new URLSearchParams(searchParams)}`;
85
91
  }
86
- if (searchParams && input instanceof URL) {
92
+ if (searchParams && url instanceof URL) {
87
93
  for (const [key, value] of Object.entries(
88
94
  new URLSearchParams(searchParams)
89
95
  )) {
90
- input.searchParams.set(key, value);
96
+ url.searchParams.set(key, value);
91
97
  }
92
98
  }
93
- return input;
99
+ return url;
94
100
  }
95
101
  function makeGetApiUrl(baseURL) {
96
102
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
97
103
  return (path, searchParams) => {
98
104
  const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
99
- return addQueryToInput(url, searchParams);
105
+ return addQueryToUrl(url, searchParams);
100
106
  };
101
107
  }
102
108
  function typedResponse(response) {
@@ -112,13 +118,13 @@ function typedResponse(response) {
112
118
  }
113
119
  function ensureStringBody(body) {
114
120
  if (typeof body === "undefined")
115
- return;
121
+ return body;
116
122
  if (typeof body === "string")
117
123
  return body;
118
- return JSON.stringify(body);
124
+ return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
119
125
  }
120
- async function enhancedFetch(input, requestInit) {
121
- var _a;
126
+ async function enhancedFetch(url, requestInit) {
127
+ var _a, _b;
122
128
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
123
129
  const headers = mergeHeaders(
124
130
  {
@@ -126,11 +132,12 @@ async function enhancedFetch(input, requestInit) {
126
132
  },
127
133
  (_a = reqInit.headers) != null ? _a : {}
128
134
  );
129
- const url = addQueryToInput(input, query);
135
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
136
+ const fullUrl = addQueryToUrl(withParams, query);
130
137
  const body = ensureStringBody(reqInit.body);
131
138
  const enhancedReqInit = { ...reqInit, headers, body };
132
- trace == null ? void 0 : trace(url, enhancedReqInit);
133
- const response = await fetch(url, enhancedReqInit);
139
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
140
+ const response = await fetch(fullUrl, enhancedReqInit);
134
141
  return typedResponse(response);
135
142
  }
136
143
  function makeService(baseURL, baseHeaders) {
@@ -141,22 +148,24 @@ function makeService(baseURL, baseHeaders) {
141
148
  const response = await enhancedFetch(url, {
142
149
  ...requestInit,
143
150
  method,
144
- headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
151
+ headers: mergeHeaders(
152
+ typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
153
+ (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
154
+ )
145
155
  });
146
156
  return response;
147
157
  };
148
158
  };
149
- return new Proxy({}, {
150
- get(_target, prop) {
151
- if (isHTTPMethod(prop))
152
- return service(prop.toUpperCase());
153
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
154
- }
155
- });
159
+ let api = {};
160
+ for (const method of HTTP_METHODS) {
161
+ const lowerMethod = method.toLowerCase();
162
+ api[lowerMethod] = service(method);
163
+ }
164
+ return api;
156
165
  }
157
166
  // Annotate the CommonJS export names for ESM import in node:
158
167
  0 && (module.exports = {
159
- addQueryToInput,
168
+ addQueryToUrl,
160
169
  enhancedFetch,
161
170
  ensureStringBody,
162
171
  makeGetApiUrl,
package/dist/index.mjs CHANGED
@@ -1,20 +1,17 @@
1
1
  // src/constants.ts
2
2
  var HTTP_METHODS = [
3
- "get",
4
- "post",
5
- "put",
6
- "delete",
7
- "patch",
8
- "options",
9
- "head"
3
+ "GET",
4
+ "POST",
5
+ "PUT",
6
+ "DELETE",
7
+ "PATCH",
8
+ "OPTIONS",
9
+ "HEAD"
10
10
  ];
11
11
 
12
12
  // src/internals.ts
13
13
  function getJson(response) {
14
14
  return async (schema) => {
15
- if (!response.ok) {
16
- throw new Error(await response.text());
17
- }
18
15
  const json = await response.json();
19
16
  return schema ? schema.parse(json) : json;
20
17
  };
@@ -25,8 +22,17 @@ function getText(response) {
25
22
  return schema ? schema.parse(text) : text;
26
23
  };
27
24
  }
28
- function isHTTPMethod(method) {
29
- return HTTP_METHODS.includes(method);
25
+ function replaceUrlParams(url, params) {
26
+ if (!params)
27
+ return url;
28
+ let urlString = String(url);
29
+ Object.entries(params).forEach(([key, value]) => {
30
+ urlString = urlString.replace(new RegExp(`:${key}($|/)`), `${value}$1`);
31
+ });
32
+ return url instanceof URL ? new URL(urlString) : urlString;
33
+ }
34
+ function typeOf(t) {
35
+ return Object.prototype.toString.call(t).replace(/^\[object (.+)\]$/, "$1").toLowerCase();
30
36
  }
31
37
 
32
38
  // src/make-service.ts
@@ -44,27 +50,27 @@ function mergeHeaders(...entries) {
44
50
  }
45
51
  return new Headers(Array.from(result.entries()));
46
52
  }
47
- function addQueryToInput(input, searchParams) {
53
+ function addQueryToUrl(url, searchParams) {
48
54
  if (!searchParams)
49
- return input;
50
- if (typeof input === "string") {
51
- const separator = input.includes("?") ? "&" : "?";
52
- return `${input}${separator}${new URLSearchParams(searchParams)}`;
55
+ return url;
56
+ if (typeof url === "string") {
57
+ const separator = url.includes("?") ? "&" : "?";
58
+ return `${url}${separator}${new URLSearchParams(searchParams)}`;
53
59
  }
54
- if (searchParams && input instanceof URL) {
60
+ if (searchParams && url instanceof URL) {
55
61
  for (const [key, value] of Object.entries(
56
62
  new URLSearchParams(searchParams)
57
63
  )) {
58
- input.searchParams.set(key, value);
64
+ url.searchParams.set(key, value);
59
65
  }
60
66
  }
61
- return input;
67
+ return url;
62
68
  }
63
69
  function makeGetApiUrl(baseURL) {
64
70
  const base = baseURL instanceof URL ? baseURL.toString() : baseURL;
65
71
  return (path, searchParams) => {
66
72
  const url = `${base}${path}`.replace(/([^https?:]\/)\/+/g, "$1");
67
- return addQueryToInput(url, searchParams);
73
+ return addQueryToUrl(url, searchParams);
68
74
  };
69
75
  }
70
76
  function typedResponse(response) {
@@ -80,13 +86,13 @@ function typedResponse(response) {
80
86
  }
81
87
  function ensureStringBody(body) {
82
88
  if (typeof body === "undefined")
83
- return;
89
+ return body;
84
90
  if (typeof body === "string")
85
91
  return body;
86
- return JSON.stringify(body);
92
+ return ["number", "boolean", "array", "object"].includes(typeOf(body)) ? JSON.stringify(body) : body;
87
93
  }
88
- async function enhancedFetch(input, requestInit) {
89
- var _a;
94
+ async function enhancedFetch(url, requestInit) {
95
+ var _a, _b;
90
96
  const { query, trace, ...reqInit } = requestInit != null ? requestInit : {};
91
97
  const headers = mergeHeaders(
92
98
  {
@@ -94,11 +100,12 @@ async function enhancedFetch(input, requestInit) {
94
100
  },
95
101
  (_a = reqInit.headers) != null ? _a : {}
96
102
  );
97
- const url = addQueryToInput(input, query);
103
+ const withParams = replaceUrlParams(url, (_b = reqInit.params) != null ? _b : {});
104
+ const fullUrl = addQueryToUrl(withParams, query);
98
105
  const body = ensureStringBody(reqInit.body);
99
106
  const enhancedReqInit = { ...reqInit, headers, body };
100
- trace == null ? void 0 : trace(url, enhancedReqInit);
101
- const response = await fetch(url, enhancedReqInit);
107
+ trace == null ? void 0 : trace(fullUrl, enhancedReqInit);
108
+ const response = await fetch(fullUrl, enhancedReqInit);
102
109
  return typedResponse(response);
103
110
  }
104
111
  function makeService(baseURL, baseHeaders) {
@@ -109,21 +116,23 @@ function makeService(baseURL, baseHeaders) {
109
116
  const response = await enhancedFetch(url, {
110
117
  ...requestInit,
111
118
  method,
112
- headers: mergeHeaders(baseHeaders != null ? baseHeaders : {}, (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {})
119
+ headers: mergeHeaders(
120
+ typeof baseHeaders === "function" ? await baseHeaders() : baseHeaders != null ? baseHeaders : {},
121
+ (_a = requestInit == null ? void 0 : requestInit.headers) != null ? _a : {}
122
+ )
113
123
  });
114
124
  return response;
115
125
  };
116
126
  };
117
- return new Proxy({}, {
118
- get(_target, prop) {
119
- if (isHTTPMethod(prop))
120
- return service(prop.toUpperCase());
121
- throw new Error(`Invalid HTTP method: ${prop.toString()}`);
122
- }
123
- });
127
+ let api = {};
128
+ for (const method of HTTP_METHODS) {
129
+ const lowerMethod = method.toLowerCase();
130
+ api[lowerMethod] = service(method);
131
+ }
132
+ return api;
124
133
  }
125
134
  export {
126
- addQueryToInput,
135
+ addQueryToUrl,
127
136
  enhancedFetch,
128
137
  ensureStringBody,
129
138
  makeGetApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "make-service",
3
- "version": "0.1.0",
3
+ "version": "1.0.0-next.0",
4
4
  "description": "Some utilities to extend the 'fetch' API to better interact with external APIs.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",