OpenAPI & Trình tạo Service

Script Node tùy chọn được cung cấp riêng biệt với Core HTTP client. Chỉ sử dụng khi tạo kiểu TypeScript và tệp dịch vụ từ OpenAPI/Swagger JSON.


Tính năng tùy chọn: Không cần thiết cho việc sử dụng Core

Người dùng thông thường của @byeolnaerim/typed-rx-http, /next hoặc /rsocket không cần phải chạy script này và không cần cài đặt openapi-typescript vào dự án.

ts
import {
  connectToSwaggerEventStream,
  generateSwaggerFromHttp,
  generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";

Cách ly phụ thuộc openapi-typescript

Việc tạo kiểu OpenAPI cần CLI openapi-typescript, nhưng gói không đưa vào dependencies thông thường. devDependencies.typescript của typed-rx-http duy trì tiêu chuẩn TypeScript 6.0.3, và chỉ sử dụng môi trường tạm thời npx riêng trong script node tự động OpenAPI.

Trong giai đoạn tạo OpenAPI cơ bản, openapi-typescript và [email protected] được chạy cùng nhau, vì vậy không thay đổi hoặc không sử dụng phiên bản TypeScript/openapi-typescript đã cài đặt trong dự án của người dùng.

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

Nếu cần, có thể cố định toàn bộ lệnh bằng 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",
});

Hoặc chỉ cần thay đổi phiên bản gói cấu thành lệnh cơ bản.

ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
  openApiTypescriptPackage: "[email protected]",
  openApiTypescriptTypescriptPackage: "[email protected]",
});

Trước tiên: Chuẩn bị tệp dự án mà commonServiceFile sẽ chỉ đến

Trước khi commonServiceFile xuất hiện trong ví dụ generator, bạn cần hiểu tệp này trước. commonServiceFile không phải là tùy chọn tạo tệp mà là đường dẫn đến tệp sở hữu dự án mà dịch vụ được tạo sẽ nhập các hàm HTTP chung. Tên tệp có thể được đặt tự do như rxjsHttpService.ts, commonService.ts, v.v.

Mã hoàn chỉnh kiểu tối thiểu

Tệp dưới đây tạo một lần createHttpClient trong dự án và export callApi và callApiStream để dịch vụ được tạo tái sử dụng. Nếu không cần bộ nhớ đệm hoặc xác thực phiên, bạn có thể bắt đầu từ cấu trúc này.

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;
Mã hoàn chỉnh bao gồm bộ nhớ đệm Session/CSR/SSR

Nếu dịch vụ được tạo sử dụng callApiClientCache hoặc callApiServerCache hoặc muốn xử lý chung header theo yêu cầu và xác thực phiên trong Next.js, hãy mở rộng theo hình thức toàn bộ sau.

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

Cách thực thi

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

Tệp được tạo ra

Cấu hình mặc định sẽ tạo ra các tệp dưới đây.

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.tsTạo không chỉ enum schema OpenAPI mà còn cả mảng hằng readonly cho enum tham số query, path, header, cookie, và cũng xử lý items.enum của tham số query.


Tùy chọn: Kết nối Swagger backend với commonService của dự án

Từ đây là ví dụ kết nối generator với dự án thực tế. WebFlux/webflux-fe-dev-assistant chỉ là một trong những cách cung cấp Swagger và không phải là phụ thuộc bắt buộc.

Chuẩn bị tài liệu Swagger

Swagger có thể là Springdoc, các công cụ OpenAPI khác hoặc tệp tự viết. Nếu bạn sử dụng endpoint functional WebFlux, webflux-fe-dev-assistantCũng có thể sử dụng cách cung cấp tài liệu Swagger.

Ví dụ về việc tạo ra với tùy chọn được chỉ định trong dự án

Có thể chỉ định endpoint backend, swagger.json sẽ lưu, đường dẫn xuất kiểu/dịch vụ và commonServiceFile theo cấu trúc dự án.

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",
  unionArraysFile: "./src/handler/service/apiUnionArrays.ts",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

Trong môi trường không thể nhận tài liệu qua HTTP, có thể sử dụng cùng tùy chọn xuất như vậy. generateSwaggerFromFileCó thể sử dụng.

commonServiceFile là đường dẫn đến tệp nhập

commonServiceFileLà đường dẫn đến tệp sở hữu dự án mà dịch vụ được tạo sẽ nhập callApi, callApiStream và wrapper bộ nhớ đệm. Generator không có tùy chọn tạo hoặc ghi đè tệp này.

ApiBusinessService.ts
ts
import { callApi } from "../rxjsHttpService";

export const workplacesSearch = ({ params }) => {
  return callApi<SearchWorkplacesResponse>({
    url: "/api/business/workplaces/search",
    method: "get",
    queryString: params,
  });
};

Ví dụ về kết quả tạo ra trong dự án

text
src/handler/service/
├─ rxjsHttpService.ts        # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│  ├─ ApiBusinessService.ts
│  ├─ ApiOrdersService.ts
│  └─ Oauth2LoginService.ts
└─ @types/
   ├─ ApiTypes.d.ts
   └─ auto/
      ├─ ApiBusinessTypes.ts
      ├─ ApiOrdersTypes.ts
      └─ Oauth2LoginTypes.ts

Tệp công cộng được chỉ định bằng commonServiceFile như rxjsHttpService.ts sẽ được dự án quản lý trực tiếp. Kết quả dưới auto và @types/auto sẽ được tạo lại khi tài liệu thay đổi.

Quy tắc tên tệp và tên hàm

Tên tệp dịch vụ được quyết định bởi hai segment đầu tiên của URL, tên hàm được quyết định từ segment thứ ba trở đi. Biến đường dẫn được bao gồm trong tên hàm theo dạng By + PascalCase.

GET /api/business/workplaces/search

ApiBusinessService.ts → workplacesSearch({ params })

GET /api/business/workplaces/{id}

ApiBusinessService.ts → workplacesById({ path })

GET /api/orders/history/search

ApiOrdersService.ts → historySearch({ params })

POST /oauth2/login

Oauth2LoginService.ts → post({ body })

Tham số gọi hàm tạo

GeneratedServiceUsage.tsx
tsx
// path variable
workplacesById({
  path: { id: "workplace-1" },
});

// query parameter
workplacesSearch({
  params: { q: "auction", size: 20 },
});

// JSON request body
post({
  body: { username, password },
});

Khóa tham số query của dịch vụ được tạo là params, biến đường dẫn là path, và body yêu cầu là body. Trong hàm tạo, chúng sẽ được chuyển đổi thành ServiceArguments cho queryString, pathVariable, body.

© 2026 Byeolnaerim. Bảo lưu mọi quyền.Giới thiệuChính sách xử lý thông tin cá nhân