Cliente HTTP

Primeiro, explicarei como usar diretamente o cliente Core, e em seguida, explicarei o adaptador HTTP compartilhado do projeto que é usado pelo serviço gerado automaticamente como um exemplo de integração opcional.


Uso direto do cliente Core

Os Paths injetados em createHttpClient<Paths>() são o contrato da requisição. A URL e o método são definidos pela chave de Paths, queryString e pathVariable são definidos nos parâmetros, e o corpo é determinado pelo requestBody.

O tipo de resposta é especificado pelo chamador em callApi<R>(). O Core não infere automaticamente o tipo de resposta a partir das respostas do OpenAPI.

httpClient.ts
ts
import {
  createHeaderStore,
  createHttpClient,
} from "@byeolnaerim/typed-rx-http";
import type { paths as Paths } from "./@types/ApiTypes";

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

export const client = createHttpClient<Paths>({
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
  headerStore,
});

export const getAdminList = () => {
  return client.callApi<{ items: any[]; total: number }>({
    url: "/api/account/search/admin/get-list",
    method: "get",
    queryString: { pageNumber: 1, pageSize: 20 },
  });
};

A forma da resposta é escolhida por API.

typed-rx-http não força o uso de ResponseWrapper. Se a resposta do servidor for um wrapper, especifique o tipo do wrapper como R, caso contrário, especifique o tipo real da resposta.

ts
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };

return client.callApi<ResponseWrapper<{ requestId: string }>>({
  url: "/api/bid/create/bid/request",
  method: "post",
  body,
});

Opcional: adaptador compartilhado do projeto + serviço gerado automaticamente.

A configuração do commonService abaixo não é uma estrutura obrigatória do Core, mas sim um padrão de integração de projeto que aplica autenticação, cabeçalhos SSR e políticas de cache a vários serviços gerados ao mesmo tempo.

O que o commonService do projeto faz.

commonService não é um nome de classe especial fornecido por @byeolnaerim/typed-rx-http, mas refere-se a um arquivo de propriedade do usuário usado para reunir políticas HTTP comuns do projeto em um só lugar. O nome do arquivo é livre e no exemplo abaixo usamos o nome real do projeto, rxjsHttpService.ts.

Em vez de repetir createHttpClient em cada tela e serviço gerado automaticamente, usamos callApi e callApiStream gerados por este arquivo como um ponto de entrada comum. Portanto, se você modificar as regras de renovação de autenticação, transmissão de cabeçalho SSR, cache e tratamento de erros uma vez, isso se aplicará a todos os serviços gerados.

Comece a ver o código completo do commonService/rxjsHttpService.

1. Mínimo funcional

A essência do commonService é o código abaixo. Crie uma vez o headerStore e o cliente HTTP tipado, e exporte callApi/callApiStream que o serviço gerado usará em comum. uploadFile e createSSEObservable também podem ser expostos a partir do mesmo cliente.

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;
2. Next.js + autenticação de sessão + cache CSR/SSR incluído como uma extensão

Abaixo está um exemplo completo do projeto que adiciona autenticação de sessão, cabeçalho SSR e cache CSR/SSR ao mesmo adaptador público. Se o serviço gerado importar o wrapper de cache, esta forma será a referência. Basta copiar, ajustar a importação de setLogin, a posição de ApiTypes e CacheNames, o caminho da API de autenticação, as variáveis de ambiente e o caminho de não autorizado para se adequar ao seu projeto.

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

Função por composição de código

paths

Tipo de caminhos OpenAPI usado quando createHttpClient e ServiceArguments verificam URL, método, variável de caminho, consulta e tipo de corpo.

headerStore

Armazena cabeçalhos comuns como Content-Type e Authorization no navegador e compartilha com autenticação de sessão e conexão RSocket.

headersProvider

CSR retorna headerStore e SSR lê os Cookies e Authorization da solicitação atual, fornecendo cabeçalhos diferentes para cada solicitação.

service

Cliente HTTP tipado real com URL base, cabeçalho comum, tratamento de 401 do servidor e mensagem de erro padrão.

sessionAuth

Sincroniza o token de sessão antes da solicitação e, se 401, tenta novamente o Observable original após o refresh; se falhar, executa o fluxo de logout.

callApi / callApiStream

Função comum importada por serviços REST gerados automaticamente e serviços de stream NDJSON. Ambos retornam um Observable RxJS.

callApiClientCache / callApiServerCache

Wrapper de cache que usa o cache CSR no navegador e carrega dinamicamente o cache SSR de @byeolnaerim/typed-rx-http/next no servidor.

Forma de conexão com o serviço gerado automaticamente

O gerador calcula o caminho relativo do commonServiceFile e insere a importação em cada serviço. A tela importa funções geradas sem chamar diretamente o arquivo público, e a função gerada converte path, params, body em pathVariable, queryString, body de ServiceArguments para passar para callApi.

generateSwagger.cjs
js
generateSwaggerFromHttp({
  serviceDir: "./src/handler/service",
  typesDir: "./src/handler/service/@types",
  commonServiceFile:
    "./src/handler/service/rxjsHttpService.ts",
});

Uma única resposta é firstValueFrom.

APIs REST que respondem uma vez e terminam, como busca, armazenamento e exclusão, podem ser recebidas como Observable gerado.firstValueFromAssim, podem ser usadas naturalmente dentro de funções assíncronas existentes.

BusinessSearchPage.tsx
tsx
import { workplacesSearch } from "@handler/service/auto/ApiBusinessService";
import { firstValueFrom } from "rxjs";

const response = await firstValueFrom(
  workplacesSearch({
    params: {
      source: "EIW",
      q: keyword,
      size: 20,
      nextToken: nextToken || undefined,
    },
  }),
);

Na tela, parâmetrosapenas aparece, mas dentro do serviço gerado estão a URL, o método HTTP e o tipo de resposta. Os parâmetros de consulta são sempre{ params: { ... } }passados como.

Vários respostas são inscritas

Solicitações que recebem vários valores, como progresso de trabalho ou fluxo RSocketsubscribesão recebidas e a assinatura é cancelada quando o componente desaparece.

BatchJobPage.tsx
tsx
const subscription = processBatchJobs({
  body: targets,
}).subscribe({
  next: (event) => {
    setProcessedCount(event.processedCount);
  },
  error: (error) => {
    setErrorMessage(error.message);
  },
  complete: () => {
    setIsSubmitting(false);
  },
});

return () => subscription.unsubscribe();

Quando você usa callApi diretamente

Em projetos que não usam o gerador de serviços ou ao verificar um endpoint temporário antes da geração, você pode chamar diretamente callApi do serviço público. O tipo de solicitação é verificado nos caminhos e o tipo de resposta é especificado genericamente pelo chamador.

TemporaryService.ts
ts
return callApi<HealthResponse>({
  url: "/health",
  method: "get",
});
© 2026 Byeolnaerim. Todos os direitos reservados.IntroduçãoPolítica de Privacidade