Fitz로 첫 API 만들기: Postgres와 인증을 활용한 URL 단축기, 30분 안에 완성
Source: Dev.to
Fitz가 제공하는 단계별 튜토리얼. fitz new 로 시작해 Docker에서 실행되는 네이티브 바이너리까지 완성합니다. 외부 의존성 없이. pip install 없이. 타입이 지정된 Postgres, JWT 인증, 자동 생성된 OpenAPI만 사용합니다.
우리가 만들게 될 것
URL 단축기와 실제 API에 필요한 네 가지 요소:
- 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 3.1 스키마가
/openapi.json에 제공됩니다. - 브라우저에서 테스트할 수 있는 Scalar UI가
/docs에 제공됩니다.
- 자동 생성된 OpenAPI 3.1 스키마가
시작합니다.
설정 (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
또는 releases 페이지에서 미리 컴파일된 바이너리를 다운로드합니다.
VSCode 확장 (강력히 권장)
이 튜토리얼을 위해서는 타입 힌트, 자동 완성, 시그니처 도움, 저장 시 포맷팅 등을 제공하는 확장이 필요합니다. 같은 releases 페이지에서 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 를 열면 Scalar UI가 나타나고 GET /health 가 문서화된 것을 볼 수 있습니다. 데코레이터가 라우트를 등록하거나 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 테이블임을 선언하지 않았으니 @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 는 스키마 마이그레이션 도구를 내장하고 있습니다. @table 로 선언한 타입을 기반으로 fitz db diff 와 fitz db migrate 를 사용합니다:
@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 을 생성하고, idempotent 마이그레이션 파일을 출력합니다. fitz db migrate 는 아직 적용되지 않은 마이그레이션을 순서대로 실행합니다. Alembic 과 비슷하지만, 소스 오브 트루스가 파일이 아닌 타입 선언이라는 점이 다릅니다.
예를 들어 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 = generar_codigo()
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}",
}
}
눈여겨볼 점 3가지
db: DbConn파라미터 – 런타임이 전역 풀에서 자동으로 연결을 주입합니다.user: User파라미터 – 인증 단계에서 전달됩니다. 다음 단계에서 인증 로직을 연결합니다. 컴파일러는 핸들러에@authenticated데코레이터가 있어야 함을 정적으로 검증합니다.id: 0– ORM 에게 “첫 번째 INSERT 이므로 ID 필드를 무시하고BIGSERIAL이 자동 할당하게 해라”는 신호입니다. 실제INSERT문에서는 이 필드가 제외됩니다.
리다이렉션 엔드포인트:
@get("/{code}")
async fn redirect(db: DbConn, code: Str) -> Redirect {
let link = Link.where(db, |l| l.code == code).first().await?;
Link.update(db, link.id, |l| l.clicks += 1).await?;
return Redirect::to(link.target_url);
}
(코드가 중간에 끊겼지만, 위와 같은 형태로 구현됩니다.)