Belajar FastAPI - Upload File & Static Files
Episode 15 of 28

Belajar FastAPI - Upload File & Static Files

Menerima file dengan UploadFile dan File, memvalidasi tipe MIME dan ukuran, menyimpan file dengan nama aman, serta melayani static files dan media yang telah diunggah lewat StaticFiles dan FileResponse.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 14 API kalian bisa bekerja di belakang layar dengan background tasks dan database async, sekarang kita tangani salah satu kebutuhan paling umum di aplikasi nyata: file. Avatar user, lampiran, dokumen, gambar produk — hampir tidak ada aplikasi yang tidak pernah menerima upload.

Mengapa episode ini penting? Karena upload file adalah salah satu titik paling berbahaya di API: eksekusi file berbahaya, path traversal, dan file yang menghabiskan disk adalah risiko nyata. Episode ini membangun upload yang aman — dan melayani file kembali ke klien dengan benar.

Persiapan: python-multipart

Upload memakai multipart/form-data, yang butuh package python-multipart (sama seperti Form di episode 6):

Install python-multipart
pip install python-multipart

Tanpa ini, FastAPI error saat import File atau UploadFile.

Endpoint Upload Pertama

Dua cara menerima file: bytes (seluruh file di memory) atau UploadFile (streaming, cocok untuk file besar):

PythonUpload dengan UploadFile
from pathlib import Path
 
from fastapi import FastAPI, File, UploadFile
 
app = FastAPI()
 
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
 
 
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)) -> dict[str, str]:
    destination = UPLOAD_DIR / file.filename
    with destination.open("wb") as buffer:
        while chunk := await file.read(1024 * 1024):
            buffer.write(chunk)
    return {"filename": file.filename, "size": destination.stat().st_size}

Poin penting:

  • UploadFile membaca file bertahap (chunk) — tidak memuat file 2 GB ke memory.
  • while chunk := await file.read(...) adalah idiom Python 3.8+ untuk membaca sampai habis.
  • file.filename berisi nama asli dari klien.

Caution

Kode di atas memakai file.filename langsung sebagai nama file tujuan — jangan pernah lakukan ini di produksi. Nama dari klien bisa mengandung path traversal (../../etc/passwd) dan karakter berbahaya. Kita perbaiki di bagian "Nama File yang Aman".

Validasi Tipe dan Ukuran

File yang masuk harus divalidasi. Dua lapis: tipe (yang diperbolehkan) dan ukuran (yang wajar):

PythonUpload dengan validasi
from fastapi import FastAPI, File, HTTPException, UploadFile
 
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp"}
MAX_SIZE = 5 * 1024 * 1024  # 5 MB
 
 
@app.post("/images/")
async def upload_image(file: UploadFile = File(...)) -> dict[str, str]:
    if file.content_type not in ALLOWED_TYPES:
        raise HTTPException(
            status_code=415,
            detail="Hanya JPG, PNG, dan WebP yang diizinkan",
        )
 
    size = 0
    destination = Path(f"uploads/{file.filename}")
    with destination.open("wb") as buffer:
        while chunk := await file.read(1024 * 1024):
            size += len(chunk)
            if size > MAX_SIZE:
                destination.unlink(missing_ok=True)
                raise HTTPException(
                    status_code=413,
                    detail="Ukuran file melebihi 5 MB",
                )
            buffer.write(chunk)
    return {"filename": file.filename, "size": size}

Perhatikan: validasi ukuran dilakukan saat membaca chunk, bukan setelah selesai — sehingga file yang terlalu besar ditolak sebelum menghabiskan disk.

Warning

file.content_type datang dari klien dan bisa dipalsukan. Untuk keamanan nyata, verifikasi isi file — misalnya dengan memeriksa magic bytes (png, jpeg, webp) atau library seperti Pillow untuk gambar. Content type klien hanya lapis pertama.

Nama File yang Aman

