OpenAPI & Generator Layanan

Skrip Node opsional yang disediakan terpisah dari klien HTTP inti. Hanya digunakan saat menghasilkan tipe TypeScript dan file layanan dari JSON OpenAPI/Swagger.


Fitur opsional: Tidak diperlukan untuk penggunaan inti

Pengguna umum @byeolnaerim/typed-rx-http, /next, atau /rsocket tidak perlu menjalankan skrip ini dan tidak perlu menginstal openapi-typescript di proyek.

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

Isolasi ketergantungan openapi-typescript

Pembuatan tipe OpenAPI memerlukan openapi-typescript CLI, tetapi paket tidak dimasukkan ke dalam dependensi umum. devDependencies.typescript dari typed-rx-http mempertahankan standar TypeScript 6.0.3, dan hanya menggunakan lingkungan npx sementara di skrip node auto OpenAPI.

Pada tahap pembuatan OpenAPI dasar, openapi-typescript dan [email protected] dijalankan bersamaan, sehingga tidak mengubah atau menggunakan versi TypeScript/openapi-typescript yang diinstal di proyek pengguna.

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

Jika perlu, seluruh perintah dapat dikunci dengan 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",
});

Atau Anda juga dapat mengubah hanya versi paket yang membentuk perintah dasar.

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

Pertama: Siapkan file proyek yang akan ditunjuk oleh commonServiceFile

Sebelum commonServiceFile muncul dalam contoh generator, Anda harus memahami file ini terlebih dahulu. commonServiceFile adalah jalur file yang dimiliki proyek yang mengimpor fungsi HTTP umum dari layanan yang dihasilkan, bukan opsi untuk membuat file. Nama file dapat ditentukan secara bebas seperti rxjsHttpService.ts, commonService.ts, dll.

Kode lengkap minimum

File di bawah ini membuat createHttpClient sekali di proyek dan mengekspor callApi dan callApiStream agar layanan yang dihasilkan dapat digunakan kembali. Jika tidak memerlukan cache atau otentikasi sesi, Anda dapat memulai dari struktur ini.

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;
Kode lengkap termasuk cache Session/CSR/SSR

Jika layanan yang dihasilkan menggunakan callApiClientCache atau callApiServerCache, atau jika Anda ingin menangani header dan otentikasi sesi secara umum di Next.js, perluasan dilakukan dengan bentuk lengkap berikut.

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

Metode eksekusi

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

File yang dihasilkan

Pengaturan default menghasilkan file-file di bawah ini.

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.tsmembuat array konstanta readonly untuk enum parameter query, path, header, dan cookie, serta menangani items.enum dari parameter query array.


Opsional: Menghubungkan Swagger backend dengan commonService proyek

Mulai dari sini adalah contoh menghubungkan generator ke proyek nyata. WebFlux/webflux-fe-dev-assistant hanyalah salah satu cara untuk menyediakan Swagger dan bukan ketergantungan yang wajib.

Siapkan dokumen Swagger

Swagger bisa berasal dari Springdoc, alat OpenAPI lainnya, atau file yang ditulis secara manual. Jika menggunakan endpoint fungsional WebFlux, webflux-fe-dev-assistantAnda juga dapat menggunakan cara untuk menyediakan dokumen Swagger.

Contoh pembuatan dengan menyatakan opsi di proyek

Anda dapat menyatakan endpoint backend, swagger.json yang akan disimpan, jalur output tipe/layanan, dan commonServiceFile sesuai dengan struktur proyek.

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

Dalam lingkungan di mana dokumen tidak dapat diterima melalui HTTP, Anda dapat menggunakan generateSwaggerFromFiledengan opsi output yang sama.

commonServiceFile adalah jalur target import

commonServiceFileadalah jalur file yang dimiliki proyek yang mengimpor callApi, callApiStream, dan pembungkus cache dari layanan yang dihasilkan. Ini bukan opsi untuk membuat atau menimpa file ini.

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

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

Contoh di mana hasil pembuatan ditempatkan di proyek

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

File umum yang ditentukan dengan commonServiceFile seperti rxjsHttpService.ts dikelola langsung oleh proyek. Hasil di bawah auto dan @types/auto akan dibuat ulang jika dokumen berubah.

Aturan nama file dan fungsi

Nama file layanan ditentukan oleh dua segmen pertama dari URL, dan nama fungsi ditentukan oleh segmen ketiga dan seterusnya. Variabel path disertakan dalam nama fungsi dalam bentuk 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 })

Argumen pemanggilan fungsi pembuatan

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

Kunci parameter query dari layanan yang dihasilkan adalah params, variabel path adalah path, dan body permintaan adalah body. Di dalam fungsi pembuatan, masing-masing akan diubah menjadi ServiceArguments dari queryString, pathVariable, dan body.

© 2026 Byeolnaerim. Semua hak dilindungi.PengenalanKebijakan Penanganan Data Pribadi