응답 규약
모든 LinkOStar endpoint 는 일관된 envelope로 응답합니다. 통합 코드 한 곳에서 unwrap 유틸을 둬 두면 모든 호출이 같은 함수를 통과합니다.
1. Single resource — { data }
HTTP 200 OK
{
"data": {
"hubUuid": "...",
"tenantUuid": "...",
"createdAt": "2026-06-01T12:00:00Z",
...
}
}존재하지 않는 ID에 대한 GET은 404 + error envelope (아래).
2. List + Pagination — { data, pagination }
HTTP 200 OK
{
"data": [
{ "hubUuid": "...", ... },
{ "hubUuid": "...", ... }
],
"pagination": {
"page": 1,
"size": 20,
"totalElements": 137,
"totalPages": 7
}
}Query 파라미터는 page (1-based), size. 응답의 totalElements 가 BFF의 카운트 집계에 가장 자주 쓰입니다.
3. 빈 body (204 No Content)
Telemetry 수집 (POST /v1/hubs/{uuid}/telemetry), claim 코드 revoke, 일부 mutation 성공이 여기 해당. body 없음. 클라이언트는 .json()호출 전에 status 또는 content-length 확인 필수.
4. Error envelope (RFC 7807 problem+json)
Backend Spring Boot 가 ProblemDetail 형식으로 통일.
HTTP 4xx / 5xx
Content-Type: application/problem+json
{
"type": "https://api.linkostar.com/errors/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "claimCode is required",
"instance": "/v1/hubs/claim",
"traceId": "abc123...",
"timestamp": "2026-06-04T12:34:56Z"
}주요 status:
| HTTP | 의미 | 대응 |
|---|---|---|
| 400 | 입력 형식/유효성 오류 | 응답 detail 확인 후 입력 수정 |
| 401 | 자격증명 없음/만료 | 토큰 갱신 후 재시도 |
| 403 | 자격은 유효한데 권한 없음 | 역할/tenant scope 확인 |
| 404 | 리소스 없음 | ID 확인 |
| 409 | 충돌 (예: 이미 존재) | 응답 detail 확인 후 분기 |
| 423 | Locked (예: blocked hub) | 운영팀 문의 |
| 429 | Rate limit | backoff + retry |
| 500 | 서버 오류 (transient 가능) | traceId 확인 후 재시도; 반복되면 운영팀 |
5. TypeScript unwrap 유틸 예시
interface ApiEnvelope<T> {
data?: T;
pagination?: { page: number; size: number; totalElements: number; totalPages: number };
}
async function apiList<T>(path: string): Promise<{ items: T[]; total: number }> {
const res = await fetch(`${BASE}${path}`, { headers: { 'X-API-Key': KEY } });
if (!res.ok) throw new ApiError(await res.json());
const env = await res.json() as ApiEnvelope<T[]>;
return {
items: env.data ?? [],
total: env.pagination?.totalElements ?? (env.data?.length ?? 0),
};
}6. Java unwrap 유틸 예시
record Envelope<T>(T data, Pagination pagination) {}
record Pagination(int page, int size, long totalElements, int totalPages) {}
public <T> long countViaList(String path, ParameterizedTypeReference<Envelope<List<T>>> tref) {
Envelope<List<T>> env = restClient
.get().uri(path + "?page=1&size=1")
.retrieve()
.body(tref);
return env.pagination() != null ? env.pagination().totalElements() : 0L;
}7. 호환성 약속
- 이 envelope shape은 breaking change 대상이 아닙니다 — 기존 키는 유지하고 새 키만 추가됩니다.
data안의 entity 키는 endpoint별로 진화. 추가는 minor, 제거/이름변경은 major (Changelog 공지).- 알 수 없는 key를 만나면 무시 하세요. throw 하지 마세요.