Selalu buat nama penyimpanan sendiri — jangan percaya nama dari klien. Pola umum: UUID + ekstensi yang disanitasi:

PythonSanitasi nama file
import uuid
from pathlib import Path
 
from fastapi import UploadFile
 
 
def safe_filename(file: UploadFile) -> str:
    ext = Path(file.filename or "").suffix.lower()
    allowed_ext = {".jpg", ".jpeg", ".png", ".webp"}
    if ext not in allowed_ext:
        raise ValueError("ekstensi tidak diizinkan")
    return f"{uuid.uuid4().hex}{ext}"

Dengan pola ini, file tersimpan sebagai a1b2c3....png — unik, tanpa path traversal, dan ekstensi tetap dipertahankan (penting untuk content-type saat di-serve).

Upload via Form + File Sekaligus

Upload nyata sering menyertakan metadata. Kombinasi Form dan UploadFile dalam satu endpoint:

PythonFile + form field
from fastapi import FastAPI, File, Form, UploadFile
 
app = FastAPI()
 
 
@app.post("/documents/")
async def upload_document(
    title: str = Form(...),
    category: str = Form("general"),
    file: UploadFile = File(...),
) -> dict[str, str]:
    return {"title": title, "category": category, "file": file.filename}

Klien mengirim multipart dengan field title, category, dan file — frontend <form> dengan enctype="multipart/form-data" bisa langsung memakai endpoint ini.

Static Files: Melayani File

Untuk melayani file yang sudah diunggah (dan asset frontend), gunakan StaticFiles:

PythonServe static dan media
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
 
app = FastAPI()
 
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
app.mount("/static", StaticFiles(directory="static"), name="static")

Setelah mount, file uploads/abc.png bisa diakses di http://host/uploads/abc.png — tanpa menulis endpoint. Perbedaan mount vs path operation: mount mendelegasikan seluruh sub-path ke aplikasi Starlette terpisah, bukan melewati routing FastAPI.

Tip

Perhatikan urutan: mount/path operations tidak boleh bentrok. Jika kalian punya route /uploads/ sebagai endpoint, jangan mount StaticFiles di path yang sama. Di produksi, file media biasanya dilayani langsung oleh CDN/reverse proxy (episode 24) karena lebih efisien.

Download File dengan FileResponse

Untuk men-download file sebagai attachment:

PythonFileResponse untuk download
from pathlib import Path
 
from fastapi.responses import FileResponse
 
@app.get("/uploads/{filename}")
def download_file(filename: str) -> FileResponse:
    file_path = Path("uploads") / filename
    if not file_path.is_file():
        raise HTTPException(status_code=404, detail="File tidak ditemukan")
    return FileResponse(
        file_path,
        media_type="application/octet-stream",
        filename=file_path.name,
    )

FileResponse menangani streaming file besar tanpa memuat ke memory — dan dengan filename=..., browser akan menawarkan download, bukan membukanya.

Common Pitfalls

PitfallSolusi
Nama file klien dipakai langsungSanitasi + UUID
Validasi hanya content typeVerifikasi magic bytes/isi
Ukuran tidak dibatasiTolak saat chunk melebihi batas
Upload ke memory dengan bytesUploadFile + chunk untuk file besar
Path traversalJangan terima path dari klien

Penutup

Inti yang harus dibawa pulang:

  • UploadFile untuk streaming; butuh python-multipart.
  • Validasi tipe dan ukuran file — content type klien bisa dipalsukan.
  • Simpan file dengan UUID + ekstensi aman, bukan nama klien.
  • StaticFiles untuk melayani media; FileResponse untuk download.

Di episode 16 selanjutnya kita akan membahas configuration & environment — memindahkan secret dan setting ke pydantic-settings dengan file .env, membuat konfigurasi terpusat yang bertipe, serta memakai environment-specific config. Ini langkah penting menuju deployment!

Belajar FastAPI - Upload File & Static Files | Belajar FastAPI