Fitz로 30분 안에 HTTP·Postgres·인증 URL 단축기 만들기
출처: Dev.to
Fitz를 이용한 단계별 튜토리얼입니다. fitz new 로 시작해 Docker에서 실행되는 네이티브 바이너리까지 완성합니다. 외부 의존성 없이 진행합니다. pip install 도 필요 없습니다. 단순히 Postgres, JWT 인증, 그리고 OpenAPI 자동 생성만 사용합니다.
우리가 만들고자 하는 것
실제 API에 꼭 필요한 네 가지 요소를 갖춘 URL 단축기:
- HTTP 엔드포인트 – 생성, 리다이렉트, 통계 조회용.
- Postgres 영속성 – 링크와 클릭 카운터 저장.
- JWT 인증 – 로그인한 사용자만 짧은 URL을 만들 수 있음.
- 네이티브 바이너리 –
fitz build로 만든 뒤 컨테이너에 바로 넣을 수 있음.
최종 규모: 약 120줄의 Fitz 코드. requirements.txt도, package.json도, cargo add도 없습니다. 오직 fitz와 Postgres만 있으면 됩니다.
완성 후 제공되는 API:
POST /login→ 자격 증명을 JWT로 교환.POST /shorten(인증 필요) → 짧은 코드 반환.GET /{code}→ 원본 URL로 리다이렉트하고 카운터 증가.GET /stats/{code}(인증 필요) → 클릭 수와 생성 일자 표시./openapi.json에 자동 생성된 OpenAPI 3.1 스키마./docs에 Scalar UI 로 브라우저에서 테스트 가능.
그럼 시작해봅시다.
설정 (2분)
Fitz 설치:
# Linux / macOS / WSL
curl -sSf https://thegreekman76.github.io/fitz/install.sh | sh
# Windows (PowerShell)
irm https://thegreekman76.github.io/fitz/install.ps1 | iex
또는 릴리즈 페이지에서 미리 컴파일된 바이너리를 다운로드합니다.
VSCode 확장 (이 튜토리얼에 강력히 권장 – 타입 힌트, 자동 완성, 시그니처 도움, 저장 시 포맷팅 제공): 같은 릴리즈 페이지에서 OS에 맞는 fitz-lang-.vsix 를 받아 설치합니다:
code --install-extension fitz-lang-.vsix --force
Language Server 가 .vsix 안에 포함돼 있어 별도 설치가 필요 없습니다. 설치 후 VSCode 를 한 번 재로드하세요.
터미널을 다시 열어 PATH 변경이 적용됐는지 확인합니다:
fitz --version
# fitz 0.15.0
Postgres도 실행 중이어야 합니다. 가장 빠른 방법은 Docker:
docker run -d --name pg-shortener \
-e POSTGRES_PASSWORD=demo \
-e POSTGRES_DB=shortener \
-p 5432:5432 \
postgres:16
프로젝트 생성:
fitz new url-shortener --http
cd url-shortener
--http 플래그는 @get 과 @server 가 이미 연결된 main.fitz 템플릿을 만들어 줍니다. main.fitz 를 열고 편집을 시작합니다.
1단계 — Hello world HTTP
main.fitz 를 아래 최소 코드로 교체합니다:
@server(8080)
fn main() => 0
@get("/health")
fn health() -> Str => "ok"
실행:
fitz dev
fitz dev 는 파일을 감시하고 저장할 때마다 재시작합니다 — uvicorn --reload 와 같은 역할입니다. 다른 터미널에서:
curl localhost:8080/health
# ok
브라우저에서 http://localhost:8080/docs 를 열어 보세요. GET /health 가 문서화된 Scalar UI 가 이미 표시됩니다. 데코레이터를 라우터에 수동으로 등록할 필요도, app.include_router(...) 같은 코드도 없습니다. 컴파일러가 AST 를 읽어 스키마를 자동 생성합니다.
2단계 — 도메인 모델링
Link 는 짧은 코드, 원본 URL, 클릭 카운터, 그리고 소유자를 나타냅니다.
type Link {
@primary id: Int,
code: Str,
target_url: Str,
user_email: Str,
clicks: Int,
created_at: Str,
}
@primary 데코레이터는 기본 키임을 표시합니다. Int 는 생성된 바이너리에서는 i64 로, Postgres에서는 bigint 로 매핑됩니다. ORM 은 컴파일 타임에 타입 정보를 읽어 이해합니다.
하지만 아직 이 type 이 Postgres 테이블이라는 것을 Fitz 에 알려주지 않았습니다. @table 을 추가합니다:
@table("links")
type Link {
@primary id: Int,
code: Str,
target_url: Str,
user_email: Str,
clicks: Int,
created_at: Str,
}
이제 Link.all(db), Link.insert(db, l), Link.where(...), .preload(...) 등 다양한 메서드가 자동으로 제공됩니다. 컴파일러는 .where(...) 안의 클로저가 존재하는 필드만 참조하도록 정적 검사를 수행합니다. 오타는 컴파일 타임에 잡히므로 프로덕션에서 오류가 발생하지 않습니다.
3단계 — Postgres 연결
db.connect(url) 은 커넥션 풀을 엽니다. 부팅 시 한 번만 호출합니다:
let DB_URL = env_or("DATABASE_URL", "postgres://postgres:demo@localhost:5432/shortener")
let db = db.connect(DB_URL)
env_or 는 내장 함수로, 환경 변수를 읽고 없으면 기본값을 사용합니다. 로컬 개발과 Docker 환경을 조건문 없이 모두 지원합니다.
테이블이 존재해야 합니다. 여기서는 스키마 마이그레이션 도구를 사용합니다 — Fitz 에는 fitz db diff 와 fitz db migrate 가 내장돼 있습니다. @table 타입을 선언하고 마이그레이터에게 SQL 생성을 맡깁니다:
@table("links")
type Link {
@primary id: Int,
code: Str,
target_url: Str,
user_email: Str,
clicks: Int = 0,
created_at: Str = "",
}
첫 실행:
$ fitz db diff
+ CREATE TABLE links (
+ id BIGSERIAL PRIMARY KEY,
+ code TEXT NOT NULL,
+ target_url TEXT NOT NULL,
+ user_email TEXT NOT NULL,
+ clicks BIGINT NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL DEFAULT ''
+ );
$ fitz db migrate
✓ applied migration_20260530_links.sql
fitz db diff 는 코드에 있는 @table 타입을 읽고 현재 DB 상태와 비교해 동기화에 필요한 SQL 을 계산한 뒤, 멱등적인 마이그레이션 파일을 생성합니다. fitz db migrate 는 아직 적용되지 않은 마이그레이션을 순서대로 실행합니다. Alembic 과 비슷하지만, 별도의 SQL 파일을 관리할 필요 없이 타입 정의 자체가 진실의 원천이 됩니다.
예를 들어 나중에 Link 에 name: Str 를 추가하면 fitz db diff 가 ALTER TABLE links ADD COLUMN name TEXT NOT NULL 를 생성하고, 다시 fitz db migrate 하면 적용됩니다. 손으로 DDL 을 작성할 필요가 없습니다.
이 튜토리얼에서는 서버를 시작하기 전에 fitz db diff 와 fitz db migrate 를 한 번씩 실행합니다.
4단계 — 생성 + 리다이렉트
두 개의 엔드포인트를 구현합니다. 코드가 얼마나 간결한지 보세요:
type ShortenRequest { target_url: Str }
type ShortenResponse { code: Str, short_url: Str }
@post("/shorten")
async fn shorten(db: DbConn, req: ShortenRequest, user: User) -> ShortenResponse {
let code = generate_code()
let link = Link {
id: 0,
code: code,
target_url: req.target_url,
user_email: user.email,
clicks: 0,
created_at: "", // DB 기본값이 채워짐
}
Link.insert(db, link).await
return ShortenResponse {
code: code,
short_url: "http://localhost:8080/{code}",
}
}
세 가지 주목할 점:
db: DbConn파라미터 — 런타임이 자동으로 커넥션을 주입합니다. Fitz 는 타입만 보고 전역 풀에서 가져오는 것을 알아냅니다.user: User파라미터 — 인증으로부터 제공됩니다. 다음 단계에서 인증 로직을 연결합니다. 컴파일러는 핸들러가@authenticated로 표시돼야 함을 정적으로 검증합니다.id: 0— ORM 에게 “첫 번째 삽입이니 이 필드는 무시하고BIGSERIAL이 자동 할당하게 해라” 라는 신호입니다. ORM 은 자동으로INSERT문에서 해당 필드를 제외합니다.
리다이렉트 엔드포인트:
@get("/{code}")
async fn redirect(db: DbConn, code: Str) -> Result {
let link: Link = match Link.where(fn(l) => l.code == code).first(db).await {
Ok(l) => l,
Err(_) => return Err("not found"),
}
// 클릭 카운터 증가
Link.where(fn(l) => l.id == link.id)
.update(db, { clicks: link.clicks + 1 })
.await?;
// 리다이렉트 응답 반환
return Ok(Redirect(link.target_url))
}
위 코드는 다음을 수행합니다:
code로Link레코드를 조회합니다. 없으면 404 오류 반환.- 조회된 레코드의
clicks를 1 증가시킵니다. - 원본 URL 로 리다