Inicio Rápido

Cliente HTTP seguro basado en RxJS para TypeScript + (opcional) adaptador Next.js/RSocket.


Características clave

  • Inyección de tipos de rutas estilo OpenAPI obtenidos de esquemas derivados de Swagger/OpenAPI / AsyncAPI / contratos personalizados, etc. (por convención, paths) para asegurar la estabilidad del tipo de solicitud.
  • Todas las API devuelven RxJS Observable.
  • El núcleo es independiente del marco (no depende de Next.js).
  • Las funciones específicas de Next.js se separan en el punto de entrada @byeolnaerim/typed-rx-http/next → Next.js solo es necesario al importar /next.
  • Las funciones específicas de RSocket se separan en el punto de entrada @byeolnaerim/typed-rx-http/rsocket → RSocket solo es necesario al importar /rsocket.

Instalación

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

Punto de entrada.

Core (independiente del framework)
ts
import {
  createHttpClient,
  createHeaderStore,
  createCsrCache,
  createSessionAuth,
  HttpResponseError,
  isHttpResponseError,
  type ServiceArguments,
  type CacheForService,
} from "@byeolnaerim/typed-rx-http";
Adaptador de Next.js (opcional).
ts
import {
  callApiSsrCache,
  redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";

En proyectos que no utilizan Next.js, no importe /next.

Adaptador de RSocket (opcional).

Instale el paquete peer solo en proyectos que utilizan 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);
});

En proyectos que no utilizan RSocket, no importe /rsocket.

Uso del Core.

1) Preparar tipo Paths (generalmente OpenAPI paths).

Paths de createHttpClient<Paths>() representan el tipo que expresa la "especificación de solicitud (ruta)". En la documentación, se llama convencionalmente paths, pero no es necesario que sea OpenAPI/Swagger, ni que se llame paths.

Sin embargo, el núcleo utiliza internamente la restricción OpenApiPathsLike, por lo que Paths debe tener una forma similar a los paths de OpenAPI como se muestra a continuación.

  • Clave superior: cadena de ruta URL (por ejemplo, "/users/{id}").
  • Clave inferior: método HTTP (get/post/put/delete/patch …).
  • Dentro de cada método, existen campos como parameters.query/path/header/cookie, requestBody, responses (o nunca).

El núcleo se refiere principalmente a los siguientes campos en esta estructura para construir el tipo de ServiceArguments.

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

Ej: la salida de openapi-typescript suele tener el siguiente formato (con algunas abreviaciones).

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) Crear HeaderStore

HeaderStore es un simple almacén en memoria para gestionar los encabezados básicos en CSR.

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

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

3) Crear cliente HTTP

  • headerStore es opcional, pero se recomienda incluirlo si se utilizan encabezados básicos/autenticación de sesión en CSR.
  • headersProvider se utiliza cuando se necesita calcular encabezados por solicitud, como en SSR/multitenencia.
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) Llamada a la API (tipo seguro)

El tipo de solicitud (url/método/pathVariable/queryString/cuerpo) se determina a partir del tipo inyectado por el usuario en createHttpClient<Paths>() (por convención, rutas de OpenAPI). El tipo de respuesta es elegido por el llamador como genérico R en callApi<R>() (el núcleo no infiere automáticamente de 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 },
  });
};

Envoltura de respuesta (ResponseWrapper) — opcional

Esta biblioteca no obliga a la envoltura de respuestas. Se puede elegir el formato de respuesta genérico por API.

Respuesta envuelta
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,
  });
};
Respuesta sin envolver
ts
export const bidCreate = (body: unknown) => {
  return client.callApi<{ requestId: string }>({
    url: "/api/bid/create/bid/request",
    method: "post",
    body,
  });
};

Streaming (NDJSON)

Cuando el servidor envía NDJSON (un JSON por línea), se utiliza callApiStream. Si no hay encabezado Accept, se establece application/x-ndjson como valor predeterminado.

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

Cache CSR (cache del cliente)

Funciones proporcionadas por createCsrCache<CacheName>:

  • callApiCsrCache(callApiFn, serviceArgs, cacheOptions)
  • removeCsrCache(cacheName) — admite tanto el nombre del cache como cadenas.
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"); // 문자열

Plugin de autenticación basado en sesión (opcional)

createSessionAuth separa la lógica de autenticación de sesión del núcleo, permitiendo agregarla o quitarla como opción.

Funcionamiento:

  • Mantener Authorization en headerStore
  • Sincronizar token con ensureToken$() (/api/auth/token)
  • Al ocurrir un 401, intenta refrescar una vez (/api/auth/token/refresh) y luego reintenta la solicitud original.
  • Si el refresh falla, realiza logout (/api/auth/logout) y pasa el error.
  • Los cambios en el estado de inicio de sesión se manejan externamente con el 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());
};

Si solo se necesita sincronizar el token sin refresh/retry:

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

Manejo de errores

Si no es 2xx, se lanza HttpResponseError (incluyendo status, response, args, data).

Compatibilidad heredada: si el cuerpo de error tiene la forma { resultType: ... }, se lanza ese objeto tal cual.

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

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

Adaptador de Next.js (/next)

redirectToUnauthorizedOnServer401

redirectToUnauthorizedOnServer401 es la implementación básica (función de conveniencia) que realiza un redireccionamiento cuando ocurre un 401 en el entorno SSR de Next.js (App Router).

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

Reglas de funcionamiento (fijas):

  • Destino de redirección: /unauthorized
  • queryString: redirect_uri=<página actual> + logout=true
  • La página actual se lee del encabezado x-page-url (si no existe, se usa /).

