OpenAPI & Service Generator
An optional Node script provided separately from the Core HTTP client. It is only used when generating TypeScript types and service files from OpenAPI/Swagger JSON.
Optional feature: Not required for Core usage
General users of @byeolnaerim/typed-rx-http, /next, or /rsocket do not need to run this script or install openapi-typescript in their project.
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";Isolation of openapi-typescript dependency
Generating OpenAPI types requires the openapi-typescript CLI, but the package is not included in the general dependencies. The devDependencies.typescript of typed-rx-http maintains TypeScript version 6.0.3, and only uses a separate npx temporary environment in the OpenAPI auto node script.
In the basic OpenAPI generation step, openapi-typescript and [email protected] are run together, so it does not change or use the TypeScript/openapi-typescript versions installed in the user's project.
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.tsIf needed, the entire command can be fixed to 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",
});Alternatively, you can just change the package version that composes the default command.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});First: Prepare the project file that commonServiceFile will point to.
Before commonServiceFile appears in the generator example, you need to understand this file first. commonServiceFile is not an option that creates a file, but the path to the project-owned file from which the generated service imports common HTTP functions. The file name can be freely chosen, such as rxjsHttpService.ts or commonService.ts.
Minimum complete version full code
The file below creates createHttpClient once in the project and exports callApi and callApiStream for reuse by the generated service. If cache or session auth is not needed, you can start with this structure.
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;Full code including Session/CSR/SSR cache
To use callApiClientCache or callApiServerCache in the generated service or to handle request-specific headers and session authentication in Next.js, expand to the following full form.
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;
};
});
};Execution method
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",
});Generated Files
The default configuration generates the files below.
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.tsgenerates not only OpenAPI schema enums but also readonly constant arrays for query, path, header, and cookie parameter enums, and handles items.enum for array query parameters.
Optional: Connect backend Swagger with project commonService
From here, an example of connecting the generator to the actual project. WebFlux/webflux-fe-dev-assistant is just one way to provide Swagger and is not a mandatory dependency.
Prepare Swagger documentation
Swagger can be from Springdoc, other OpenAPI tools, or manually written files. If you use WebFlux functional endpoints, webflux-fe-dev-assistantYou can also use this method to provide Swagger documentation.
Example of generating with specified options in the project
You can specify the backend endpoint, the swagger.json to save, the type/service output path, and commonServiceFile according to the project structure.
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",
unionArraysFile: "./src/handler/service/apiUnionArrays.ts",
commonServiceFile:
"./src/handler/service/rxjsHttpService.ts",
});In environments where documents cannot be received via HTTP, you can use the same output options along with this. generateSwaggerFromFileYou can use this.
commonServiceFile is the import target path
commonServiceFileis the path to the project-owned file from which the generated service imports callApi, callApiStream, and cache wrapper. The generator does not have the option to create or overwrite this file.
ApiBusinessService.ts
tsimport { callApi } from "../rxjsHttpService";
export const workplacesSearch = ({ params }) => {
return callApi<SearchWorkplacesResponse>({
url: "/api/business/workplaces/search",
method: "get",
queryString: params,
});
};Example of where the generation results are placed in the project.
src/handler/service/
├─ rxjsHttpService.ts # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│ ├─ ApiBusinessService.ts
│ ├─ ApiOrdersService.ts
│ └─ Oauth2LoginService.ts
└─ @types/
├─ ApiTypes.d.ts
└─ auto/
├─ ApiBusinessTypes.ts
├─ ApiOrdersTypes.ts
└─ Oauth2LoginTypes.tsThe common file specified as commonServiceFile like rxjsHttpService.ts is managed directly by the project. The results under auto and @types/auto will be regenerated if the documentation changes.
File name and function name rules
The service file name is determined by the first two segments of the URL, and the function name is determined by the segments after the third. Path variables are included in the function name in the By + PascalCase format.
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 })
Arguments for the creation function
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 },
});The query parameter key of the generated service is params, the path variable is path, and the request body is body. Each is converted to ServiceArguments for queryString, pathVariable, and body within the creation function.