Files
fe-agent-new/skills/api-server-route/SKILL.md

5.1 KiB

name, description
name description
api-server-route Nuxt 3 서버 라우트(Nitro)와 API 통합 레이어를 작성할 때 사용합니다. "API 라우트 만들어줘", "서버 라우트", "server/api", "BFF", "프록시 API", "서버 미들웨어", "Nitro", "defineEventHandler" 등을 요청하면 트리거됩니다.

Nuxt 서버 라우트 (Nitro)

이 skill은 Nuxt 3의 Nitro 서버 라우트를 생성합니다. BFF 프록시 패턴, API 엔드포인트, 서버 미들웨어, 서버 유틸리티를 포함합니다.

작업 순서

  1. 라우트 목적 파악

    • BFF 프록시 (외부 API 중계)
    • 내부 데이터 변환/가공
    • 인증 엔드포인트
    • 서버 전용 로직
  2. 기존 서버 라우트 탐색

    • server/api/ 구조 확인
    • server/utils/ 에서 공유 유틸리티 확인
    • server/middleware/ 에서 기존 미들웨어 확인
  3. HTTP 메서드 및 파일 네이밍 결정

    • 메서드별 파일: xxx.get.ts, xxx.post.ts, xxx.delete.ts
    • 통합 파일: xxx.ts (모든 메서드 처리)
  4. 요청/응답 타입 정의

    • readBody<T>(), getQuery(), getRouterParam() 의 타입 명시
  5. 핸들러 구현

    • defineEventHandler 사용
    • 입력 검증 추가
    • 에러 처리는 createError 사용
  6. 클라이언트 연결

    • 대응하는 composable에서 useFetch('/api/xxx') 로 연결 안내
  7. 검증

    • TypeScript 오류 확인
    • nuxt dev 에서 API 라우트 정상 동작 확인 안내

디렉토리 구조

server/
├── api/
│   ├── auth/
│   │   ├── login.post.ts
│   │   └── logout.post.ts
│   └── users/
│       ├── index.get.ts      # GET /api/users
│       ├── index.post.ts     # POST /api/users
│       └── [id].get.ts       # GET /api/users/:id
├── middleware/
│   └── auth.ts               # 모든 요청에 적용
└── utils/
    └── api-client.ts          # 공유 유틸리티

GET 라우트 템플릿

// server/api/users/[id].get.ts
import type { UserProfile } from '~/types/user';

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id');

  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: 'User ID is required',
    });
  }

  const config = useRuntimeConfig();

  const profile = await $fetch<UserProfile>(
    `${config.apiBaseUrl}/users/${id}`,
    {
      headers: {
        Authorization: getHeader(event, 'authorization') ?? '',
      },
    },
  );

  return profile;
});

POST 라우트 템플릿

// server/api/users/index.post.ts
interface CreateUserBody {
  name: string;
  email: string;
}

export default defineEventHandler(async (event) => {
  const body = await readBody<CreateUserBody>(event);

  if (!body.name || !body.email) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Name and email are required',
    });
  }

  const config = useRuntimeConfig();

  const result = await $fetch(`${config.apiBaseUrl}/users`, {
    method: 'POST',
    body,
  });

  return result;
});

서버 미들웨어 템플릿

// server/middleware/auth.ts
export default defineEventHandler((event) => {
  const protectedPaths = ['/api/protected', '/api/admin'];
  const isProtected = protectedPaths.some((path) =>
    event.path.startsWith(path),
  );

  if (!isProtected) {
    return;
  }

  const token = getHeader(event, 'authorization');

  if (!token) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized',
    });
  }
});

서버 유틸리티 템플릿

// server/utils/api-client.ts
export function createApiClient() {
  const config = useRuntimeConfig();

  return {
    get: <T>(path: string, headers?: Record<string, string>) =>
      $fetch<T>(`${config.apiBaseUrl}${path}`, { headers }),

    post: <T>(path: string, body: unknown, headers?: Record<string, string>) =>
      $fetch<T>(`${config.apiBaseUrl}${path}`, {
        method: 'POST',
        body,
        headers,
      }),
  };
}

nuxt.config.ts 런타임 설정

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 서버 전용 (클라이언트에 노출 안 됨)
    apiBaseUrl: process.env.API_BASE_URL || 'http://localhost:8080',
    apiSecret: process.env.API_SECRET,

    // 클라이언트에도 노출
    public: {
      appName: 'My App',
    },
  },
});

주의사항

  • 비밀키는 useRuntimeConfig() 로만 접근: 클라이언트 코드에 노출 금지. runtimeConfig (public이 아닌) 필드에 저장
  • 서버 라우트는 Nitro(Node.js) 컨텍스트: Vue 반응성(ref, computed), 브라우저 API(window, document) 사용 불가
  • readBody<T>() 입력 검증 필수: 타입 단언만으로는 런타임 안전성 미보장. 필수 필드 체크 추가
  • createError 로 일관된 에러 응답: statusCode + statusMessage 형태
  • 외부 API 프록시 시 헤더 전달 주의: 내부 전용 헤더가 외부로 유출되지 않도록 필요한 헤더만 선택 전달
  • 파일 네이밍은 kebab-case.ts (rules/coding-conventions.md 참조)
  • any 타입 사용 금지