OpenAPI & Service Generator

Optionales Node-Skript, das zusätzlich zum Core HTTP-Client bereitgestellt wird. Wird nur verwendet, um TypeScript-Typen und Dienstdateien aus OpenAPI/Swagger JSON zu generieren.


Optionale Funktion: Nicht erforderlich für die Verwendung des Cores

Allgemeine Benutzer von @byeolnaerim/typed-rx-http, /next oder /rsocket müssen dieses Skript nicht ausführen und müssen openapi-typescript nicht im Projekt installieren.

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

Isolierung der openapi-typescript-Abhängigkeit

Für die Erstellung von OpenAPI-Typen ist die openapi-typescript CLI erforderlich, aber das Paket wird nicht in die allgemeinen Abhängigkeiten aufgenommen. Die devDependencies.typescript von typed-rx-http halten sich an den Standard von TypeScript 6.0.3 und verwenden nur eine separate npx-Temporärumgebung im OpenAPI-Auto-Node-Skript.

Im grundlegenden OpenAPI-Generierungsprozess werden openapi-typescript und [email protected] zusammen ausgeführt, sodass die in Ihrem Projekt installierte TypeScript/openapi-typescript-Version nicht geändert oder nicht verwendet wird.

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

Wenn nötig, kann der gesamte Befehl auf openApiTypescriptCommand festgelegt werden.

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

Alternativ können Sie nur die Paketversion ändern, die den Standardbefehl konfiguriert.

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

Zuerst: Bereiten Sie die Projektdatei vor, auf die commonServiceFile verweist.

Bevor commonServiceFile im Generatorbeispiel erscheint, müssen Sie diese Datei verstehen. commonServiceFile ist der Pfad zur Projektbesitzdatei, die die gemeinsamen HTTP-Funktionen des generierten Dienstes importiert, nicht eine Option, die eine Datei erstellt. Der Dateiname kann frei gewählt werden, z. B. rxjsHttpService.ts, commonService.ts.

Minimale vollständige Codeversion

Die folgende Datei erstellt einmal den createHttpClient im Projekt und exportiert callApi und callApiStream zur Wiederverwendung durch den generierten Dienst. Wenn kein Cache oder keine Sitzungsauthentifizierung erforderlich ist, können Sie mit dieser Struktur beginnen.

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;
Vollständiger Code einschließlich Session/CSR/SSR-Cache

Wenn der generierte Dienst callApiClientCache oder callApiServerCache verwenden soll oder wenn Next.js die header und Sitzungsauthentifizierung pro Anfrage gemeinsam verarbeiten soll, erweitern Sie es in dieser vollständigen Form.

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

Ausführungsweise

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

Generierte Dateien

Die Standardeinstellungen erzeugen die folgenden Dateien.

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.tserzeugt nicht nur OpenAPI-Schema-Enums, sondern auch readonly-Konstantenarrays für die Enums von Abfrage-, Pfad-, Header- und Cookie-Parametern und verarbeitet auch die items.enum von Abfrageparametern.


Optional: Verbindung zwischen Backend-Swagger und Projekt-commonService

Hier beginnt ein Beispiel, wie der Generator mit einem echten Projekt verbunden wird. WebFlux/webflux-fe-dev-assistant ist nur eine Möglichkeit, Swagger bereitzustellen, und keine zwingende Abhängigkeit.

Vorbereitung der Swagger-Dokumentation

Swagger kann von Springdoc, anderen OpenAPI-Tools oder von Ihnen selbst erstellten Dateien stammen. Wenn Sie WebFlux-Funktionsendpunkte verwenden, können Sie webflux-fe-dev-assistantEs kann auch eine Methode verwendet werden, um Swagger-Dokumentation bereitzustellen.

Beispiel für die Generierung mit expliziten Optionen im Projekt

Sie können den Backend-Endpunkt, den zu speichernden swagger.json, den Typ-/Service-Ausgabepfad und commonServiceFile entsprechend der Projektstruktur angeben.

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

In Umgebungen, in denen Dokumente nicht über HTTP empfangen werden können, können Sie die gleichen Ausgabeoptionen verwenden. Swagger aus Datei generierenkann verwendet werden.

commonServiceFile ist der Importzielpfad

commonServiceFileist der Pfad zur Projektbesitzdatei, die die generierte Dienstleistung callApi, callApiStream und den Cache-Wrapper importiert. Der Generator hat keine Option, diese Datei zu erstellen oder zu überschreiben.

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

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

Beispiel für die Platzierung der Generierungsergebnisse im Projekt

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

Eine gemeinsame Datei, die als commonServiceFile wie rxjsHttpService.ts angegeben ist, wird direkt vom Projekt verwaltet. Die Ergebnisse unter auto und @types/auto werden neu generiert, wenn sich das Dokument ändert.

Dateinamen- und Funktionsnamensregeln

Der Name der Dienstdatei wird aus den ersten beiden Segmenten der URL gebildet, der Funktionsname aus dem dritten und den folgenden Segmenten. Pfadvariablen werden in der Form By + PascalCase in den Funktionsnamen aufgenommen.

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

Argumente für den Aufruf der Generierungsfunktion

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

Der Schlüssel für die Abfrageparameter des generierten Dienstes ist params, die Pfadvariable ist path, der Anfragekörper ist body. Innerhalb der Generierungsfunktion werden sie jeweils in die ServiceArguments von queryString, pathVariable und body umgewandelt.

© 2026 Byeolnaerim. Alle Rechte vorbehalten.EinführungDatenschutzrichtlinie