Es decir, use estas reglas de ruta/query solo si coinciden con su proyecto. Si la ruta es diferente o las reglas de query son distintas, implemente onServer401 directamente como se muestra a continuación y inyecte.

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

Este es un asistente de caché SSR basado en next/cache(unstable_cache) de Next.

  • GET + cacheTime > 0 → caché forzada + revalidar
  • De lo contrario → no-store
  • Inyección de Cookie / Authorization por solicitud a través de headersProvider
  • Si ocurre 401 y hay onServer401, se ejecuta (normalmente 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$);

Ejemplo de integración de Next.js: código completo de rxjsHttpService/commonService

El siguiente rxjsHttpService.ts es el adaptador HTTP común del proyecto que se refiere al commonServiceFile del ejemplo de generador posterior. No es un archivo generado por la biblioteca, sino que es gestionado directamente por el proyecto, exportando el cliente central + autenticación de sesión + caché CSR + asistente de caché SSR en un solo lugar. A continuación se muestra el código completo.

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

Referencia de API (núcleo)

createHttpClient<Paths>(opciones)

Retorna:

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

Opciones:

  • baseUrl: string
  • headerStore?: HeaderStore
  • headersProvider?: () => Record<string, string> | Promise<Record<string, string>>
  • dropAuthWhenCacheControl?: boolean (valor por defecto: true)
  • onServer401?: () => void | Promise<void>
createHeaderStore(initial?)

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

createCsrCache<CacheName>()
  • callApiCsrCache(callApiFn, serviceArgs, cacheForService)
  • removeCsrCache(cacheName) (tipo + cadena)
createSessionAuth(options)
  • withSessionAuth(), withEnsureToken()
  • ensureToken$(), refreshToken$(), logout$()

Requisitos de tiempo de ejecución

  • Uso de fetch / Response API (rxjs/fetch)
  • Streaming (NDJSON) requiere ReadableStream + TextDecoder
  • SSE requiere EventSource

Está disponible en la mayoría de los navegadores modernos y en el tiempo de ejecución de Next.js. Puede requerir un polyfill en un tiempo de ejecución de Node personalizado.

Auto Node Script: Generación de código OpenAPI/Swagger (opcional)

Este paquete proporciona, además del cliente HTTP en tiempo de ejecución, un script de Node que genera código de tipo/servicio a partir de JSON de OpenAPI/Swagger. Este script es una función opcional.

Los usuarios de @byeolnaerim/typed-rx-http, /next, /rsocket no necesitan ejecutar este script ni instalar openapi-typescript.

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

La generación de tipos OpenAPI requiere el CLI de openapi-typescript. Sin embargo, este paquete no incluye openapi-typescript en las dependencias generales.

La biblioteca @byeolnaerim/typed-rx-http utiliza TypeScript 6.0.3 en devDependencies.typescript. Los puntos de entrada de typed-rx-http, /next, /rsocket y la construcción de la biblioteca mantienen este estándar de TypeScript 6.0.3.

Sin embargo, openapi-typescript puede requerir una versión específica de TypeScript 5.x, por lo que el script de auto nodo de OpenAPI se ejecuta en un entorno temporal separado con openapi-typescript y [email protected]. Este entorno temporal no cambia el devDependencies.typescript 6.0.3 de la biblioteca y no utiliza la versión de typescript o openapi-typescript instalada en el proyecto del usuario.

El valor por defecto es generar y ejecutar el siguiente comando solo en el momento de la ejecución del script automático.

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

Por lo tanto, la forma de uso no cambia. Se puede llamar al script de nodo automático como antes, y solo se utiliza un entorno aislado de TypeScript 5.9.3 en la etapa de generación de tipos de OpenAPI. Los usuarios que no utilizan el script automático no están atados a openapi-typescript ni a TypeScript 5.9.3.

Si es necesario, puedes fijar el comando directamente con 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",
});

O puedes cambiar solo la versión del paquete que configura el comando por defecto.

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

La configuración predeterminada genera los siguientes archivos.

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 genera no solo enums del esquema OpenAPI, sino también enums de parámetros de consulta, ruta, encabezado y cookie como arreglos constantes. También maneja los items.enum de los parámetros de consulta en arreglo.

Monitoreo de 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",
});
Solicitud HTTP única
ts
await generateSwaggerFromHttp({
  hostname: "localhost",
  port: 8788,
  path: "/oauth2/for-local/get-swagger",
});
Generar desde un archivo local
ts
generateSwaggerFromFile({
  inputFile: "./swagger.json",
});

Ejemplo de integración de proyecto existente: WebFlux + generación automática de Swagger

A partir de aquí, es un ejemplo de integración de un proyecto que utiliza Swagger en el backend junto con el servicio de generación automática. Las funcionalidades de uso del Core y las opciones seleccionadas se pueden usar solo con typed-rx-http, y el flujo a continuación se aplica adicionalmente en proyectos que utilizan la generación automática de servicios basada en Swagger.

1. Escriba el endpoint REST en el backend.

El código del backend se escribe como de costumbre. En este ejemplo, se recibe el nombre como variable de ruta y el mensaje como parámetro de consulta. Monoy se responde.

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

2. Generar servicio de Front en Swagger.

Recibe el del backend. swagger.jsony genera los archivos de tipo y servicio. Esta tarea se puede llamar una vez al ejecutar el servidor de desarrollo o conectarse con un script de vigilancia.

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. Enviar solicitud con la función generada.

Puede cambiar a continuación. Código del Backenddel elemento objetivo y ResultadoPuede hacer clic para abrir la pantalla de ejecución a la derecha. Cambie los valores y presione el botón de solicitud. Parte visible en el proyecto real.Código del Front

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);
© 2026 Byeolnaerim. Todos los derechos reservados.IntroducciónPolítica de privacidad