เริ่มต้นอย่างรวดเร็ว

HTTP client ที่ปลอดภัยตามประเภทที่ใช้ RxJS สำหรับ TypeScript + (เลือก) Next.js/RSocket adapter.


คุณสมบัติหลัก

  • ฉีดประเภท Paths แบบ OpenAPI-style ที่ได้จาก Swagger/OpenAPI / AsyncAPI สคีม่า / สัญญาที่กำหนดเอง เพื่อให้มั่นใจในความเสถียรของประเภทการร้องขอ (route)
  • API ทุกตัวจะส่งคืน RxJS Observable
  • core เป็นอิสระจากเฟรมเวิร์ก (ไม่ขึ้นอยู่กับ Next.js)
  • ฟังก์ชันเฉพาะสำหรับ Next.js แยกออกเป็น @byeolnaerim/typed-rx-http/next เอนทรีพอยต์ → ต้องการ Next.js เฉพาะเมื่อ import /next.
  • ฟังก์ชันเฉพาะสำหรับ RSocket แยกออกเป็น @byeolnaerim/typed-rx-http/rsocket เอนทรีพอยต์ → ต้องการแพ็คเกจ RSocket เฉพาะเมื่อ import /rsocket.

ติดตั้ง

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

เอนทรีพอยต์

Core (ไม่ขึ้นกับ framework)
ts
import {
  createHttpClient,
  createHeaderStore,
  createCsrCache,
  createSessionAuth,
  HttpResponseError,
  isHttpResponseError,
  type ServiceArguments,
  type CacheForService,
} from "@byeolnaerim/typed-rx-http";
Next.js adapter (เลือก)
ts
import {
  callApiSsrCache,
  redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";

ในโครงการที่ไม่ใช้ Next.js อย่านำเข้า /next.

RSocket adapter (เลือก)

ติดตั้ง peer package เฉพาะในโครงการที่ใช้ RSocket.

bash
npm i rsocket-core rsocket-types rsocket-websocket-client
ts
import { createRSocketApi } from "@byeolnaerim/typed-rx-http/rsocket";

const api = createRSocketApi("ws://localhost:7000/rsocket", jwt);

api.mono<User>("user.findById", { id: 1 }).subscribe((user) => {
  console.log(user);
});

api.stream<Notification>("notification.stream").subscribe((notification) => {
  console.log(notification);
});

ในโครงการที่ไม่ใช้ RSocket อย่านำเข้า /rsocket.

วิธีการใช้ Core

1) เตรียมประเภท Paths (ปกติคือ OpenAPI paths)

Paths ของ createHttpClient<Paths>() แสดงถึงประเภท "สเปคคำขอ (route)" ในเอกสารจะเรียกว่า paths ตามธรรมเนียม แต่ไม่จำเป็นต้องเป็น OpenAPI/Swagger หรือชื่อว่า paths.

อย่างไรก็ตาม core ใช้ข้อจำกัด OpenApiPathsLike ภายใน ดังนั้น Paths จะต้องมีลักษณะคล้ายกับ OpenAPI paths ตามด้านล่าง.

  • คีย์ระดับสูงสุด: สตริงเส้นทาง URL (เช่น "/users/{id}")
  • คีย์ระดับล่าง: วิธีการ HTTP (get/post/put/delete/patch …)
  • ภายในแต่ละวิธีจะมีฟิลด์ parameters.query/path/header/cookie, requestBody, responses (หรือ never)

core จะอ้างอิงฟิลด์ด้านบนเพื่อสร้างประเภท ServiceArguments.

  • url: keyof Paths
  • method: keyof Paths[url]
  • queryString: parameters.query
  • pathVariable: parameters.path
  • body: requestBody

ตัวอย่าง: ผลลัพธ์ของ openapi-typescript มักมีรูปแบบดังต่อไปนี้ (บางส่วนย่อ)

ts
export interface paths {
  "/test/get-hello-world": {
    parameters: {
      query?: never;
      header?: never;
      path?: never;
      cookie?: never;
    };
    get: {
      parameters: {
        query?: never;
        header?: never;
        path?: never;
        cookie?: never;
      };
      requestBody?: never;
      responses: {
        200: {
          content: {
            "application/json": components["schemas"]["TestResponse"];
          };
        };
      };
    };
  };
}
ts
// aliasing: OpenAPI `paths` -> `Paths`
import type { paths as Paths } from "./@types/ApiTypes";

2) สร้าง HeaderStore

