HTTP Client
首先说明如何直接使用 Core 客户端,然后以自动生成服务使用的项目公共 HTTP 适配器作为可选集成示例进行说明。
直接使用 Core 客户端
注入到 createHttpClient<Paths>() 中的 Paths 是请求合同。url 和 method 来自 Paths 的 key,queryString 和 pathVariable 来自 parameters,body 的类型由 requestBody 决定。
响应类型由调用者指定为 callApi<R>() 的 R。Core 不会自动推断 OpenAPI 响应中的响应类型。
httpClient.ts
tsimport {
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,否则直接指定实际响应类型。
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };
return client.callApi<ResponseWrapper<{ requestId: string }>>({
url: "/api/bid/create/bid/request",
method: "post",
body,
});可选:项目公共适配器 + 自动生成服务
下面的 commonService 配置并不是 Core 的必需结构,而是一个项目集成模式,旨在一次性应用多个生成服务的认证、SSR header 和缓存策略。
项目 commonService 的功能
commonService 不是 @byeolnaerim/typed-rx-http 提供的特殊类名,而是项目中用于集中管理公共 HTTP 策略的用户拥有文件。文件名可以自由命名,下面的示例中使用了实际项目名称 rxjsHttpService.ts。
界面和自动生成服务不需要重复使用 createHttpClient,而是使用该文件生成的 callApi 和 callApiStream 作为公共入口。因此,认证更新、SSR header 传递、缓存和错误处理规则只需修改一次,即可应用于所有生成服务。
从 commonService/rxjsHttpService 全部代码开始查看
1. 最小完成型
commonService的本质如下代码。只需创建一次headerStore和typed HTTP客户端,生成的服务将共同使用callApi/callApiStream进行导出。uploadFile和createSSEObservable也可以在同一客户端中直接暴露出来。
rxjsHttpService.ts
tsimport {
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
tsimport {
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
jsgenerateSwaggerFromHttp({
serviceDir: "./src/handler/service",
typesDir: "./src/handler/service/@types",
commonServiceFile:
"./src/handler/service/rxjsHttpService.ts",
});像搜索、保存、删除这样一次响应后结束的REST API,可以将生成的Observable
作为接收,这样在现有的async函数中也能自然使用。firstValueFrom在界面上只显示
BusinessSearchPage.tsx
tsximport { 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,
},
}),
);,但生成的服务中包含URL、HTTP方法和响应类型。查询参数总是通过 params传递。{ params: { ... } }{ params: { ... } }
多个响应请订阅
像任务进度或RSocket流这样的请求会到达多个值subscribe并在组件消失时取消订阅。
BatchJobPage.tsx
tsxconst 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
tsreturn callApi<HealthResponse>({
url: "/health",
method: "get",
});