HTTP Client

First, I will explain how to use the Core client directly, and then I will describe the optional integration example of the project-wide HTTP adapter used by the auto-generated service.


Direct Use of Core Client

The Paths injected into createHttpClient<Paths>() are the request contracts. The url and method are determined from the keys of Paths, queryString and pathVariable from parameters, and body from requestBody.

The response type is specified by the caller in callApi<R>(). Core does not automatically infer response types from OpenAPI responses.

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 },
  });
};

Response shape is selected per API.

typed-rx-http does not enforce ResponseWrapper. If the server response is a wrapper, specify the wrapper type as R; otherwise, specify the actual response type directly.

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,
});

Option: Project common adapter + auto-generated service

The commonService configuration below is not a mandatory structure of Core, but a project integration pattern that aims to apply authentication, SSR headers, and cache policies to multiple generated services at once.

What the project commonService does

commonService refers to a user-owned file used to consolidate common HTTP policies in the project, not a special class name provided by @byeolnaerim/typed-rx-http. The filename is flexible, and in the example below, the actual project name rxjsHttpService.ts is used.

Instead of repeating createHttpClient for each screen and auto-generated service, this file uses the callApi and callApiStream it generates as a common entry point. Therefore, if you modify the rules for authentication renewal, SSR header transmission, cache, and error handling once, it will apply uniformly to all generated services.

View the entire code of commonService/rxjsHttpService first

1. Minimum complete version

The essence of commonService is the code below. It creates headerStore and typed HTTP client once, and exports callApi/callApiStream for common use by the generated service. uploadFile and createSSEObservable can also be exposed from the same 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. Expandable version including Next.js + session auth + CSR/SSR cache

Below is a complete project example that adds session auth, SSR headers, and CSR/SSR cache to the same common adapter. If the generated service imports the cache wrapper, this form becomes the standard. Just copy it, then adjust setLogin import, ApiTypes and CacheNames locations, authentication API paths, environment variables, and unauthorized paths to fit your project.

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;
		};
	});
};

Role by code configuration

paths

This is the OpenAPI paths type used by createHttpClient and ServiceArguments to check URL, method, path variable, query, and body types.

headerStore

Stores common headers like Content-Type and Authorization in the browser and shares them with session authentication and RSocket connections.

headersProvider

Returns headerStore in CSR and reads the current request's Cookie and Authorization in SSR to provide different headers for each request.

service

A real typed HTTP client with base URL, common headers, server 401 handling, and default error messages.

sessionAuth

Synchronizes the session token before the request, retries the original Observable once after refreshing if 401, and executes the logout flow on failure.

callApi / callApiStream

Common functions imported by automatically generated general REST service and NDJSON stream service. Both return RxJS Observables.

callApiClientCache / callApiServerCache

A cache wrapper that uses CSR cache in the browser and dynamically loads the SSR cache from @byeolnaerim/typed-rx-http/next on the server.

Method of connecting to the auto-generated service.

The generator calculates the relative path of commonServiceFile and adds imports to each service. The screen imports the generated functions without directly calling the public file, and the generated function converts path, params, and body into ServiceArguments' pathVariable, queryString, and body to pass to callApi.

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

A single response is firstValueFrom.

REST APIs that respond once and finish, like search, save, and delete, can be naturally used within existing async functions if received as a generated Observable.firstValueFromIf received as such, it can be used naturally even within the existing async function.

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,
    },
  }),
);

On the screen, paramsonly appears, but the generated service contains the URL, HTTP method, and response type. Query parameters are always{ params: { ... } }passed as.

Multiple responses are subscribed.

Requests that receive multiple values, like task progress or RSocket streams,subscribeare received, and the subscription is canceled when the component disappears.

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();

When using callApi directly,

For projects not using the service generator or to check temporary endpoints before generation, you can directly call callApi of the public service. The request type is checked in paths, and the response type is specified generically by the caller.

TemporaryService.ts
ts
return callApi<HealthResponse>({
  url: "/health",
  method: "get",
});
© 2026 Byeolnaerim. All rights reserved.IntroductionPrivacy Policy