웹 API 개발
서버 구조
packages/server/src/
├── index.ts # 서버 진입점 (localhost:3000), 미들웨어, 인증
├── history-sync.ts # 히스토리 동기화 진입점
├── history/
│ ├── store.ts # 분할 히스토리 SQLite 저장소
│ ├── store.test.ts # store 단위 테스트
│ ├── sync.ts # 히스토리 동기화 로직
│ └── types.ts # 히스토리 타입 정의
├── lib/
│ └── resolve-model.ts # provider+model 유효성 검증 및 해석
└── routes/
├── decks.ts # 덱 관련
├── cards.ts # 카드 관련
├── split.ts # 분할 (preview/apply/reject)
├── backup.ts # 백업/롤백
├── media.ts # Anki 미디어 프록시
├── clinic.ts # 검증 5종 + YAGNI + fix/apply
├── llm.ts # LLM 모델/프로바이더 정보
├── embedding.ts # 임베딩
├── prompts.ts # 프롬프트 버전 관리 + systemPrompt CAS
└── history.ts # 분할 히스토리
API 인증 미들웨어
모든 /api/* 요청은 인증 미들웨어를 거친다. /api/health만 면제.
인증 방식 (2가지)
- X-API-Key 헤더:
X-API-Key: <key>
- Bearer 토큰:
Authorization: Bearer <key>
둘 다 전달되면 X-API-Key가 우선. timing-safe 비교로 타이밍 공격 방지.
환경변수
| 변수 |
설명 |
기본값 |
ANKI_SPLITTER_API_KEY |
API 키 값 |
(없으면 503) |
ANKI_SPLITTER_REQUIRE_API_KEY |
인증 강제 여부 |
"true" |
REQUIRE_API_KEY=true + API_KEY 미설정 -> 모든 API 503 반환 (/api/health 제외)
REQUIRE_API_KEY=false -> 인증 비활성화 (Tailscale 등 격리 환경 전용)
CORS
CORS_ORIGINS 환경변수로 허용 origin 제어. 기본: http://localhost:5173, http://127.0.0.1:5173.
쉼표 구분. 허용 헤더: Content-Type, X-API-Key, Authorization.
전체 엔드포인트 목록
Health
| Method |
Path |
설명 |
| GET |
/api/health |
헬스 체크. 인증 면제. { status: "ok", timestamp } 반환 |
Decks & Cards
| Method |
Path |
설명 |
| GET |
/api/decks |
덱 목록 |
| GET |
/api/decks/:name/stats |
덱 통계 (분할 후보 수, 임베딩 커버리지) |
| GET |
/api/cards/deck/:name |
카드 목록 (page 기반 페이지네이션, filter) |
| GET |
/api/cards/deck/:name/difficult |
학습 데이터 기반 어려운 카드 조회 |
| GET |
/api/cards/:noteId |
카드 상세 |
Split & Backup
| Method |
Path |
설명 |
| POST |
/api/split/preview |
분할 미리보기 (AI 호출, 비용 가드레일) |
| POST |
/api/split/apply |
분할 적용 (자동 백업 + 자동 롤백) |
| POST |
/api/split/reject |
분할 반려 (rejectionReason 필수, 프롬프트 메트릭 기록) |
| GET |
/api/backup |
백업 목록 |
| GET |
/api/backup/latest |
최근 백업 ID 조회 |
| POST |
/api/backup/:id/rollback |
롤백 |
Clinic
| Method |
Path |
설명 |
| POST |
/api/clinic/fact-check |
팩트 체크 |
| POST |
/api/clinic/freshness |
최신성 검사 |
| POST |
/api/clinic/similarity |
유사성 검사 (useEmbedding 옵션) |
| POST |
/api/clinic/context |
문맥 일관성 검사 |
| POST |
/api/clinic/verbose |
Verbose 감지 |
| POST |
/api/clinic/yagni |
YAGNI 검사 |
| POST |
/api/clinic/all |
전체 검증 (병렬) |
| POST |
/api/clinic/fix/apply |
검증 결과 수정 적용 |
Embedding
| Method |
Path |
설명 |
| POST |
/api/embedding/generate |
덱 전체 임베딩 생성 |
| GET |
/api/embedding/status/:deckName |
캐시 상태 |
| DELETE |
/api/embedding/cache/:deckName |
캐시 삭제 |
| POST |
/api/embedding/single |
단일 텍스트 임베딩 (디버깅) |
LLM
| Method |
Path |
설명 |
| GET |
/api/llm/models |
사용 가능한 프로바이더/모델 목록, 가격 정보, 기본 모델, 서버 예산 캡 |
History
| Method |
Path |
설명 |
| GET |
/api/history |
분할 히스토리 목록 (페이지네이션, 필터: deckName, status, startDate, endDate) |
| GET |
/api/history/sync/health |
히스토리 동기화 상태 |
| GET |
/api/history/:sessionId |
세션 상세 조회 |
Media
| Method |
Path |
설명 |
| GET |
/api/media/:filename |
Anki 미디어 파일 프록시 (Base64 디코딩, MIME 자동 감지, 24시간 캐시) |
Prompts -- managing-prompts 스킬 참조
| Method |
Path |
설명 |
| GET |
/api/prompts/system |
원격 systemPrompt 조회 |
| POST |
/api/prompts/system |
CAS 기반 systemPrompt 저장 (expectedRevision + reason 필수) |
| GET/POST |
/api/prompts/versions |
버전 목록/생성 |
| GET/PUT/DELETE |
/api/prompts/versions/:id |
버전 CRUD |
| POST |
/api/prompts/versions/:id/activate |
활성화 |
| GET |
/api/prompts/active |
현재 활성 버전 |
| GET/POST |
/api/prompts/history |
히스토리 |
| GET |
/api/prompts/versions/:id/failure-patterns |
실패 패턴 |
| GET/POST |
/api/prompts/experiments |
A/B 테스트 목록/생성 |
| GET |
/api/prompts/experiments/:id |
실험 상세 조회 |
| POST |
/api/prompts/experiments/:id/complete |
실험 완료 |
라우트 추가 패턴
라우트 파일은 prefix 없이 상대 경로로 정의하고, index.ts에서 prefix를 부여하는 패턴을 사용한다.
// packages/server/src/routes/new-route.ts
// 라우트 내부에서는 prefix 없이 정의
import { NotFoundError, ValidationError } from "@anki-splitter/core";
import { Hono } from "hono";
const app = new Hono();
app.get("/:id", async (c) => {
const id = c.req.param("id");
const resource = await findResource(id);
if (!resource) throw new NotFoundError(`리소스 ${id}를 찾을 수 없습니다`);
return c.json(resource);
});
export default app;
// packages/server/src/index.ts에서 prefix 부여하여 등록
import newRoute from './routes/new-route.js';
app.route('/api/new-route', newRoute);
// -> GET /api/new-route/:id 로 접근 가능
주의사항
- 카드 목록 API에서 텍스트 200자 제한 (성능) -> 상세 조회 별도 필요
c.json(result) 형식으로 응답 반환
- 에러는 throw -> 글로벌
app.onError 핸들러가 중앙 처리 (try/catch 불필요)
PUT /api/prompts/versions/:id에서 systemPrompt 필드 수정 차단 -> /api/prompts/system 전용
디버깅
# 헬스 체크 (인증 불필요)
curl -s http://localhost:3000/api/health | python3 -m json.tool
# 인증이 필요한 엔드포인트
curl -s -H "X-API-Key: $ANKI_SPLITTER_API_KEY" http://localhost:3000/api/decks | python3 -m json.tool
curl -s -H "Authorization: Bearer $ANKI_SPLITTER_API_KEY" http://localhost:3000/api/cards/1757399484677 | python3 -m json.tool
상세 참조
references/route-patterns.md -- 전체 라우트 패턴, 요청/응답 예시
references/error-handling.md -- Hono 에러 핸들러, 에러 클래스
references/troubleshooting.md -- API 디버깅 팁, 인증 문제 해결
1---2name: developing-web-api3description: Hono REST API 서버의 라우트 추가, 수정, 에러 핸들링, 인증 미들웨어 등 API 서버 관련 작업이면 반드시 이 스킬을 먼저 확인할 것. Triggers: "API 라우트 추가", "Hono 엔드포인트", "서버 에러", "REST API", "라우트 패턴", "API 응답 형식", "서버 포트", "API 인증", "API key", "CORS", "미들웨어", "health check", "split reject", "split 반려", "백업 API", "프롬프트 API", "시스템 프롬프트 API", "서버 구조", "라우트 등록". Covers the Hono REST API server, route patterns, authentication, error handling, and all API endpoints.4---56# 웹 API 개발78## 서버 구조910```11packages/server/src/12├── index.ts # 서버 진입점 (localhost:3000), 미들웨어, 인증13├── history-sync.ts # 히스토리 동기화 진입점14├── history/15│ ├── store.ts # 분할 히스토리 SQLite 저장소16│ ├── store.test.ts # store 단위 테스트17│ ├── sync.ts # 히스토리 동기화 로직18│ └── types.ts # 히스토리 타입 정의19├── lib/20│ └── resolve-model.ts # provider+model 유효성 검증 및 해석21└── routes/22 ├── decks.ts # 덱 관련23 ├── cards.ts # 카드 관련24 ├── split.ts # 분할 (preview/apply/reject)25 ├── backup.ts # 백업/롤백26 ├── media.ts # Anki 미디어 프록시27 ├── clinic.ts # 검증 5종 + YAGNI + fix/apply28 ├── llm.ts # LLM 모델/프로바이더 정보29 ├── embedding.ts # 임베딩30 ├── prompts.ts # 프롬프트 버전 관리 + systemPrompt CAS31 └── history.ts # 분할 히스토리32```3334## API 인증 미들웨어3536모든 `/api/*` 요청은 인증 미들웨어를 거친다. `/api/health`만 면제.3738### 인증 방식 (2가지)39401. **X-API-Key 헤더**: `X-API-Key: <key>`412. **Bearer 토큰**: `Authorization: Bearer <key>`4243둘 다 전달되면 `X-API-Key`가 우선. **timing-safe 비교**로 타이밍 공격 방지.4445### 환경변수4647| 변수 | 설명 | 기본값 |48|------|------|--------|49| `ANKI_SPLITTER_API_KEY` | API 키 값 | (없으면 503) |50| `ANKI_SPLITTER_REQUIRE_API_KEY` | 인증 강제 여부 | `"true"` |5152- `REQUIRE_API_KEY=true` + `API_KEY` 미설정 -> 모든 API 503 반환 (`/api/health` 제외)53- `REQUIRE_API_KEY=false` -> 인증 비활성화 (Tailscale 등 격리 환경 전용)5455### CORS5657`CORS_ORIGINS` 환경변수로 허용 origin 제어. 기본: `http://localhost:5173`, `http://127.0.0.1:5173`.58쉼표 구분. 허용 헤더: `Content-Type`, `X-API-Key`, `Authorization`.5960## 전체 엔드포인트 목록6162### Health63| Method | Path | 설명 |64|--------|------|------|65| GET | /api/health | 헬스 체크. 인증 면제. `{ status: "ok", timestamp }` 반환 |6667### Decks & Cards68| Method | Path | 설명 |69|--------|------|------|70| GET | /api/decks | 덱 목록 |71| GET | /api/decks/:name/stats | 덱 통계 (분할 후보 수, 임베딩 커버리지) |72| GET | /api/cards/deck/:name | 카드 목록 (page 기반 페이지네이션, filter) |73| GET | /api/cards/deck/:name/difficult | 학습 데이터 기반 어려운 카드 조회 |74| GET | /api/cards/:noteId | 카드 상세 |7576### Split & Backup77| Method | Path | 설명 |78|--------|------|------|79| POST | /api/split/preview | 분할 미리보기 (AI 호출, 비용 가드레일) |80| POST | /api/split/apply | 분할 적용 (자동 백업 + 자동 롤백) |81| POST | /api/split/reject | 분할 반려 (rejectionReason 필수, 프롬프트 메트릭 기록) |82| GET | /api/backup | 백업 목록 |83| GET | /api/backup/latest | 최근 백업 ID 조회 |84| POST | /api/backup/:id/rollback | 롤백 |8586### Clinic87| Method | Path | 설명 |88|--------|------|------|89| POST | /api/clinic/fact-check | 팩트 체크 |90| POST | /api/clinic/freshness | 최신성 검사 |91| POST | /api/clinic/similarity | 유사성 검사 (useEmbedding 옵션) |92| POST | /api/clinic/context | 문맥 일관성 검사 |93| POST | /api/clinic/verbose | Verbose 감지 |94| POST | /api/clinic/yagni | YAGNI 검사 |95| POST | /api/clinic/all | 전체 검증 (병렬) |96| POST | /api/clinic/fix/apply | 검증 결과 수정 적용 |9798### Embedding99| Method | Path | 설명 |100|--------|------|------|101| POST | /api/embedding/generate | 덱 전체 임베딩 생성 |102| GET | /api/embedding/status/:deckName | 캐시 상태 |103| DELETE | /api/embedding/cache/:deckName | 캐시 삭제 |104| POST | /api/embedding/single | 단일 텍스트 임베딩 (디버깅) |105106### LLM107| Method | Path | 설명 |108|--------|------|------|109| GET | /api/llm/models | 사용 가능한 프로바이더/모델 목록, 가격 정보, 기본 모델, 서버 예산 캡 |110111### History112| Method | Path | 설명 |113|--------|------|------|114| GET | /api/history | 분할 히스토리 목록 (페이지네이션, 필터: deckName, status, startDate, endDate) |115| GET | /api/history/sync/health | 히스토리 동기화 상태 |116| GET | /api/history/:sessionId | 세션 상세 조회 |117118### Media119| Method | Path | 설명 |120|--------|------|------|121| GET | /api/media/:filename | Anki 미디어 파일 프록시 (Base64 디코딩, MIME 자동 감지, 24시간 캐시) |122123### Prompts -- `managing-prompts` 스킬 참조124| Method | Path | 설명 |125|--------|------|------|126| GET | /api/prompts/system | 원격 systemPrompt 조회 |127| POST | /api/prompts/system | CAS 기반 systemPrompt 저장 (expectedRevision + reason 필수) |128| GET/POST | /api/prompts/versions | 버전 목록/생성 |129| GET/PUT/DELETE | /api/prompts/versions/:id | 버전 CRUD |130| POST | /api/prompts/versions/:id/activate | 활성화 |131| GET | /api/prompts/active | 현재 활성 버전 |132| GET/POST | /api/prompts/history | 히스토리 |133| GET | /api/prompts/versions/:id/failure-patterns | 실패 패턴 |134| GET/POST | /api/prompts/experiments | A/B 테스트 목록/생성 |135| GET | /api/prompts/experiments/:id | 실험 상세 조회 |136| POST | /api/prompts/experiments/:id/complete | 실험 완료 |137138## 라우트 추가 패턴139140라우트 파일은 prefix 없이 상대 경로로 정의하고, `index.ts`에서 prefix를 부여하는 패턴을 사용한다.141142```typescript143// packages/server/src/routes/new-route.ts144// 라우트 내부에서는 prefix 없이 정의145import { NotFoundError, ValidationError } from "@anki-splitter/core";146import { Hono } from "hono";147148const app = new Hono();149150app.get("/:id", async (c) => {151 const id = c.req.param("id");152 const resource = await findResource(id);153 if (!resource) throw new NotFoundError(`리소스 ${id}를 찾을 수 없습니다`);154 return c.json(resource);155});156157export default app;158159// packages/server/src/index.ts에서 prefix 부여하여 등록160import newRoute from './routes/new-route.js';161app.route('/api/new-route', newRoute);162// -> GET /api/new-route/:id 로 접근 가능163```164165## 주의사항166167- 카드 목록 API에서 텍스트 200자 제한 (성능) -> 상세 조회 별도 필요168- `c.json(result)` 형식으로 응답 반환169- 에러는 throw -> 글로벌 `app.onError` 핸들러가 중앙 처리 (try/catch 불필요)170- `PUT /api/prompts/versions/:id`에서 `systemPrompt` 필드 수정 차단 -> `/api/prompts/system` 전용171172## 디버깅173174```bash175# 헬스 체크 (인증 불필요)176curl -s http://localhost:3000/api/health | python3 -m json.tool177178# 인증이 필요한 엔드포인트179curl -s -H "X-API-Key: $ANKI_SPLITTER_API_KEY" http://localhost:3000/api/decks | python3 -m json.tool180curl -s -H "Authorization: Bearer $ANKI_SPLITTER_API_KEY" http://localhost:3000/api/cards/1757399484677 | python3 -m json.tool181```182183## 상세 참조184185- `references/route-patterns.md` -- 전체 라우트 패턴, 요청/응답 예시186- `references/error-handling.md` -- Hono 에러 핸들러, 에러 클래스187- `references/troubleshooting.md` -- API 디버깅 팁, 인증 문제 해결