
API 호출 코드를 손으로 짜는 게 귀찮다면 OpenAPI 스펙에서 타입 안전한 클라이언트를 자동으로 뽑아내는
@hey-api/openapi-ts라는 것을 써봅시다.
백엔드와 프론트엔드를 함께 개발하다 보면 이런 상황이 반복된다.
fetch/axios 코드를 직접 찾아서 수정하는 경우interface로 옮겨 적는 경우NestJS + Swagger 조합을 쓰면 백엔드가 /docs-json 형태의 OpenAPI JSON을 자동으로 만들어줍니다. 여기서 한 발짝 더 나아가면 그 JSON을 바탕으로 프론트 API 클라이언트 코드까지 자동 생성할 수 있습니다. 그게 @hey-api/openapi-ts를 쓰는 이유입니다.
@hey-api/openapi-ts는 OpenAPI 스펙(JSON 또는 YAML)을 입력받아 TypeScript 타입, API 함수, HTTP 클라이언트 설정 코드를 자동으로 생성해주는 코드 제너레이터입니다.
타입 안전성이 백엔드까지 연결된다
백엔드 Swagger 스펙이 곧 프론트 타입의 source of truth가 됩니다. 백엔드에서 필드 하나 바뀌면, openapi-ts를 다시 실행하는 것만으로 프론트 타입도 갱신됩니다.
반복 작업이 사라진다
요청 파라미터 타입, 응답 타입, API 함수 선언 — 이 모든 걸 손으로 쓸 필요가 없습니다.
플러그인 기반으로 확장 가능하다
@hey-api/client-next(fetch 기반 클라이언트), @tanstack/react-query 훅 자동 생성 등 다양한 플러그인을 조합할 수 있습니다.
런타임 없이 순수 코드 생성
생성된 파일은 별도 런타임 의존성 없이 동작합니다. 빌드 결과물에 불필요한 라이브러리가 끼어들지 않습니다.
openapi-ts.config.tsimport { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "http://localhost:3001/docs-json",
output: "generated/openapi-client",
plugins: [
{
name: "@hey-api/client-next",
runtimeConfigPath: "../config/openapi-runtime"
},
],
});
input
OpenAPI 스펙의 출처입니다. 로컬 파일 경로(./openapi.json)도 되고, 위처럼 실행 중인 서버의 URL도 됩니다. NestJS에서 @nestjs/swagger를 쓰면 /docs-json 경로로 JSON이 생깁니다.
output
생성된 파일이 저장될 디렉토리입니다. 이 안에 types.gen.ts, services.gen.ts, client.gen.ts 등이 만들어집니다.
name: "@hey-api/client-next"
어떤 HTTP 클라이언트를 사용할지 지정하는 플러그인입니다. client-next는 Web 표준 fetch 기반으로 동작하며, Next.js의 서버 컴포넌트/서버 액션 환경과 잘 맞습니다.
runtimeConfigPath
생성된 클라이언트가 런타임 설정(baseURL, 인증 토큰 등)을 가져올 파일의 경로입니다. 이 파일은 직접 작성해야 하며, 아래에서 설명합니다.
package.json에 스크립트를 추가해두면 편합니다:
{
"scripts": {
"openapi-ts": "openapi-ts"
}
}
openapi-runtime.tsimport { CreateClientConfig } from "../openapi-client/client";
import { getCookie } from "cookies-next/server";
import { cookies } from "next/headers";
const AUTH_COOKIE_NAME =
process.env.NODE_ENV === "production"
? "__Secure-authjs.session-token"
: "authjs.session-token";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
export const createClientConfig: CreateClientConfig = (config) => ({
...config,
baseURL: API_URL,
async auth() {
return getCookie(AUTH_COOKIE_NAME, { cookies });
},
});
이 파일은 자동 생성이 아닌 직접 작성하는 설정 파일입니다. 앞서 openapi-ts.config.ts의 runtimeConfigPath가 가리키는 곳이 바로 여기입니다.
auth()
인증 토큰을 반환하는 비동기 함수입니다. 생성된 클라이언트 코드에서 엔드포인트를 호출할 때 이 함수의 반환값이 Authorization: Bearer <token> 헤더로 자동 삽입됩니다.
지금은 next-auth(authjs)의 세션 쿠키를 서버 사이드에서 읽어 토큰을 가져오고 있습니다. cookies-next의 서버 버전과 Next.js의 next/headers를 함께 사용해서 서버 컴포넌트/Route Handler에서도 동작합니다.
services.gen.ts// 조회 (인증 불필요)
export const coursesControllerFindAll = <ThrowOnError extends boolean = false>(
options?: Options<CoursesControllerFindAllData, ThrowOnError>
) =>
(options?.client ?? client).get<CoursesControllerFindAllResponses, unknown, ThrowOnError>({
url: '/courses',
...options
});
// 생성 (인증 필요)
export const coursesControllerCreate = <ThrowOnError extends boolean = false>(
options: Options<CoursesControllerCreateData, ThrowOnError>
) =>
(options.client ?? client).post<CoursesControllerCreateResponses, unknown, ThrowOnError>({
security: [{ scheme: 'bearer', type: 'http' }],
url: '/courses',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
이 파일은 pnpm run openapi-ts를 실행할 때마다 덮어쓰기됩니다.
security: [{ scheme: 'bearer', type: 'http' }]
백엔드 Swagger에서 @UseGuards(AuthGuard) 또는 @ApiBearerAuth()가 붙은 엔드포인트에 자동으로 추가됩니다. 앞서 createClientConfig의 auth() 함수와 연결되어 자동으로 Authorization 헤더를 붙여줍니다.
Content-Type 자동 주입
POST/PATCH처럼 body가 있는 요청은 Content-Type: application/json이 자동으로 포함됩니다.
import { coursesControllerFindAll } from "@/generated/openapi-client";
export default async function CoursesPage() {
const { data, error } = await coursesControllerFindAll();
if (error) return <div>에러 발생</div>;
return (
<ul>
{data?.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
data의 타입은 백엔드 스펙에서 자동으로 추론됩니다. course.titlee처럼 오타를 내면 컴파일 에러가 납니다.
백엔드에 Swagger가 붙어있다면 @hey-api/openapi-ts 도입 비용은 매우 낮습니다. 특히 Next.js + NestJS 조합에서 서버 컴포넌트의 인증 처리까지 깔끔하게 해결해주는 점이 간편하고 좋습니다.
백엔드 스펙이 바뀔 때마다 타입 오류로 먼저 알 수 있고 간단한 명령어로 불필요한 코드 작성 및 수정이 줄어드는 것이 제일 큰 장점이라고 생각합니다.