البدء

أولاً، سأشرح كيفية استخدام عميل typed-rx-http Core، ثم سأشرح كيفية ربط محول HTTP العام المملوك للمشروع وخدمة Swagger التلقائية فقط في المشاريع التي تحتاج إلى ذلك.


1. التثبيت

bash
npm install @byeolnaerim/typed-rx-http rxjs

2. استخدام العميل الأساسي مباشرة بدون توليد تلقائي

أولاً، أبسط طريقة للاستخدام هي createHttpClient<Paths>()إنشاء عميل مرة واحدة، واستدعاء callApi لهذا العميل. Paths هو نوع المسارات الذي تم إنشاؤه في OpenAPI، ولكن يمكنك أيضًا استخدام نوع معرف مباشرة يتوافق مع نفس الهيكل.

httpClient.ts
ts
import {
  createHeaderStore,
  createHttpClient,
} from "@byeolnaerim/typed-rx-http";
import type { 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,
});

3. اختيار: ما هو rxjsHttpService/commonService؟

ما يُشار إليه في الوثيقة باسم rxjsHttpService.ts أو commonService.ts ليس اسم ملف ثابت يتم إنشاؤه تلقائيًا بواسطة المكتبة. إنه ملف محول HTTP مملوك مباشرة للمشروع. تستخدم عدة خدمات REST التي تم إنشاؤها تلقائيًا نفس الوظائف callApi/callApiStream/cache التي تصدرها هذا الملف كنقطة دخول مشتركة.

أبسط شكل هو إنشاء headerStore وcreateHttpClient مرة واحدة كما في الكود الكامل أدناه، وإعادة تصدير طرق المكتبة كوظائف للمشروع. إذا لم تستخدم وظيفة التخزين المؤقت في الخدمة التي تم إنشاؤها تلقائيًا، يمكنك فهم الهيكل باستخدام هذا التكوين فقط.

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;

للتعامل مع مصادقة الجلسة، وطلبات SSR لكل ملف تعريف، وذاكرة التخزين المؤقت CSR وذاكرة التخزين المؤقت SSR في مشروع Next.js الفعلي، يمكنك توسيعها كما هو موضح أدناه. هذا الملف أيضًا ليس منتجًا تم إنشاؤه تلقائيًا، بل هو كود المشروع.

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

4. اختيار: في حالة استخدام خدمة Swagger التلقائية.

بعد ربط مولد خدمة OpenAPI، لا تعيد كتابة عنوان URL ونوع الاستجابة في الشاشة. يتم استيراد الدوال المولدة وتمرير المسار والمعلمات والجسم المطلوبة فقط.

MemberSearchPage.tsx
tsx
import { search } from "@handler/service/auto/ApiMembersService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  search({
    params: {
      keyword: "kim",
      page: 0,
      size: 20,
    },
  }),
);

المستند التالي

© 2026 بيولناريم. جميع الحقوق محفوظة.مقدمةسياسة معالجة المعلومات الشخصية