설치부터 fastapi dev로 첫 서버를 띄우기까지, 그리고 FastAPI의 가장 기본인 라우팅 — @app.get 데코레이터, 경로 매개변수, 쿼리 매개변수, 타입 힌트로 자동 검증되는 422 에러, 라우트 순서 함정, 405와 307 — 을 실제 실행 결과와 함께 설명한다. 주소를 입력하면 어느 함수로 가는지 보여 주는 라우트 매칭기 위젯으로 직접 확인해 볼 수 있다.
1편에서 백엔드 프레임워크가 하는 일 다섯 가지 중 첫 번째가 라우팅이라고 했다. 라우팅은 “이 주소로 온 요청은 이 함수가 처리한다”는 주소록이다. FastAPI 코드를 처음 열었을 때 가장 먼저 읽어야 하는 것도, 협업할 때 가장 자주 이야기하는 것도 이 주소록이다.
브라우저에서 http://127.0.0.1:8000을 열면 {"message":"안녕하세요, FastAPI!"}가 보인다. 파일을 고치고 저장하면 서버가 자동으로 재시작된다(reloader). 개발할 때는 fastapi dev, 실제 배포할 때는 자동 재시작을 끈 fastapi run을 쓴다.
💡
uvicorn은 뭔가요? 예전 글에서는 uvicorn main:app --reload로 실행하는 경우가 많다. fastapi dev는 그 uvicorn을 편하게 부르는 명령이다. 둘 다 같은 서버(uvicorn)를 띄운다. main:app은 “main.py 파일 안의 app 객체”라는 뜻이다.
1-3. 코드 한 줄씩 읽기
코드
뜻
app = FastAPI()
API 서버 객체를 하나 만든다. 앞으로 모든 라우트를 여기에 등록한다.
@app.get("/")
“GET 메서드로 / 주소에 요청이 오면, 바로 아래 함수를 불러라.” 이것이 라우트 한 줄이다.
def root():
요청을 처리할 함수(FastAPI 용어로 경로 작동 함수, path operation function). 이름은 자유지만 무슨 일을 하는지 드러나게 짓는다.
return {...}
파이썬 딕셔너리를 돌려주면 FastAPI가 JSON으로 바꿔서 응답한다. 상태 코드는 기본 200.
@로 시작하는 줄을 데코레이터라고 한다. 어렵게 생각할 필요 없다. 함수 위에 붙이는 이름표다. @app.get("/")라는 이름표를 붙이면 FastAPI의 주소록에 “GET / → root 함수”라고 한 줄이 추가된다.
{user_id} 자리에 오는 값이 같은 이름의 함수 인자로 들어온다. 여기서 중요한 것은 : int다. 실제로 요청해 보면 이렇다.
요청
상태 코드
응답 본문
GET /users/42
200
{"user_id": 42} — 문자열 “42”가 정수 42로 바뀌었다
GET /users/abc
422
함수가 실행되지도 않고 에러 응답이 나간다 (아래)
/users/abc의 실제 응답은 이렇다.
json
{"detail":[{"type":"int_parsing","loc":["path","user_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}]}
어떤 종류의 오류인가 — int_parsing은 정수로 못 바꿨다는 뜻. 프로그램이 분기할 때 쓰기 좋은 고정 값이다.
msg
사람이 읽는 설명.
input
실제로 들어온 값 "abc". 디버깅할 때 가장 먼저 본다.
⚠️
422는 “네가 보낸 게 틀렸다”는 뜻이다. 422(Unprocessable Content)는 서버 버그(500)가 아니라 요청하는 쪽의 문제다. 프론트엔드 개발자가 422를 받으면 백엔드에 “서버가 죽었어요”가 아니라 loc을 보고 “user_id를 문자열로 보내고 있었네”를 먼저 확인하면 된다. 이 구분만 팀에서 공유해도 불필요한 메시지가 확 준다.
2-1. 함정 — 라우트 순서
“내 정보” API를 추가한다고 하자.
python
@app.get("/users/{user_id}")defread_user(user_id: int):
return {"user_id": user_id}
@app.get("/users/me") # ← 위 라우트 아래에 추가defread_me():
return {"user_id": "현재 로그인한 사용자"}
GET /users/me를 부르면 결과는 422다. 실제 응답의 input이 "me"로 찍힌다. 왜일까?
FastAPI는 요청이 오면 위에서부터 라우트를 비교해서 모양이 맞는 첫 번째를 고른다. 이때는 타입을 보지 않는다. /users/me는 /users/{user_id} 모양에 맞으니 첫 번째 라우트가 선택되고, 그다음에 "me"를 정수로 바꾸려다 실패한다.
그런데 여기까지 오면서 우리는 요청을 확인하려고 매번 브라우저 주소창에 주소를 쳤다. POST처럼 본문을 보내야 하는 요청은 주소창으로는 보낼 수도 없다. 다음 편에서는 FastAPI가 공짜로 주는 /docs 화면으로 요청을 보내고, 그 화면을 팀의 공용 API 명세서로 쓰는 법을 다룬다.
실행 환경. 이 글의 모든 코드와 응답(422 본문, 405, 307 리다이렉트, bool 변환 규칙, 라우트 순서 함정)은 FastAPI 0.141.1 · Pydantic 2.13.5 · Starlette 1.7 · Python 3.11에서 직접 실행해 확인한 결과를 옮겼다. 버전이 다르면 에러 문구(msg)가 조금 다를 수 있지만 type과 loc의 구조는 같다. 삽화는 코어닷투데이가 생성했다.