Skip to content

Commit 2a4b067

Browse files
Gruakkerwanp
andauthored
feat(openapi-fetch): baseUrl per request (#1817)
* feat(openapi-fetch): baseUrl per request * chore: update docs * chore: add changeset * fix(openapi-fetch): lint error * Update .changeset/happy-singers-fry.md Co-authored-by: Martin Paucot <[email protected]> * test: fix typo --------- Co-authored-by: Martin Paucot <[email protected]>
1 parent 0e42cbb commit 2a4b067

File tree

5 files changed

+76
-3
lines changed

5 files changed

+76
-3
lines changed

.changeset/happy-singers-fry.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
Allow specifying baseUrl per request

docs/openapi-fetch/api.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ client.GET("/my-url", options);
3636
| `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) |
3737
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
3838
| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | (optional) Parse the response using [a built-in instance method](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods) (default: `"json"`). `"stream"` skips parsing altogether and returns the raw stream. |
39+
| `baseUrl` | `string` | Prefix the fetch URL with this option (e.g. "https://myapi.dev/v1/") |
3940
| `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) |
4041
| `middleware` | `Middleware[]` | [See docs](/openapi-fetch/middleware-auth) |
4142
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) |

packages/openapi-fetch/src/index.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export type FetchResponse<T, Options, Media extends MediaType> =
110110

111111
export type RequestOptions<T> = ParamsOption<T> &
112112
RequestBodyOption<T> & {
113+
baseUrl?: string;
113114
querySerializer?: QuerySerializer<T> | QuerySerializerOptions;
114115
bodySerializer?: BodySerializer<T>;
115116
parseAs?: ParseAs;
@@ -292,3 +293,6 @@ export declare function createFinalURL<O>(
292293

293294
/** Merge headers a and b, with b taking priority */
294295
export declare function mergeHeaders(...allHeaders: (HeadersOptions | undefined)[]): Headers;
296+
297+
/** Remove trailing slash from url */
298+
export declare function removeTrailingSlash(url: string): string;

packages/openapi-fetch/src/index.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ export default function createClient(clientOptions) {
4040
headers: baseHeaders,
4141
...baseOptions
4242
} = { ...clientOptions };
43-
if (baseUrl.endsWith("/")) {
44-
baseUrl = baseUrl.substring(0, baseUrl.length - 1);
45-
}
43+
baseUrl = removeTrailingSlash(baseUrl);
4644
baseHeaders = mergeHeaders(DEFAULT_HEADERS, baseHeaders);
4745
const middlewares = [];
4846

@@ -53,6 +51,7 @@ export default function createClient(clientOptions) {
5351
*/
5452
async function coreFetch(schemaPath, fetchOptions) {
5553
const {
54+
baseUrl: localBaseUrl,
5655
fetch = baseFetch,
5756
headers,
5857
params = {},
@@ -61,6 +60,9 @@ export default function createClient(clientOptions) {
6160
bodySerializer = globalBodySerializer ?? defaultBodySerializer,
6261
...init
6362
} = fetchOptions || {};
63+
if (localBaseUrl) {
64+
baseUrl = removeTrailingSlash(localBaseUrl);
65+
}
6466

6567
let querySerializer =
6668
typeof globalQuerySerializer === "function"
@@ -563,3 +565,14 @@ export function mergeHeaders(...allHeaders) {
563565
}
564566
return finalHeaders;
565567
}
568+
569+
/**
570+
* Remove trailing slash from url
571+
* @type {import("./index.js").removeTrailingSlash}
572+
*/
573+
export function removeTrailingSlash(url) {
574+
if (url.endsWith("/")) {
575+
return url.substring(0, url.length - 1);
576+
}
577+
return url;
578+
}

packages/openapi-fetch/test/index.test.ts

+50
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,29 @@ describe("client", () => {
777777
expect(getRequestUrl().href).toBe(toAbsoluteURL("/self"));
778778
});
779779

780+
it("baseUrl per request", async () => {
781+
const localBaseUrl = "https://api.foo.bar/v1";
782+
let client = createClient<paths>({ baseUrl });
783+
784+
const { getRequestUrl } = useMockRequestHandler({
785+
baseUrl: localBaseUrl,
786+
method: "get",
787+
path: "/self",
788+
status: 200,
789+
body: { message: "OK" },
790+
});
791+
792+
await client.GET("/self", { baseUrl: localBaseUrl });
793+
794+
// assert baseUrl and path mesh as expected
795+
expect(getRequestUrl().href).toBe(toAbsoluteURL("/self", localBaseUrl));
796+
797+
client = createClient<paths>({ baseUrl });
798+
await client.GET("/self", { baseUrl: localBaseUrl });
799+
// assert trailing '/' was removed
800+
expect(getRequestUrl().href).toBe(toAbsoluteURL("/self", localBaseUrl));
801+
});
802+
780803
describe("headers", () => {
781804
it("persist", async () => {
782805
const headers: HeadersInit = { Authorization: "Bearer secrettoken" };
@@ -1282,6 +1305,33 @@ describe("client", () => {
12821305
expect(req.headers.get("onFetch")).toBe("exists");
12831306
expect(req.headers.get("onRequest")).toBe("exists");
12841307
});
1308+
1309+
it("baseUrl can be overridden", async () => {
1310+
useMockRequestHandler({
1311+
baseUrl: "https://api.foo.bar/v1/",
1312+
method: "get",
1313+
path: "/self",
1314+
status: 200,
1315+
body: {},
1316+
});
1317+
1318+
let requestBaseUrl = "";
1319+
1320+
const client = createClient<paths>({
1321+
baseUrl,
1322+
});
1323+
client.use({
1324+
onRequest({ options }) {
1325+
requestBaseUrl = options.baseUrl;
1326+
return undefined;
1327+
},
1328+
});
1329+
1330+
await client.GET("/self", {
1331+
baseUrl: "https://api.foo.bar/v1/",
1332+
});
1333+
expect(requestBaseUrl).toBe("https://api.foo.bar/v1");
1334+
});
12851335
});
12861336
});
12871337

0 commit comments

Comments
 (0)