HTTP-Client

Zuerst wird erklärt, wie man den Core-Client direkt verwendet, gefolgt von einem Beispiel für die optionale Integration des gemeinsamen HTTP-Adapters, den der automatisch generierte Service verwendet.


Direkte Verwendung des Core-Clients

Die Paths, die in createHttpClient<Paths>() injiziert werden, sind die Anfrageverträge. URL und Methode stammen aus dem Schlüssel von Paths, queryString und pathVariable aus den Parametern, und der Body wird aus requestBody abgeleitet.

Der Antworttyp wird von dem R bestimmt, das der Aufrufer in callApi<R>() angibt. Core leitet den Antworttyp nicht automatisch aus den OpenAPI-Antworten ab.

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

Die Antwortform kann pro API ausgewählt werden.

typed-rx-http zwingt nicht zur Verwendung von ResponseWrapper. Wenn die Serverantwort ein Wrapper ist, geben Sie den Wrapper-Typ als R an, andernfalls geben Sie den tatsächlichen Antworttyp direkt an.

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

Optional: Gemeinsamer Adapter + automatisch generierter Service

Die folgende Konfiguration von commonService ist keine zwingende Struktur des Core, sondern ein Projektintegrationsmuster, das versucht, Authentifizierung, SSR-Header und Cache-Richtlinien auf mehrere generierte Services gleichzeitig anzuwenden.

Was der Projekt-commonService tut

commonService ist keine spezielle Klassenbezeichnung, die von @byeolnaerim/typed-rx-http bereitgestellt wird, sondern eine benutzerdefinierte Datei, die im Projekt verwendet wird, um gemeinsame HTTP-Richtlinien an einem Ort zu bündeln. Der Dateiname ist frei und im folgenden Beispiel wird der tatsächliche Projektname rxjsHttpService.ts verwendet.

Um zu vermeiden, dass createHttpClient für jeden Bildschirm und jeden automatisch generierten Service wiederholt wird, wird callApi und callApiStream, die von dieser Datei erstellt wurden, als gemeinsamer Einstiegspunkt verwendet. Daher werden Änderungen an den Regeln für die Authentifizierung, die Übertragung von SSR-Headern, Cache und Fehlerbehandlung einmal vorgenommen und gelten für alle generierten Services.

Beginnen Sie mit dem gesamten Code von commonService/rxjsHttpService.

1. Minimale vollständige Version

Die Essenz des commonService ist der folgende Code. Erstellen Sie einmal den headerStore und den typisierten HTTP-Client, und exportieren Sie die generierte Dienstleistung, die callApi/callApiStream gemeinsam nutzt. uploadFile und createSSEObservable können ebenfalls im selben Client exponiert werden.

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. Next.js + Sitzungsauthentifizierung + erweiterte CSR/SSR-Cache

Unten ist ein vollständiges Projektbeispiel, das die Sitzungsauthentifizierung, SSR-Header und CSR/SSR-Cache zum gleichen gemeinsamen Adapter hinzufügt. Wenn das generierte Dienstprojekt den Cache-Wrapper importiert, wird diese Form zum Standard. Kopieren Sie es einfach und passen Sie setLogin-Import, ApiTypes und CacheNames-Position, Authentifizierungs-API-Pfad, Umgebungsvariablen und nicht autorisierte Pfade an Ihr Projekt an.

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

Rolle nach Code-Struktur

paths

Typen von OpenAPI-Pfaden, die von createHttpClient und ServiceArguments verwendet werden, um URL, Methode, Pfadvariable, Abfrage und Body-Typ zu überprüfen.

headerStore

Speichert gemeinsame Header wie Content-Type und Authorization im Browser und teilt sie mit der Sitzungsauthentifizierung und RSocket-Verbindungen.

headersProvider

Gibt headerStore in CSR zurück und liest in SSR die Cookies und Authorization der aktuellen Anfrage, um für jede Anfrage unterschiedliche Header bereitzustellen.

service

Echter typisierter HTTP-Client mit Basis-URL, gemeinsamen Headern, Server-401-Verarbeitung und Standardfehlernachricht.

sessionAuth

Synchronisiert das Sitzungstoken vor der Anfrage und versucht bei 401 nach einem Refresh das ursprüngliche Observable einmal erneut; bei Fehlschlag wird der Logout-Prozess ausgeführt.

callApi / callApiStream

Gemeinsame Funktionen, die von automatisch generierten allgemeinen REST-Diensten und NDJSON-Stream-Diensten importiert werden. Beide geben ein RxJS Observable zurück.

callApiClientCache / callApiServerCache

Cache-Wrapper, der im Browser den CSR-Cache verwendet und auf dem Server den SSR-Cache von @byeolnaerim/typed-rx-http/next dynamisch lädt.

Art und Weise, wie es mit dem automatisch generierten Dienst verbunden ist.

Der Generator berechnet den relativen Pfad der commonServiceFile und fügt jedem Dienst einen Import hinzu. Der Bildschirm importiert die generierten Funktionen, ohne die öffentlichen Dateien direkt aufzurufen, und die generierte Funktion übergibt den Pfad, die Parameter und den Body an callApi als pathVariable, queryString und body von ServiceArguments.

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

Eine Antwort ist firstValueFrom

REST-APIs, die einmal antworten und enden, wie Suchen, Speichern, Löschen, können das generierte Observableempfangen werden. Dies ist derzeit die am häufigsten verwendete Methode im NPL-Projekt.erhalten, sodass sie auch in bestehenden asynchronen Funktionen natürlich verwendet werden können.

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

Auf dem Bildschirm werden paramsnur angezeigt, aber im generierten Service sind URL, HTTP-Methode und Antworttyp enthalten. Abfrageparameter werden immer{ params: { ... } }übergeben.

Mehrere Antworten abonnieren

Anfragen, bei denen mehrere Werte wie der Fortschritt oder RSocket-Stream ankommen,firstValueFromwerden empfangen, und die Abonnierung wird aufgehoben, wenn die Komponente verschwindet.

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

Wenn Sie callApi direkt verwenden

In Projekten, die keinen Dienstgenerator verwenden oder vor der Erstellung temporäre Endpunkte überprüfen, kann die callApi des öffentlichen Dienstes direkt aufgerufen werden. Der Anfragetyp wird in den Pfaden überprüft und der Antworttyp wird vom Aufrufer generisch angegeben.

TemporaryService.ts
ts
return callApi<HealthResponse>({
  url: "/health",
  method: "get",
});
© 2026 Byeolnaerim. Alle Rechte vorbehalten.EinführungDatenschutzrichtlinie