OpenAPI & Gerador de Serviço

Script Node opcional fornecido separadamente do cliente HTTP Core. Usado apenas ao gerar tipos TypeScript e arquivos de serviço a partir do JSON OpenAPI/Swagger.


Funcionalidade opcional: não necessária para uso do Core

Usuários comuns de @byeolnaerim/typed-rx-http, /next ou /rsocket não precisam executar este script e não precisam instalar openapi-typescript em seu projeto.

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

Isolamento da dependência openapi-typescript

A geração de tipos OpenAPI requer o CLI openapi-typescript, mas o pacote não o inclui nas dependências comuns. As devDependencies.typescript do typed-rx-http mantêm a versão 6.0.3 do TypeScript, e o script auto node OpenAPI usa um ambiente temporário npx separado.

Na fase básica de geração do OpenAPI, openapi-typescript e [email protected] são executados juntos, portanto, não altera ou usa a versão do TypeScript/openapi-typescript instalada no projeto do usuário.

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

Se necessário, o comando completo pode ser fixado como 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",
});

Ou você pode apenas alterar a versão do pacote que compõe o comando básico.

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

Primeiro: prepare o arquivo do projeto que commonServiceFile apontará.

Antes que commonServiceFile apareça no exemplo do gerador, você deve entender este arquivo primeiro. commonServiceFile é o caminho do arquivo que possui as funções HTTP comuns que o serviço gerado importará, não é uma opção que cria arquivos. O nome do arquivo pode ser escolhido livremente, como rxjsHttpService.ts, commonService.ts, etc.

Código completo mínimo funcional

O arquivo abaixo cria uma vez o createHttpClient no projeto e exporta callApi e callApiStream para reutilização pelo serviço gerado. Se cache ou autenticação de sessão não forem necessários, você pode começar com esta estrutura.

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;
Código completo incluindo cache de sessão/CSR/SSR

Se o serviço gerado usar callApiClientCache ou callApiServerCache, ou se você quiser tratar cabeçalhos por solicitação e autenticação de sessão de forma comum no Next.js, expanda para a forma completa a seguir.

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

Método de execução

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

Arquivos gerados

As configurações padrão geram os arquivos abaixo.

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.tsgera não apenas enums de schema OpenAPI, mas também arrays constantes readonly de enums de parâmetros de query, path, header e cookie, além de processar os items.enum de parâmetros de query array.


Opcional: conectar o Swagger do backend ao commonService do projeto

A partir daqui, um exemplo de como conectar o gerador ao projeto real. WebFlux/webflux-fe-dev-assistant é apenas uma das maneiras de fornecer Swagger, não é uma dependência obrigatória.

Preparar documentação Swagger

O Swagger pode ser do Springdoc, de outras ferramentas OpenAPI ou de arquivos escritos manualmente. Se você usar o endpoint funcional do WebFlux, webflux-fe-dev-assistantVocê também pode usar a forma de fornecer documentação Swagger.

Exemplo de geração especificando opções no projeto

Você pode especificar o endpoint do backend, o swagger.json a ser salvo, o caminho de saída de tipos/serviços e commonServiceFile de acordo com a estrutura do projeto.

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

Em ambientes onde não é possível receber documentos via HTTP, você pode usar junto com as mesmas opções de saída. generateSwaggerFromFilepara isso.

commonServiceFile é o caminho de importação

commonServiceFileé o caminho do arquivo que possui as funções callApi, callApiStream e o wrapper de cache que o serviço gerado importará. O gerador não é uma opção para criar ou sobrescrever este arquivo.

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

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

Exemplo de onde os resultados da geração são colocados no projeto

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

Um arquivo público designado como commonServiceFile, como rxjsHttpService.ts, é gerenciado diretamente pelo projeto. Os resultados abaixo de auto e @types/auto são regenerados quando o documento muda.

Regras de nome de arquivo e função

O nome do arquivo de serviço é determinado pelos dois primeiros segmentos da URL, e o nome da função é determinado a partir do terceiro segmento. Variáveis de caminho são incluídas no nome da função no formato 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 })

Argumentos de chamada da função de geraçã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 },
});

As chaves dos parâmetros de query do serviço gerado são params, as variáveis de caminho são path, e o corpo da solicitação é body. Dentro da função de geração, cada um é convertido em ServiceArguments de queryString, pathVariable e body.

© 2026 Byeolnaerim. Todos os direitos reservados.IntroduçãoPolítica de Privacidade