HeaderStore เป็น in-memory store ที่ง่ายสำหรับการจัดการ header พื้นฐานใน CSR

ts
import { createHeaderStore } from "@byeolnaerim/typed-rx-http";

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

3) สร้าง HTTP client

  • headerStore เป็นตัวเลือก แต่แนะนำให้ใส่หากต้องการใช้ header/การรับรองเซสชันพื้นฐานใน CSR
  • headersProvider ใช้เมื่อจำเป็นต้องคำนวณ header สำหรับแต่ละคำขอ เช่น SSR/มัลติเทนแนนท์
ts
import { createHttpClient } from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

const apiUrl = process.env.NEXT_PUBLIC_API_URL!;

export const client = createHttpClient<Paths>({
  baseUrl: apiUrl,
  headerStore,
});

4) เรียก API (ประเภทปลอดภัย)

ประเภทคำขอ (url/method/pathVariable/queryString/body) จะถูกกำหนดจากประเภทที่ผู้ใช้ฉีดเข้าไปใน createHttpClient<Paths>() (ตามธรรมเนียมคือ OpenAPI paths) ประเภทการตอบกลับจะถูกเลือกโดยผู้เรียกใน callApi<R>() โดยใช้ R ที่เป็นเจนเนอริก (คอร์จะไม่อนุมานจาก responses โดยอัตโนมัติ)

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

การห่อหุ้มการตอบกลับ (ResponseWrapper) — ตัวเลือก

ไลบรารีนี้ไม่บังคับให้มีการห่อหุ้มการตอบกลับ สามารถเลือกประเภทการตอบกลับแบบเจนเนอริกตาม API ได้

การตอบกลับที่ห่อหุ้ม
ts
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };

export const bidCreate = (body: unknown) => {
  return client.callApi<ResponseWrapper<{ requestId: string }>>({
    url: "/api/bid/create/bid/request",
    method: "post",
    body,
  });
};
การตอบกลับที่ไม่มีการห่อหุ้ม
ts
export const bidCreate = (body: unknown) => {
  return client.callApi<{ requestId: string }>({
    url: "/api/bid/create/bid/request",
    method: "post",
    body,
  });
};

การสตรีม (NDJSON)

เมื่อเซิร์ฟเวอร์ส่ง NDJSON (JSON หนึ่งตัวต่อหนึ่งบรรทัด) ให้ใช้ callApiStream หากไม่มี Accept header จะตั้งค่าเป็น application/x-ndjson โดยค่าเริ่มต้น

ts
export const streamEvents = () => {
  return client.callApiStream<{ eventName: string; content: unknown }>({
    url: "/api/event/stream",
    method: "get",
    // headers: { Accept: "application/x-ndjson" }, // 선택
  });
};

CSR แคช (แคชของไคลเอนต์)

ฟังก์ชันที่ให้โดย createCsrCache<CacheName>():

  • callApiCsrCache(callApiFn, serviceArgs, cacheOptions)
  • removeCsrCache(cacheName) — รองรับชื่อแคชประเภท + สตริงทั้งหมด
ts
import { createCsrCache } from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

type CacheNames = "adminList" | "profile";

const csrCache = createCsrCache<CacheNames>();

export const callApiClientCache = <R>(
  args: ServiceArguments<Paths, any, any, R>,
  cache: { cacheName: CacheNames; cacheTime: number; cacheSize?: number },
) => csrCache.callApiCsrCache(client.callApi, args as any, cache as any);

// 무효화
csrCache.removeCsrCache("adminList"); // 타입
csrCache.removeCsrCache("any-prefix"); // 문자열

ปลั๊กอินการรับรองเซสชัน (ตัวเลือก)

createSessionAuth แยกตรรกะการรับรองเซสชันออกจากคอร์และสามารถเพิ่มหรือลบได้ตามต้องการ

การทำงาน:

  • เก็บ Authorization ไว้ใน headerStore
  • ซิงโครไนซ์โทเค็นด้วย ensureToken$() (/api/auth/token)
  • เมื่อเกิด 401 ให้ลองรีเฟรช 1 ครั้ง (/api/auth/token/refresh) แล้วลองคำขอเดิมอีกครั้ง
  • เมื่อรีเฟรชล้มเหลวให้ logout (/api/auth/logout) และส่งต่อข้อผิดพลาด
  • การเปลี่ยนแปลงสถานะการเข้าสู่ระบบจะถูกจัดการจากภายนอกด้วย callback onLoginChange
