【Python】FastAPI ~ ファイルアップロード ~

◾️はじめに

https://dk521123.hatenablog.com/entry/2025/10/05/014051

の続き。

今回は、Python の WebフレームワークであるFastAPIでの
ファイルアップロードについて扱う

# これで、あと、FastAPI のDBの扱いさえ覚えれば、
# バックエンド側でやりたいことができそう

目次

【1】インストール
【2】例1:Web server
 1)ファイル構成
 2)コード
 3)サーバ起動および動作確認
【3】例2:REST server + Frontend
 1)ファイル構成
 2)コード
 3)サーバ起動および動作確認

【1】インストール

pip install fastapi uvicorn jinja2 aiofiles python-multipart

【2】例1:Web server

* 画像又はPDFをアップロードし、画面に表示する

1)ファイル構成

app
├── main.py
├── templates
│   └── upload_form.html
└── uploads ... アップロードしたファイルを溜め込む

2)コード

app/main.py

from fastapi import FastAPI, File, UploadFile, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import shutil
import os

os.makedirs("uploads", exist_ok=True)

app = FastAPI()

# Set up templates
templates = Jinja2Templates(directory="templates")

# Mount the upload directory as static files
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")

# Show upload form
@app.get("/", response_class=HTMLResponse)
async def upload_form(request: Request):
    return templates.TemplateResponse("upload_form.html", {"request": request})

# Save and display uploaded file
@app.post("/upload", response_class=HTMLResponse)
async def upload_file(request: Request, file: UploadFile = File(...)):
    upload_dir = "uploads"
    os.makedirs(upload_dir, exist_ok=True)

    file_path = os.path.join(upload_dir, file.filename)
    
    with open(file_path, "wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    if file.content_type.startswith("image/"):
        file_type = "image"
    elif file.content_type == "application/pdf":
        file_type = "pdf"
    else:
        file_type = "other"

    # After saving, display the file
    file_url = f"/uploads/{file.filename}"
    return templates.TemplateResponse(
        "upload_form.html",
        {
            "request": request,
            "file_url": file_url,
            "file_type": file_type
        }
    )

app/templates/upload_form.html

<!DOCTYPE html>
<html>
<head>
    <title>Upload demo</title>
</head>
<body>
    <h1>Upload demo</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="application/pdf,image/*">
        <button type="submit">Upload</button>
    </form>

    {% if file_url %}
        <h2>Uploaded File:</h2>
        <img src="{{ file_url }}" alt="Uploaded File" style="max-width: 300px;">
    {% endif %}
</body>
</html>

3)サーバ起動および動作確認

uvicorn main:app --reload

# main: ファイル名(main.py)
# app: FastAPI のインスタンス名
# --reload: 自動リロード

# ブラウザで以下にアクセスし、画像もしくはPDFをアップロードしてみる

http://localhost:8000/

【3】例2:REST server + Frontend

1)ファイル構成

├── backend
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
├── frontend
│   ├── Dockerfile
│   └── index.html
├── docker-compose.yml

2)コード

backend/main.py

from asyncio.log import logger
import os
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
import shutil


app = FastAPI()

# CORS settings for Development
app.add_middleware(
    CORSMiddleware,
    # Allow the frontend URL
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/upload/")
async def upload_file(upload_file: UploadFile = File(...)):
    try:
        # Save the uploaded file
        file_path = os.path.join(UPLOAD_DIR, upload_file.filename)
        with open(file_path, "wb") as file:
            file.write(await upload_file.read())
    except Exception as ex:
        logger.error(f"File upload error: {ex}")
        return {
            "status": "error",
            "error": str(ex)
        }

    if upload_file.content_type.startswith("image/"):
        file_type = "image"
    elif upload_file.content_type == "application/pdf":
        file_type = "pdf"
    else:
        file_type = "other"

    return {
        "status": "success",
        "filename": upload_file.filename,
        "file_type": file_type
    }

frontend/index.html

<!DOCTYPE html>
<html>
<head>
    <title>OCR Upload</title>
</head>
<body>
    <h1>Upload Image or PDF</h1>
    <input type="file" id="fileInput" />
    <button onclick="uploadFile()">Upload</button>
    <pre id="result"></pre>

    <script>
async function uploadFile() {
    const fileInput = document.getElementById("fileInput");
    const file = fileInput.files[0];
    const formData = new FormData();
    formData.append("upload_file", file);

    try {
        const response = await fetch("http://localhost:8000/upload/", {
            method: "POST",
            body: formData,
        });

        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }

        const result = await response.json();
        document.getElementById("result").innerText = JSON.stringify(result, null, 2);
    } catch (error) {
        console.error("Upload failed:", error);
        alert("Upload failed. Check console for details.");
    }
}
    </script>
</body>
</html>

backend/Dockerfile

# backend/Dockerfile

FROM python:3.13-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    tesseract-ocr \
    tesseract-ocr-jpn \
    fonts-noto-cjk \
    poppler-utils \
    build-essential \
    gcc \
    libgl1 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY . /app

RUN mkdir -p /app/uploads

RUN pip install --no-cache-dir -r requirements.txt

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/Dockerfile

FROM python:3.13-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# RUN pip install --no-cache-dir -r requirements.txt

WORKDIR /app

COPY . /app

CMD ["python3", "-m", "http.server", "3000"]

docker-compose.yml

services:
  backend:
    build: ./backend
    container_name: fastapi_app
    ports:
      - "8000:8000"
  frontend:
    build: ./frontend
    container_name: frontend_app
    ports:
      - "3000:3000"
    depends_on:
      - backend

3)サーバ起動および動作確認

docker compose up --build

# docker compose down -v

http://localhost:3000

にアクセスして、PDF/画像をアップロードしてみる

4)注意点

* 変数名は、一致させておく必要がある
 => サンプルで言うと「upload_file」

backend/main.py

async def upload_file(upload_file: UploadFile = File(...)):

frontend/index.html

    formData.append("upload_file", file);

関連記事

FastAPI ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2025/10/05/014051
FastAPI ~ 基本編 ~
https://dk521123.hatenablog.com/entry/2025/10/09/001303
Flask ~ SQLAlchemy / 入門編 ~
https://dk521123.hatenablog.com/entry/2018/09/19/223200
Flask ~ SQLAlchemy / 基本編 ~
https://dk521123.hatenablog.com/entry/2018/09/23/165130
Python ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2014/08/07/231242
Python ~ 基本編 / 文字列 ~
https://dk521123.hatenablog.com/entry/2019/10/12/075251
Python ~ PyFPDF ~
https://dk521123.hatenablog.com/entry/2023/07/19/001703
Python 〜 PDF to TEXT 〜
https://dk521123.hatenablog.com/entry/2025/10/04/214922
Python ~ 画像処理 / Pillow ~
https://dk521123.hatenablog.com/entry/2023/07/10/000000
Python 〜 Tesseract OCR
https://dk521123.hatenablog.com/entry/2025/10/03/141326