Início Rápido
Cliente HTTP seguro em tipo baseado em RxJS para TypeScript + (opcional) adaptador Next.js/RSocket.
Características principais.
- Injetar tipos de Paths estilo OpenAPI obtidos de esquemas derivados de Swagger/OpenAPI / AsyncAPI / contratos personalizados, garantindo a segurança do tipo de rota (requisição).
- Todas as APIs retornam RxJS Observable.
- O core é independente de framework (sem dependência do Next.js).
- Funcionalidades específicas do Next.js são separadas no ponto de entrada @byeolnaerim/typed-rx-http/next → Next.js é necessário apenas ao importar /next.
- Funcionalidades específicas do RSocket são separadas no ponto de entrada @byeolnaerim/typed-rx-http/rsocket → RSocket é necessário apenas ao importar /rsocket.
Instalação
npm
npm i @byeolnaerim/typed-rx-http rxjsPonto de entrada.
Core (independente de framework)
import {
createHttpClient,
createHeaderStore,
createCsrCache,
createSessionAuth,
HttpResponseError,
isHttpResponseError,
type ServiceArguments,
type CacheForService,
} from "@byeolnaerim/typed-rx-http";Adaptador Next.js (opcional).
import {
callApiSsrCache,
redirectToUnauthorizedOnServer401,
} from "@byeolnaerim/typed-rx-http/next";Em projetos que não utilizam Next.js, não importe /next.
Adaptador RSocket (opcional).
Instale o pacote peer apenas em projetos que utilizam RSocket.
npm i rsocket-core rsocket-types rsocket-websocket-clientimport { 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);
});Em projetos que não utilizam RSocket, não importe /rsocket.
Uso do Core.
1) Preparar tipo Paths (geralmente são os paths do OpenAPI).
Os Paths em createHttpClient<Paths>() representam o tipo que expressa a "especificação da requisição (rota)". Na documentação, é comumente chamado de paths, mas não precisa ser necessariamente OpenAPI/Swagger, nem ter o nome de paths.
No entanto, o core utiliza internamente a restrição OpenApiPathsLike, portanto, os Paths devem ter uma forma semelhante aos paths do OpenAPI.
- Chave de nível superior: string de caminho da URL (ex: "/users/{id}").
- Chave de nível inferior: método HTTP (get/post/put/delete/patch …).
- Dentro de cada método, existem campos como parameters.query/path/header/cookie, requestBody, responses (ou nunca).
O core refere-se principalmente aos campos abaixo para compor o tipo ServiceArguments.
- url: keyof Paths.
- method: keyof Paths[url].
- queryString: parameters.query.
- pathVariable: parameters.path.
- body: requestBody.
Ex: a saída do openapi-typescript geralmente segue o padrão abaixo (com algumas abreviações).
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"];
};
};
};
};
};
}// aliasing: OpenAPI `paths` -> `Paths`
import type { paths as Paths } from "./@types/ApiTypes";2) Criar HeaderStore
HeaderStore é um simples armazenamento em memória para gerenciar cabeçalhos padrão no CSR.
import { createHeaderStore } from "@byeolnaerim/typed-rx-http";
export const headerStore = createHeaderStore({
"Content-Type": "application/json",
});3) Criar cliente HTTP
- headerStore é opcional, mas é recomendado incluí-lo se você quiser usar cabeçalhos padrão/autenticação de sessão no CSR.
- headersProvider é usado quando é necessário calcular cabeçalhos para cada solicitação, como em SSR/multi-tenant.
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) Chamada de API (tipo seguro)
O tipo de solicitação (url/método/pathVariable/queryString/corpo) é determinado pelo tipo injetado pelo usuário em createHttpClient<Paths>() (geralmente caminhos OpenAPI). O tipo de resposta é escolhido pelo chamador em callApi<R>() como um genérico R (o núcleo não infere automaticamente das respostas).
export const getAdminList = () => {
return client.callApi<{ items: any[]; total: number }>({
url: "/api/account/search/admin/get-list",
method: "get",
queryString: { pageNumber: 1, pageSize: 20 },
});
};Encapsulamento de resposta (ResponseWrapper) — opcional
Esta biblioteca não força o encapsulamento de resposta. Você pode escolher o formato de resposta genericamente para cada API.
Resposta encapsulada
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,
});
};Resposta sem encapsulamento
export const bidCreate = (body: unknown) => {
return client.callApi<{ requestId: string }>({
url: "/api/bid/create/bid/request",
method: "post",
body,
});
};Streaming (NDJSON)
Quando o servidor retorna NDJSON (um JSON por linha), use callApiStream. Se não houver cabeçalho Accept, application/x-ndjson será definido como padrão.
export const streamEvents = () => {
return client.callApiStream<{ eventName: string; content: unknown }>({
url: "/api/event/stream",
method: "get",
// headers: { Accept: "application/x-ndjson" }, // 선택
});
};Cache CSR (cache do cliente)
Funcionalidade fornecida por createCsrCache<CacheName>:
callApiCsrCache(callApiFn, serviceArgs, cacheOptions)- removeCsrCache(cacheName) — suporta tanto nomes de cache de tipo quanto strings.
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 autenticação baseado em sessão (opcional)
createSessionAuth separa a lógica de autenticação de sessão do núcleo, permitindo que você a adicione ou remova como uma opção.
Funcionamento:
- Manter Authorization no headerStore
- Sincronizar token com ensureToken$() (/api/auth/token)
- Ao ocorrer 401, tente refresh uma vez (/api/auth/token/refresh) e reenvie a solicitação original.
- Se o refresh falhar, faça logout (/api/auth/logout) e passe o erro.
- Mudanças no estado de login são tratadas externamente pelo callback onLoginChange.
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());
};Se apenas a sincronização do token for necessária, sem refresh/retry:
return client.callApi(...).pipe(auth.withEnsureToken());Tratamento de erros
Se não for 2xx, lança HttpResponseError (incluindo status, resposta, args, dados).
Compatibilidade legada: se o corpo do erro estiver no formato { resultType: ... }, lança esse objeto diretamente.
import { isHttpResponseError } from "@byeolnaerim/typed-rx-http";
client.callApi(...).subscribe({
error: (e) => {
if (isHttpResponseError(e)) {
console.log(e.status, e.data);
}
},
});Adaptador Next.js (/next)
redirectToUnauthorizedOnServer401
redirectToUnauthorizedOnServer401 é a implementação padrão (função de conveniência) que realiza o redirecionamento quando ocorre 401 no ambiente SSR do Next.js (App Router).
import { redirectToUnauthorizedOnServer401 } from "@byeolnaerim/typed-rx-http/next";Regras de operação (fixas):
- Destino do redirecionamento: /unauthorized
- queryString: redirect_uri=<página atual> + logout=true
- A página atual é lida do cabeçalho x-page-url (se não houver, será /).
Ou seja, use essas regras de caminho/query apenas se elas corresponderem ao seu projeto. Se o caminho for diferente ou as regras de query forem diferentes, você pode implementar onServer401 diretamente e injetá-lo como abaixo.
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 é um assistente de cache SSR baseado em next/cache(unstable_cache) do Next.
- GET + cacheTime > 0 → force-cache + revalidate
- Outros → no-store
- Injeção de Cookie / Autorização por solicitação com headersProvider
- Se ocorrer 401 e houver onServer401, execute (geralmente redirect())
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$);Exemplo de integração do Next.js: código completo do projeto rxjsHttpService/commonService
O rxjsHttpService.ts abaixo é o adaptador HTTP comum do projeto que é referenciado pelo commonServiceFile do exemplo do gerador a seguir. Não é um arquivo gerado pela biblioteca, mas gerenciado diretamente pelo projeto, exportando core client + session auth + CSR cache + SSR cache helper em um só lugar. Abaixo está o código completo.
rxjsHttpService.ts
tsimport {
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;
};
});
};Referência da API (core)
createHttpClient<Paths>(options)
Retorno:
callApi<R>(args): Observable<R>callApiStream<RChunk>(args): Observable<RChunk>uploadFile({ file, url, ifNoneMatch?, headers? }): Observable<Response>createSSEObservable<R>(args): Observable<R>
Opções:
baseUrl: stringheaderStore?: HeaderStoreheadersProvider?: () => Record<string, string> | Promise<Record<string, string>>- dropAuthWhenCacheControl?: boolean (padrão: true)
onServer401?: () => void | Promise<void>
createHeaderStore(initial?)
get(), set(), merge(), remove(), clear()
createCsrCache<CacheName>()
callApiCsrCache(callApiFn, serviceArgs, cacheForService)- removeCsrCache(cacheName) (tipo + string)
createSessionAuth(options)
withSessionAuth(), withEnsureToken()ensureToken$(), refreshToken$(), logout$()
Requisitos de tempo de execução
- Uso da API fetch / Response (rxjs/fetch)
- Streaming (NDJSON) requer ReadableStream + TextDecoder
- SSE requer EventSource
A maioria dos navegadores modernos e o tempo de execução do Next.js oferecem suporte nativo. Um tempo de execução Node personalizado pode precisar de um polyfill.
Auto Node Script: Geração de código OpenAPI/Swagger (opcional)
Este pacote fornece, além do cliente HTTP em tempo de execução, um script Node que gera código de tipo/serviço a partir de JSON OpenAPI/Swagger. Este script é uma funcionalidade opcional.
Usuários comuns de @byeolnaerim/typed-rx-http, /next, /rsocket não precisam executar este script e não precisam instalar openapi-typescript.
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";Isolamento de dependências
A geração de tipos OpenAPI requer o CLI openapi-typescript. No entanto, este pacote não inclui openapi-typescript nas dependências gerais.
A biblioteca @byeolnaerim/typed-rx-http em si usa TypeScript 6.0.3 em devDependencies.typescript. Os pontos de entrada e a construção da biblioteca typed-rx-http, /next, /rsocket mantêm esse padrão de TypeScript 6.0.3.
No entanto, openapi-typescript pode exigir uma versão específica do TypeScript 5.x, portanto, o script auto node OpenAPI é executado em um ambiente temporário npx separado com openapi-typescript e [email protected]. Este ambiente temporário não altera o devDependencies.typescript 6.0.3 da biblioteca e não usa a versão do typescript ou openapi-typescript instalada no projeto do usuário.
O padrão é gerar e executar os seguintes comandos apenas no momento da execução do script automático.
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.tsPortanto, a forma de uso não muda. Você pode chamar o script auto node como antes, e um ambiente isolado de TypeScript 5.9.3 será usado apenas na etapa de geração de tipos OpenAPI. Usuários que não usam o script automático não ficam vinculados ao openapi-typescript ou TypeScript 5.9.3.
Se necessário, você pode fixar o comando diretamente com openApiTypescriptCommand.
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 configura o comando padrão.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});Arquivos gerados
As configurações padrão geram os arquivos abaixo.
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.tsapiUnionArrays.ts gera não apenas enums do esquema OpenAPI, mas também enums de parâmetros de query, path, header e cookie como arrays constantes. Também lida com os items.enum de parâmetros de query em array.
Monitoramento de EventStream
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",
});Requisição HTTP única
await generateSwaggerFromHttp({
hostname: "localhost",
port: 8788,
path: "/oauth2/for-local/get-swagger",
});Gerar a partir de arquivo local
generateSwaggerFromFile({
inputFile: "./swagger.json",
});Exemplo de integração de projeto existente: WebFlux + Geração automática de Swagger
A partir daqui, é um exemplo de integração de projeto que usa o Swagger do backend e o serviço gerado automaticamente. O uso do Core e as funcionalidades opcionais podem ser utilizados apenas com o typed-rx-http, e o fluxo abaixo é aplicado em projetos que utilizam a geração automática de serviços baseados em Swagger.
1. Escreva o endpoint REST no backend.
O código do backend é escrito como de costume. Neste exemplo, recebemos o nome como variável de caminho e a mensagem como parâmetro de consulta. MonoE respondemos.
TypedRxHttpExampleRouter.java
java@Bean
public RouterFunction<ServerResponse> typedRxHttpExample(
TypedRxHttpExampleHandler handler
) {
return route()
.GET("/typed-rx-http/example/{name}", handler::getExample)
.build();
}2. Gere o serviço front no Swagger.
Receba o do backend. swagger.jsonGere os arquivos de tipo e serviço. Esta tarefa pode ser chamada uma vez ao executar o servidor de desenvolvimento ou conectada a um script de observação.
generateSwagger.cjs
jsconst {
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. Envie a solicitação com a função gerada.
Você pode alterar abaixo. Código Backenddo elemento alvo e ResultadoVocê pode clicar para abrir a tela de execução à direita. Altere os valores e pressione o botão de solicitação. Parte visível no projeto real.Código Front
TypedRxHttpRequestExample.tsx
tsximport { 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);