/{code} 동적 라우트가 /urls를 가로채던 문제 해결 기록URL Shortener 프로젝트를 만들다가 예상치 못한 상황을 마주했다.
프론트엔드 대시보드에서 전체 URL 목록을 불러오기 위해 /urls 엔드포인트를 호출했는데, 계속 404가 떨어지는 문제가 발생했다.
{
"detail": "Short URL not found"
}
문제는 프론트엔드가 아니라 서버 라우터 쪽에 있었다.
Swagger와 curl에서도 동일하게 /urls가 404를 반환했다.
curl https://url-shorter.onrender.com/urls
# → {"detail": "Short URL not found"}
FastAPI log에서도 /urls 요청이 들어갔는데, 내부에서는 완전히 다른 라우트가 실행되고 있었다.
결론부터 말하면 아래 라우트가 문제였다.
@app.get("/{code}")
def redirect(code: str):
...
FastAPI는 라우트를 정의된 순서대로 검사한다.
/{code}는 어떤 문자열이든 모두 매칭되기 때문에 아래처럼 동작한다.
GET /urls → code = "urls" 로 매칭됨
그 결과 FastAPI는 /urls를 정적 라우트로 처리하지 않고, 동적 라우트로 인식한다.
즉,
"/urls"가 아닌 "/{code}"가 실행되고 있었던 것.
그래서 DB에서 "urls"라는 short code를 찾다가 없어서
404("Short URL not found")가 내려온 것이다.
이 상황은 URL Shortener처럼 단일 동적 엔드포인트가 있는 서비스에서 쉽게 발생한다.
정적인 라우트를 동적 라우트보다 앞에 놓아야 한다.
잘못된 순서:
@app.get("/{code}") # 동적
@app.get("/urls") # 정적 (가려짐)
올바른 순서:
@app.get("/urls") # 가장 먼저
@app.get("/stats/{code}") # 부분 동적
@app.get("/{code}") # 마지막
FastAPI는 선언 순서가 곧 우선순위이다.
정적 → 부분 동적 → 완전 동적 순으로 정리하면 충돌이 없다.
FastAPI의 라우팅은 Starlette 라우터를 기반으로 한다.
Starlette는 URL 매칭을 다음 기준으로 처리한다.
/urls, /health)/stats/{code})/{code})/{path:path})문제는 “우선순위”가 아니라 코드 선언 순서가 우선 적용된다는 점이다.
동적 경로를 맨 위에 선언하면 정적 경로보다 먼저 검사되어 그대로 가로채 버린다.
라우트 순서를 다시 정렬한 뒤에는 다음과 같이 정상적으로 응답이 내려왔다.
GET /urls → 정상 200
GET /stats/abc123 → 정상 200
GET /xyz789 → redirect 정상 작동
정리된 구조는 아래와 같다.
@app.get("/urls")
def list_urls(): ...
@app.get("/stats/{code}")
def stats(code: str): ...
@app.get("/{code}")
def redirect(code: str): ...
이번 문제는 URL Shortener에서 특히 자주 발생하는 라우트 충돌 이슈였다.
프레임워크의 라우팅 매칭 원리를 정확히 알고 있어야 해결할 수 있으며,
동적 라우트가 하나라도 포함되는 프로젝트라면 라우팅 순서를 항상 신경 써야 한다.
개발 과정에서 흔히 겪을 수 있는 오류이지만,
이 원리를 잘 이해해두면 FastAPI 구조 설계가 훨씬 깔끔해진다.