Fitz로 첫 API 만들기: Postgres와 인증을 활용한 URL 단축기, 30분 안에 완성

발행: (2026년 6월 9일 PM 07:12 GMT+9)
10 분 소요
원문: Dev.to

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 에 제공됩니다.

시작합니다.

설정 (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 difffitz 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 과 비슷하지만, 소스 오브 트루스가 파일이 아닌 타입 선언이라는 점이 다릅니다.

예를 들어 Linkname: Str 필드를 추가하면 fitz db diffALTER TABLE links ADD COLUMN name TEXT NOT NULL 를 생성하고, 다시 fitz db migrate 하면 자동으로 적용됩니다. 직접 DDL 을 작성할 필요가 없습니다.

이 튜토리얼에서는 서버를 올리기 전에 한 번 fitz db difffitz 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가지

  1. db: DbConn 파라미터 – 런타임이 전역 풀에서 자동으로 연결을 주입합니다.
  2. user: User 파라미터 – 인증 단계에서 전달됩니다. 다음 단계에서 인증 로직을 연결합니다. 컴파일러는 핸들러에 @authenticated 데코레이터가 있어야 함을 정적으로 검증합니다.
  3. 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);
}

(코드가 중간에 끊겼지만, 위와 같은 형태로 구현됩니다.)

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...