ts
import { createSessionAuth } from "@byeolnaerim/typed-rx-http";

const auth = createSessionAuth({
  headerStore,
  onLoginChange: (loggedIn) => {
    // 예: setLogin(loggedIn) 같은 외부 상태 업데이트(선택)
  },
  // tokenUrl / refreshUrl / logoutUrl 커스터마이징 가능
});

export const secureCall = () => {
  return client
    .callApi<{ ok: true }>({ url: "/api/secure", method: "get" })
    .pipe(auth.withSessionAuth());
};

หากต้องการเพียงซิงโครไนซ์โทเค็นโดยไม่ต้องรีเฟรช/ลองใหม่:

ts
return client.callApi(...).pipe(auth.withEnsureToken());

การจัดการข้อผิดพลาด

หากไม่ใช่ 2xx จะโยน HttpResponseError (รวมถึง status, response, args, data)

ความเข้ากันได้กับเลกาซี: หากร่างข้อผิดพลาดมีรูปแบบ { resultType: ... } จะโยนวัตถุที่ตรงนั้นโดยตรง

ts
import { isHttpResponseError } from "@byeolnaerim/typed-rx-http";

client.callApi(...).subscribe({
  error: (e) => {
    if (isHttpResponseError(e)) {
      console.log(e.status, e.data);
    }
  },
});

Next.js อะแดปเตอร์ (/next)

redirectToUnauthorizedOnServer401

redirectToUnauthorizedOnServer401 เป็นการดำเนินการพื้นฐาน (ฟังก์ชันสะดวก) ที่ทำการ redirect เมื่อเกิด 401 ในสภาพแวดล้อม SSR ของ Next.js (App Router)

ts
import { redirectToUnauthorizedOnServer401 } from "@byeolnaerim/typed-rx-http/next";

กฎการทำงาน (คงที่):

  • เป้าหมายการ redirect: /unauthorized
  • queryString: redirect_uri=<หน้าปัจจุบัน> + logout=true
  • หน้าปัจจุบันจะอ่านจาก header x-page-url (ถ้าไม่มีจะใช้ /)

หมายความว่าให้ใช้กฎเส้นทาง/query ข้างต้นตามที่ตรงกับโปรเจกต์เท่านั้น หากเส้นทางแตกต่างหรือกฎ query แตกต่าง ให้สร้าง onServer401 โดยตรงและฉีดเข้าไปตามที่ต้องการ.

ts
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export const onServer401 = async () => {
  const h = await headers();
  const pageUrl = h.get("x-page-url") || "/";
  redirect(`/login?next=${encodeURIComponent(pageUrl)}`);
};
callApiSsrCache

นี่คือผู้ช่วยแคช SSR ที่ใช้ next/cache(unstable_cache) ของ Next.

  • GET + cacheTime > 0 → force-cache + revalidate
  • อื่น ๆ → no-store
  • ฉีด Cookie / Authorization ตามคำขอด้วย headersProvider
  • เมื่อเกิด 401 หากมี onServer401 จะถูกเรียกใช้งาน (ปกติคือ redirect())
