HTTP 客戶端

首先說明如何直接使用 Core 客戶端,然後以選擇性整合範例說明自動生成服務使用的專案共用 HTTP adapter。


直接使用 Core 客戶端

注入到 createHttpClient<Paths>() 的 Paths 是請求合約。url 和 method 來自 Paths 的鍵,queryString 和 pathVariable 來自 parameters,body 的類型由 requestBody 決定。

響應類型由調用者指定為 callApi<R>() 的 R。Core 不會自動推斷 OpenAPI 響應中的響應類型。

httpClient.ts
ts
import {
  createHeaderStore,
  createHttpClient,
} from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

export const headerStore = createHeaderStore({
  "Content-Type": "application/json",
});

export const client = createHttpClient<Paths>({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  headerStore,
});

export const getAdminList = () => {
  return client.callApi<{ items: any[]; total: number }>({
    url: "/api/account/search/admin/get-list",
    method: "get",
    queryString: { pageNumber: 1, pageSize: 20 },
  });
};

響應形狀根據 API 選擇

typed-rx-http 不強制使用 ResponseWrapper。如果伺服器響應是包裝器,則將包裝器類型指定為 R,否則直接指定實際響應類型。

ts
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };

return client.callApi<ResponseWrapper<{ requestId: string }>>({
  url: "/api/bid/create/bid/request",
  method: "post",
  body,
});

選擇:專案共用 adapter + 自動生成服務

下面的 commonService 配置不是 Core 的必需結構,而是希望一次性將多個生成服務的認證·SSR 標頭·快取策略應用於專案的整合模式。

專案 commonService 的功能

commonService 不是 @byeolnaerim/typed-rx-http 提供的特殊類名,而是用於將項目中的公共 HTTP 政策集中到一個地方的用戶擁有文件。文件名是自由的,以下示例中使用了實際項目名稱 rxjsHttpService.ts。

界面和自動生成服務不重複使用 createHttpClient,而是使用此文件生成的 callApi 和 callApiStream 作為公共入口點。因此,認證更新、SSR 標頭傳遞、緩存和錯誤處理規則只需修改一次,即可應用於所有生成的服務。

從 commonService/rxjsHttpService 全部代碼開始查看

1. 最小完成型

commonService的本質如下代碼所示。只需創建一次headerStore和typed HTTP client,然後export生成的服務以供共用的callApi/callApiStream使用。uploadFile和createSSEObservable也可以在同一client中直接暴露出來。

rxjsHttpService.ts
ts
import {
	createHeaderStore,
	createHttpClient,
	type ServiceArguments,
} from "@byeolnaerim/typed-rx-http";
import type { Observable } from "rxjs";

import type { paths } from "./@types/ApiTypes";

export const headerStore = createHeaderStore({
	"Content-Type": "application/json",
});

export const service = createHttpClient<paths>({
	baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
	headerStore,
});

export const callApi = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return service.callApi<R, TPath, TMethod>(serviceArguments);
};

export const callApiStream = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return service.callApiStream<R, TPath, TMethod>(serviceArguments);
};

export const uploadFile = service.uploadFile;
export const createSSEObservable = service.createSSEObservable;
2. 包含session auth + CSR/SSR快取的擴展型

以下是將session auth、SSR header和CSR/SSR快取添加到相同公共適配器的完整專案範例。如果生成的服務導入了快取包裝器,則此形式為基準。只需複製並調整setLogin導入、ApiTypes和CacheNames位置、認證API路徑、環境變數和unauthorized路徑以符合自己的專案即可。

rxjsHttpService.ts
ts
import {
	createCsrCache,
	createHeaderStore,
	createHttpClient,
	createSessionAuth,
} from "@byeolnaerim/typed-rx-http";

import type {
	CacheForService,
	ServiceArguments,
} from "@byeolnaerim/typed-rx-http";

import { defer, Observable } from "rxjs";

import { setLogin } from "@ui/handler/hooks/useAccounts";

import type { paths } from "./@types/ApiTypes";

import type { CacheNames } from "./@types/CacheNames";

export const redirectToUnauthorizedOnServer401 = async () => {
	const { headers } = await import("next/headers");
	const { redirect } = await import("next/navigation");

	const h = await headers();
	const pageUrl = h.get("x-page-url") || "/";
	const redirectUri = encodeURIComponent(pageUrl);

	redirect(`/unauthorized?redirect_uri=${redirectUri}&logout=true`);
};

export const headerStore = createHeaderStore({
	"Content-Type": "application/json",
});

const headersProvider = async () => {
	if (typeof window !== "undefined") {
		/**
		 * 라이브러리쪽 코드가
		 * if (headersProvider) return headersProvider();
		 * if (headerStore) return headerStore.get();
		 * 이렇게 되어있음 같이 못씀, 방향자체는 맞음. 라이브러리쪽에서 marge해버리면 csr/ssr 구분못함
		 */
		return headerStore.get();
	}

	const { cookies } = await import("next/headers");

	const cookieStore = await cookies();
	const cookieHeader = cookieStore
		.getAll()
		.map((c) => `${c.name}=${c.value}`)
		.join("; ");

	const authorization = cookieStore.get("Authorization")?.value;
	const accessToken = cookieStore.get("accessToken")?.value;

	return {
		...(cookieHeader ? { Cookie: cookieHeader } : {}),
		...(authorization
			? {
					Authorization: authorization.startsWith("Bearer ")
						? authorization
						: `Bearer ${authorization}`,
				}
			: {}),
		...(!authorization && accessToken
			? {
					Authorization: accessToken.startsWith("Bearer ")
						? accessToken
						: `Bearer ${accessToken}`,
				}
			: {}),
	};
};

