@hey-api/openapi-ts로 API 클라이언트 자동 생성하기

김민석·2026년 3월 24일
post-thumbnail

@hey-api/openapi-ts로 API 클라이언트 자동 생성하기 (feat. Next.js)

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란?

@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.ts

import { 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.ts

import { 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.tsruntimeConfigPath가 가리키는 곳이 바로 여기입니다.

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()가 붙은 엔드포인트에 자동으로 추가됩니다. 앞서 createClientConfigauth() 함수와 연결되어 자동으로 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 조합에서 서버 컴포넌트의 인증 처리까지 깔끔하게 해결해주는 점이 간편하고 좋습니다.

백엔드 스펙이 바뀔 때마다 타입 오류로 먼저 알 수 있고 간단한 명령어로 불필요한 코드 작성 및 수정이 줄어드는 것이 제일 큰 장점이라고 생각합니다.


profile
나만의 기록장

0개의 댓글