ts
import type { paths as Paths } from "./@types/ApiTypes";
import { firstValueFrom } from "rxjs";
import {
  callApiSsrCache,
  redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";

const apiUrl = process.env.NEXT_PUBLIC_API_URL!;

const headersProvider = async () => {
  const { cookies } = await import("next/headers");
  const cookieStore = await cookies();

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

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

  return {
    ...(cookieHeader ? { Cookie: cookieHeader } : {}),
    ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
  };
};

const obs$ = callApiSsrCache<Paths, { ok: true }, "adminList">({
  baseUrl: apiUrl,
  serviceArguments: { url: "/api/secure", method: "get" },
  cacheForService: { cacheName: "adminList", cacheTime: 10_000 },
  headersProvider,
  onServer401: redirectToUnauthorizedOnServer401,
});

const res = await firstValueFrom(obs$);

ตัวอย่างการรวม Next.js: โค้ดทั้งหมดของโปรเจกต์ rxjsHttpService/commonService

ไฟล์ rxjsHttpService.ts ด้านล่างนี้คือ HTTP adapter สาธารณะของโปรเจกต์ที่อ้างถึง commonServiceFile ในตัวอย่าง generator ต่อไปนี้ ไม่ใช่ไฟล์ที่สร้างโดยไลบรารี แต่โปรเจกต์จัดการโดยตรง และส่งออก core client + session auth + CSR cache + SSR cache helper ในที่เดียว นี่คือโค้ดทั้งหมด.

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

เอกสารอ้างอิง API (core)

createHttpClient<Paths>(options)

ส่งคืน:

  • callApi<R>(args): Observable<R>
  • callApiStream<RChunk>(args): Observable<RChunk>
  • uploadFile({ file, url, ifNoneMatch?, headers? }): Observable<Response>
  • createSSEObservable<R>(args): Observable<R>

ตัวเลือก:

  • baseUrl: string
  • headerStore?: HeaderStore
  • headersProvider?: () => Record<string, string> | Promise<Record<string, string>>
  • dropAuthWhenCacheControl?: boolean (ค่าเริ่มต้น: true)
  • onServer401?: () => void | Promise<void>
createHeaderStore(initial?)

get(), set(), merge(), remove(), clear()

createCsrCache<CacheName>()
  • callApiCsrCache(callApiFn, serviceArgs, cacheForService)
  • removeCsrCache(cacheName) (ประเภท + สตริง)
createSessionAuth(options)
  • withSessionAuth(), withEnsureToken()
  • ensureToken$(), refreshToken$(), logout$()

ข้อกำหนดในการทำงาน

  • ใช้ fetch / Response API (rxjs/fetch)
  • การสตรีม (NDJSON) ต้องการ ReadableStream + TextDecoder
  • SSE ต้องการ EventSource

ส่วนใหญ่จะมีให้ในเบราว์เซอร์ล่าสุดและ runtime ของ Next.js อาจต้องใช้ polyfill ใน runtime ของ Node ที่กำหนดเอง.

Auto Node Script: การสร้างโค้ด OpenAPI/Swagger (ตัวเลือก)

แพ็คเกจนี้ให้สคริปต์ Node ที่สร้างโค้ดประเภท/บริการจาก OpenAPI/Swagger JSON แยกต่างหากจาก HTTP client runtime สคริปต์นี้เป็นฟังก์ชันเสริม.

ผู้ใช้ทั่วไปของ @byeolnaerim/typed-rx-http, /next, /rsocket ไม่จำเป็นต้องเรียกใช้สคริปต์นี้ และไม่จำเป็นต้องติดตั้ง openapi-typescript.

ts
import {
  connectToSwaggerEventStream,
  generateSwaggerFromHttp,
  generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";
การแยกการพึ่งพา

การสร้างประเภท OpenAPI ต้องการ openapi-typescript CLI แต่แพ็คเกจนี้ไม่ใส่ openapi-typescript ไว้ใน dependencies ทั่วไป.

ไลบรารี @byeolnaerim/typed-rx-http ใช้ TypeScript 6.0.3 ใน devDependencies.typescript เอง. typed-rx-http, /next, /rsocket entry points และการสร้างไลบรารีจะรักษามาตรฐาน TypeScript 6.0.3 นี้.

อย่างไรก็ตาม openapi-typescript อาจต้องการเวอร์ชัน TypeScript 5.x เฉพาะ ดังนั้น OpenAPI auto node script จะทำงานในสภาพแวดล้อมชั่วคราวที่แยกต่างหากโดยใช้ openapi-typescript และ [email protected] สภาพแวดล้อมชั่วคราวนี้จะไม่เปลี่ยนแปลง devDependencies.typescript 6.0.3 ของไลบรารี และจะไม่ใช้เวอร์ชัน typescript หรือ openapi-typescript ที่ติดตั้งในโปรเจกต์ของผู้ใช้.

ค่าเริ่มต้นคือการสร้างและเรียกใช้คำสั่งด้านล่างในช่วงเวลาที่เรียกใช้ auto script.

bash
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts

ดังนั้นวิธีการใช้งานจึงไม่เปลี่ยนแปลง ผู้ใช้สามารถเรียกใช้ auto node script ได้ตามปกติ และจะใช้สภาพแวดล้อม TypeScript 5.9.3 ที่แยกต่างหากในขั้นตอนการสร้างประเภท OpenAPI ผู้ใช้ที่ไม่ใช้ auto script จะไม่ถูกผูกพันกับ openapi-typescript หรือ TypeScript 5.9.3.

หากจำเป็น สามารถกำหนดคำสั่งโดยตรงด้วย openApiTypescriptCommand ได้

ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
  openApiTypescriptCommand:
    "npx -y -p [email protected] -p [email protected] openapi-typescript ./swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts",
});