export const service = createHttpClient<paths>({
	baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
	onServer401: redirectToUnauthorizedOnServer401,
	defaultErrorMessage: () => {
		return "알 수 없는 오류가 발생했습니다.";
	},
	headerStore,
	headersProvider,
	dropAuthWhenCacheControl: true,
});

const sessionAuth = createSessionAuth({
	headerStore,
	tokenUrl: "/api/auth/token",
	refreshUrl: "/api/auth/token/refresh",
	logoutUrl: "/api/auth/logout",
	onLoginChange: (loggedIn) => {
		if (typeof window !== "undefined") {
			setLogin(loggedIn);
		}
	},
});

export const callApi = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApi<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

export const callApiStream = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
	return defer(() =>
		service.callApiStream<R, TPath, TMethod>(serviceArguments),
	).pipe(sessionAuth.withSessionAuth());
};

// CSR cache
const csrCache = createCsrCache<CacheNames>();

export const callApiClientCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	return csrCache.callApiCsrCache(callApi, serviceArguments, cacheForService);
};

export const removeCsrCache = csrCache.removeCsrCache;

export const callApiServerCache = <
	R,
	TPath extends keyof paths = keyof paths,
	TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
	serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
	cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
	if (typeof window !== "undefined") {
		return callApiClientCache(serviceArguments, cacheForService);
	}

	return new Observable<R>((subscriber) => {
		let closed = false;

		import("@byeolnaerim/typed-rx-http/next")
			.then(({ callApiSsrCache }) => {
				if (closed) {
					return;
				}

				const subscription = callApiSsrCache<paths, R, CacheNames>({
					baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
					serviceArguments: serviceArguments as ServiceArguments<
						paths,
						keyof paths,
						keyof paths[keyof paths] & string,
						R
					>,
					cacheForService,
					headersProvider,
					onServer401: redirectToUnauthorizedOnServer401,
				}).subscribe(subscriber);

				subscriber.add(() => {
					closed = true;
					subscription.unsubscribe();
				});
			})
			.catch((error) => {
				if (!closed) {
					subscriber.error(error);
				}
			});

		return () => {
			closed = true;
		};
	});
};

代碼組成的角色

paths

createHttpClient和ServiceArguments用於檢查URL、方法、路徑變數、查詢和主體類型的OpenAPI路徑類型。

headerStore

在瀏覽器中存儲Content-Type和Authorization等公共標頭,並與會話認證和RSocket連接共享。

headersProvider

在CSR中返回headerStore,而在SSR中讀取當前請求的Cookie和Authorization,為每個請求提供不同的標頭。

service

具有基本URL、公共標頭、伺服器401處理和基本錯誤消息的實際類型化HTTP客戶端。

sessionAuth

在請求之前同步會話token,若為401則刷新後重新嘗試原始Observable一次,若失敗則執行登出流程。

callApi / callApiStream

自動生成的通用REST服務和NDJSON流服務所導入的公共函數。兩者都返回RxJS Observable。

callApiClientCache / callApiServerCache

在瀏覽器中使用CSR快取,並在伺服器中動態加載@byeolnaerim/typed-rx-http/next的SSR快取的快取包裝器。

與自動生成服務的連接方式

生成器計算commonServiceFile的相對路徑並為每個服務添加導入。畫面不直接調用公共文件,而是導入生成的函數,生成函數將路徑、參數、主體轉換為ServiceArguments的pathVariable、queryString、body並傳遞給callApi。

generateSwagger.cjs
js
generateSwaggerFromHttp({
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

一次響應是 firstValueFrom

像搜索、保存、刪除這樣一次響應結束的 REST API,可以通過生成的 ObservablefirstValueFrom在現有的 async 函數中自然使用。

BusinessSearchPage.tsx
tsx
import { workplacesSearch } from "@handler/service/auto/ApiBusinessService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  workplacesSearch({
    params: {
      source: "EIW",
      q: keyword,
      size: 20,
      nextToken: nextToken || undefined,
    },
  }),
);

在畫面上只顯示 params,但生成的服務中包含 URL、HTTP 方法和響應類型。查詢參數始終{ params: { ... } }傳遞為

多個回應請訂閱

像是工作進度或 RSocket stream 等多個值到達的請求subscribe接收後,當組件消失時取消訂閱。

BatchJobPage.tsx
tsx
const subscription = processBatchJobs({
  body: targets,
}).subscribe({
  next: (event) => {
    setProcessedCount(event.processedCount);
  },
  error: (error) => {
    setErrorMessage(error.message);
  },
  complete: () => {
    setIsSubmitting(false);
  },
});

return () => subscription.unsubscribe();

直接使用 callApi 的情況

在不使用服務生成器的項目中,或在生成前檢查臨時端點時,可以直接調用公共服務的callApi。請求類型在paths中檢查,響應類型由調用者指定為泛型。

TemporaryService.ts
ts
return callApi<HealthResponse>({
  url: "/health",
  method: "get",
});
© 2026 Byeolnaerim. 版權所有。介紹隱私政策