Client HTTP
Je vais d'abord expliquer comment utiliser directement le client Core, puis expliquer l'intégration optionnelle de l'adaptateur HTTP commun utilisé par le service généré automatiquement.
Utilisation directe du client Core.
Les Paths injectés dans createHttpClient<Paths>() sont le contrat de requête. L'URL et la méthode proviennent de la clé de Paths, la queryString et le pathVariable proviennent des paramètres, et le corps est déterminé par requestBody.
Le type de réponse est spécifié par l'appelant dans callApi<R>(). Core ne déduit pas automatiquement le type de réponse à partir des réponses OpenAPI.
httpClient.ts
tsimport {
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 },
});
};La forme de la réponse est choisie par API.
typed-rx-http n'impose pas ResponseWrapper. Si la réponse du serveur est un wrapper, spécifiez le type de wrapper comme R, sinon spécifiez simplement le type de réponse réel.
type ResponseWrapper<T> = { data: T; message?: string; resultType?: string };
return client.callApi<ResponseWrapper<{ requestId: string }>>({
url: "/api/bid/create/bid/request",
method: "post",
body,
});Option : adaptateur commun du projet + service généré automatiquement.
La configuration commonService ci-dessous n'est pas une structure obligatoire de Core, mais un modèle d'intégration de projet visant à appliquer l'authentification, les en-têtes SSR et les politiques de cache à plusieurs services générés en une seule fois.
Ce que fait le commonService du projet.
commonService ne désigne pas un nom de classe spécial fourni par @byeolnaerim/typed-rx-http, mais un fichier appartenant à l'utilisateur utilisé pour regrouper les politiques HTTP communes du projet. Le nom du fichier est libre, et dans l'exemple ci-dessous, nous utilisons le nom de projet réel rxjsHttpService.ts.
L'écran et le service généré automatiquement n'appellent pas createHttpClient plusieurs fois, et ce fichier utilise callApi et callApiStream comme point d'entrée commun. Ainsi, si vous modifiez une fois les règles de renouvellement d'authentification, de transmission d'en-têtes SSR, de cache et de gestion des erreurs, elles s'appliquent à tous les services générés.
Commencez par voir le code complet de commonService/rxjsHttpService.
1. Forme minimale complète
L'essence de commonService est le code ci-dessous. Créez une fois headerStore et un client HTTP typé, puis exportez callApi/callApiStream que le service généré utilisera communément. uploadFile et createSSEObservable peuvent également être exposés de la même manière depuis ce client.
rxjsHttpService.ts
tsimport {
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 + authentification de session + cache CSR/SSR inclus
Voici un exemple de projet complet ajoutant l'authentification de session, l'en-tête SSR et le cache CSR/SSR au même adaptateur public. Si le service généré importe également le wrapper de cache, cette forme devient la référence. Il suffit de copier, puis d'ajuster setLogin import, ApiTypes et CacheNames, le chemin de l'API d'authentification, les variables d'environnement et le chemin non autorisé selon votre projet.
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;
};
});
};Rôle par composition de code
paths
C'est le type de chemins OpenAPI utilisé par createHttpClient et ServiceArguments pour vérifier l'URL, la méthode, les variables de chemin, les requêtes et les types de corps.
headerStore
Stocke les en-têtes communs comme Content-Type et Authorization dans le navigateur et les partage avec l'authentification de session et la connexion RSocket.
headersProvider
CSR renvoie headerStore et SSR lit les cookies et l'Authorization de la requête actuelle pour fournir des en-têtes différents à chaque requête.
service
Client HTTP typé réel avec URL de base, en-têtes communs, gestion des erreurs 401 du serveur et message d'erreur par défaut.
sessionAuth
Synchronise le token de session avant la requête et, en cas de 401, réessaye une fois l'Observable d'origine après un rafraîchissement, sinon exécute le flux de déconnexion.
callApi / callApiStream
Fonctions communes importées par le service REST généré automatiquement et le service de flux NDJSON. Les deux renvoient un Observable RxJS.
callApiClientCache / callApiServerCache
Wrapper de cache qui utilise le cache CSR dans le navigateur et charge dynamiquement le cache SSR de @byeolnaerim/typed-rx-http/next sur le serveur.
Méthode de connexion au service généré automatiquement
Le générateur calcule le chemin relatif du commonServiceFile et ajoute l'import à chaque service. L'écran n'appelle pas directement le fichier commun, mais importe la fonction générée, qui transforme le chemin, les paramètres et le corps en pathVariable, queryString et body de ServiceArguments pour les transmettre à callApi.
generateSwagger.cjs
jsgenerateSwaggerFromHttp({
serviceDir: "./src/handler/service",
typesDir: "./src/handler/service/@types",
commonServiceFile:
"./src/handler/service/rxjsHttpService.ts",
});Une seule réponse est firstValueFrom.
Les API REST qui répondent une fois et se terminent, comme la recherche, l'enregistrement et la suppression, peuvent êtrefirstValueFromutilisées naturellement même dans des fonctions async existantes.
BusinessSearchPage.tsx
tsximport { 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,
},
}),
);À l'écran, paramsseulementtransmis sous la forme { params: { ... } }.est visible, mais le service généré contient l'URL, la méthode HTTP et le type de réponse. Les paramètres de requête sont toujours
Plusieurs réponses sont abonnées
Les requêtes avec plusieurs valeurs, comme la progression des tâches ou le flux RSocket, arriventsubscribeet se désabonnent lorsque le composant disparaît.
BatchJobPage.tsx
tsxconst subscription = processBatchJobs({
body: targets,
}).subscribe({
next: (event) => {
setProcessedCount(event.processedCount);
},
error: (error) => {
setErrorMessage(error.message);
},
complete: () => {
setIsSubmitting(false);
},
});
return () => subscription.unsubscribe();Lorsque vous utilisez directement callApi
Pour les projets ne utilisant pas le générateur de service ou pour vérifier un point de terminaison temporaire avant la génération, vous pouvez appeler directement callApi du service commun. Le type de requête est vérifié dans les chemins et le type de réponse est spécifié par le demandeur en générique.
TemporaryService.ts
tsreturn callApi<HealthResponse>({
url: "/health",
method: "get",
});