หรือสามารถเปลี่ยนแค่เวอร์ชันของแพ็กเกจที่กำหนดคำสั่งพื้นฐานได้

ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
  openApiTypescriptPackage: "[email protected]",
  openApiTypescriptTypescriptPackage: "[email protected]",
});
ไฟล์ที่สร้างขึ้น

การตั้งค่าเริ่มต้นจะสร้างไฟล์ด้านล่างนี้

text
swagger.json
src/handler/service/@types/ApiTypes.d.ts
src/handler/service/auto/*Service.ts
src/handler/service/@types/auto/*Types.ts
src/handler/service/apiUnionArrays.ts

apiUnionArrays.ts จะสร้าง enum ของ OpenAPI schema รวมถึง query, path, header, cookie parameter enum เป็นอาร์เรย์คงที่ด้วย อาร์เรย์ query parameter ของ items.enum ก็จะถูกจัดการด้วยเช่นกัน

การตรวจสอบ EventStream
ts
connectToSwaggerEventStream({
  hostname: "localhost",
  port: 8788,
  path: "/oauth2/for-local/get-swagger",
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  commonServiceFile: "./src/handler/service/rxjsHttpService.ts",
});
HTTP 1 ครั้ง
ts
await generateSwaggerFromHttp({
  hostname: "localhost",
  port: 8788,
  path: "/oauth2/for-local/get-swagger",
});
สร้างจากไฟล์ท้องถิ่น
ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
});

ตัวอย่างการรวมโปรเจกต์ที่มีอยู่: WebFlux + Swagger สร้างอัตโนมัติ

ตั้งแต่ที่นี่เป็นต้นไปเป็นตัวอย่างการรวมโปรเจกต์ที่ใช้ Swagger ด้านหลังและบริการสร้างอัตโนมัติร่วมกัน ฟังก์ชันการใช้งานหลักด้านบนและฟังก์ชันเลือกสามารถใช้ได้ด้วย typed-rx-http เอง และกระบวนการด้านล่างจะใช้ในโปรเจกต์ที่ใช้การสร้างบริการอัตโนมัติจาก Swagger

1. เขียน REST endpoint ที่ Backend

เขียนโค้ด Backend ตามปกติ ในตัวอย่างนี้จะรับชื่อเป็น path variable และข้อความเป็น query parameter Monoและตอบกลับ

TypedRxHttpExampleRouter.java
java
@Bean
public RouterFunction<ServerResponse> typedRxHttpExample(
  TypedRxHttpExampleHandler handler
) {
  return route()
    .GET("/typed-rx-http/example/{name}", handler::getExample)
    .build();
}

2. สร้างบริการ Front จาก Swagger

รับจาก Backend แต่ผมไม่ต้องการให้ typed-rx-http ถูกใช้เฉพาะใน WebFlux เท่านั้น แม้ว่า backend จะไม่สร้าง Swagger โดยตรง แต่หากมี TypeScript ที่ตรงตาม OpenAPI สเปค ก็สามารถลดการเขียน URL สตริงและ request type ด้วยมือได้ RSocket client ก็ถูกสร้างขึ้นตามเหตุผลเดียวกันโดยอิงจากเอกสาร AsyncAPIเพื่อสร้างไฟล์ประเภทและบริการ การทำงานนี้สามารถเรียกใช้ได้ครั้งเดียวเมื่อรันเซิร์ฟเวอร์พัฒนา หรือเชื่อมต่อกับสคริปต์ watch

generateSwagger.cjs
js
const {
  generateSwaggerFromHttp,
} = require("@byeolnaerim/typed-rx-http/auto-node-script/openapi");

generateSwaggerFromHttp({
  hostname: "localhost",
  port: 8123,
  path: "/for-local/get-swagger",
  swaggerFile: "./swagger.json",
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  typesFile: "./src/handler/service/@types/ApiTypes.d.ts",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

3. ส่งคำขอด้วยฟังก์ชันที่สร้างขึ้น

สามารถเปลี่ยนแปลงได้ด้านล่าง โค้ด Frontและ โค้ด Backendสามารถเปลี่ยนแปลงได้ ผลลัพธ์เมื่อกดจะเปิดหน้าจอการทำงานทางด้านขวา เปลี่ยนค่าแล้วกดปุ่มคำขอ

TypedRxHttpRequestExample.tsx
tsx
import { byName } from "@handler/service/auto/TypedRxHttpExampleService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  byName({
    path: {
      name: name.trim(),
    },
    params: {
      message: message.trim() || undefined,
    },
  }),
);

setResponse(response);