diff --git a/src/shared/utils/helpers/callApi/callApi.spec.ts b/src/shared/utils/helpers/callApi/callApi.spec.ts new file mode 100644 index 0000000..a85378c --- /dev/null +++ b/src/shared/utils/helpers/callApi/callApi.spec.ts @@ -0,0 +1,40 @@ +import { HTTPError } from "ky"; +import { callApi } from "./callApi"; + +function makeHttpError(status: number, body: object) { + const response = new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); + return new HTTPError(response, new Request("http://test.com"), {}); +} + +describe("callApi", () => { + describe("happy path", () => { + it("returns [data, null] when the fn resolves", async () => { + const [data, error] = await callApi(() => Promise.resolve({ id: 1 })); + + expect(data).toEqual({ id: 1 }); + expect(error).toBeNull(); + }); + }); + + describe("error cases", () => { + it("returns [null, ApiError] on HTTPError", async () => { + const [data, error] = await callApi(() => + Promise.reject(makeHttpError(401, { message: "Unauthorized" })), + ); + + expect(data).toBeNull(); + expect(error).toEqual({ status: 401, message: "Unauthorized" }); + }); + + it("re-throws non-HTTP errors", async () => { + const unexpected = new TypeError("Network failure"); + + await expect( + callApi(() => Promise.reject(unexpected)), + ).rejects.toThrow("Network failure"); + }); + }); +}); diff --git a/src/shared/utils/helpers/callApi/callApi.ts b/src/shared/utils/helpers/callApi/callApi.ts new file mode 100644 index 0000000..217f514 --- /dev/null +++ b/src/shared/utils/helpers/callApi/callApi.ts @@ -0,0 +1,33 @@ +import { HTTPError } from "ky"; + +export interface ApiError { + /** + * Client error response status code + */ + status: number; + /** + * Error message + */ + message: string; +} + +/** + * Helper function that calls Ky and manages its errors; + * @returns A tuple [data, error] in Golang-like fashion + */ +export async function callApi( + fn: () => Promise, +): Promise<[T, null] | [null, ApiError]> { + try { + const data = await fn(); + return [data, null]; + } catch (error) { + if (error instanceof HTTPError) { + const body = await error.response.json<{ message: string }>(); + return [null, { status: error.response.status, message: body.message }]; + } + + // re-throw unexpected errors + throw error; + } +} diff --git a/src/shared/utils/helpers/index.ts b/src/shared/utils/helpers/index.ts new file mode 100644 index 0000000..b3a9272 --- /dev/null +++ b/src/shared/utils/helpers/index.ts @@ -0,0 +1 @@ +export * from "./callApi/callApi"; diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts new file mode 100644 index 0000000..d4e09d7 --- /dev/null +++ b/src/shared/utils/index.ts @@ -0,0 +1 @@ +export * from "./helpers";