Back to Scaling Python Applications guides

Uploading Files Using FastAPI: A Complete Guide to Secure File Handling

Stanley Ulili
Updated on July 2, 2025

FastAPI has changed how developers build web APIs. It's fast, easy to use, and handles complex tasks like file uploads without the usual headaches.

File uploading is crucial for modern web apps, but developers often mess it up. They create security holes or build systems that frustrate users. FastAPI gives you the tools to do it right.

This guide shows you how to build file upload systems that work in production. You'll learn everything from basic single-file uploads to advanced features like progress tracking and multi-file processing.

Prerequisites

You need Python 3.8 or newer on your computer. This tutorial assumes you know Python basics, understand async/await, and have built web apps before.

You should also understand HTTP multipart forms since file uploads use this protocol to send binary data along with form fields.

Setting up your FastAPI file upload service

Building a solid file upload system starts with good planning and project structure. You need a foundation that grows with your app.

Create a new project directory and set up a clean development environment:

 
mkdir fastapi-file-uploads && cd fastapi-file-uploads
 
python3 -m venv venv
 
source venv/bin/activate

Install the packages you need for handling files:

 
pip install fastapi "uvicorn[standard]" python-multipart pillow python-magic

Here's what each package does:

  • fastapi: The main framework that handles file uploads and requests.
  • uvicorn[standard]: The server that runs your app with performance features and auto-reload for development.
  • python-multipart: Parses the multipart form data that carries your uploaded files.
  • pillow: Processes, validates, and transforms uploaded images.
  • python-magic: Detects file types based on content, not just file extensions.

Start by creating a simple FastAPI server to verify everything works. Create your main.py file with just the basics:

main.py
from fastapi import FastAPI

app = FastAPI(title="FastAPI File Upload Service")

@app.get("/")
async def root():
    return {"message": "FastAPI File Upload Service is running"}

Test this minimal setup first to make sure your environment is working correctly. Start the server:

 
uvicorn main:app --reload
Output
INFO:     Will watch for changes in these directories: ['/Users/developer/fastapi-file-uploads']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [12345] using WatchFiles
INFO:     Started server process [12347]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Visit http://localhost:8000/ in your browser. You should see:

 
{"message": "FastAPI File Upload Service is running"}

Screenshot showing the basic FastAPI response in a browser with the JSON message confirming the service is running

Your FastAPI server is working.

Getting started and testing your setup

Now let's add file upload functionality. Replace your main.py content with this expanded version:

main.py
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from typing import List
import os
import shutil
from pathlib import Path
# Create upload directory
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
app = FastAPI(title="FastAPI File Upload Service")
@app.post("/upload/single")
async def upload_single_file(file: UploadFile = File(...)):
"""Upload a single file with basic validation"""
if file.filename == "":
raise HTTPException(status_code=400, detail="No file selected")
file_path = UPLOAD_DIR / file.filename
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {
"filename": file.filename,
"content_type": file.content_type,
"size": file.size,
"location": str(file_path)
}
@app.get("/") async def root(): return {"message": "FastAPI File Upload Service is running"}

This code adds file upload functionality to your basic server. The UploadFile type gives you access to the file's information and content. The File(...) parameter makes the field required. Using shutil.copyfileobj streams the file to disk efficiently without loading everything into memory at once.

The uvicorn server should automatically restart when you save the file changes. If it didn't restart automatically, stop the server (Ctrl+C) and start it again:

 
uvicorn main:app --reload

Visit http://localhost:8000/docs to access FastAPI's automatic interactive documentation. This is where you'll test your file upload functionality.

Screenshot of FastAPI's SwaggerUI documentation interface showing the file upload endpoints and interactive testing features

The interactive docs show one of FastAPI's best features: automatic API documentation that stays up-to-date with your code. You never have outdated docs, and you get immediate feedback while developing.

Click on the POST /upload/single endpoint to expand it, then click "Try it out" to test your file upload functionality. You'll see a file selection button that lets you choose a file from your computer:

Screenshot of the try it out

Select a small test file and click "Execute". If you don't have a test file ready, create a simple text file on your computer:

  1. Open a text editor (Notepad on Windows, TextEdit on Mac, or any code editor)
  2. Type some sample content like "This is a test file for FastAPI upload"
  3. Save it as test-file.txt on your desktop or downloads folder

Select a small test file and click "Execute".

Screenshot of the "Execute" button

You should now see a successful response with details about your uploaded file:

 
{
  "filename": "test-file.txt",
  "content_type": "text/plain",
  "size": 38,
  "location": "uploads/test-file.txt"
}

Screenshot showing a successful file upload response in SwaggerUI with JSON output displaying filename, content type, size, and location

Check your project directory - you should now see an uploads folder containing your test file. This confirms your basic file upload system is working correctly.

Building comprehensive file validation systems

Your basic upload works, but it's not safe for production. Currently, users can upload files of any type and size. This creates security risks and can crash your system.

You need validation that checks file types and sizes before accepting uploads. Let's add this to your existing upload endpoint.

Create a new file called validators.py:

validators.py
from pathlib import Path
from fastapi import UploadFile

class DocumentValidator:
    def __init__(self, max_size: int = 10 * 1024 * 1024):  # 10MB default
        self.max_size = max_size
        self.allowed_extensions = {'.pdf', '.txt', '.json'}

    async def validate_file(self, file: UploadFile) -> dict:
        """Check if the document file is valid"""
        result = {"valid": True, "errors": []}

        # Check if user selected a file
        if not file.filename or file.filename.strip() == "":
            result["valid"] = False
            result["errors"].append("No file selected")
            return result

        # Check file extension
        file_ext = Path(file.filename).suffix.lower()
        if file_ext not in self.allowed_extensions:
            result["valid"] = False
            result["errors"].append(
                f"File extension '{file_ext}' not allowed. Use: .pdf, .txt, or .json"
            )

        # Read file to check size
        content = await file.read()
        await file.seek(0)  # Reset file pointer for later use

        # Check file size
        file_size = len(content)
        if file_size > self.max_size:
            result["valid"] = False
            result["errors"].append(
                f"File too large ({file_size:,} bytes). Maximum: {self.max_size:,} bytes"
            )

        return result

Now update your main.py file to add validation to your existing endpoint:

main.py
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from typing import List
import os
import shutil
import uuid
from pathlib import Path
from datetime import datetime
from validators import DocumentValidator
# Create upload directory UPLOAD_DIR = Path("uploads") UPLOAD_DIR.mkdir(exist_ok=True) app = FastAPI(title="FastAPI File Upload Service")
# Create validator instance
doc_validator = DocumentValidator(max_size=25 * 1024 * 1024) # 25MB limit
@app.post("/upload/single") async def upload_single_file(file: UploadFile = File(...)):
"""Upload a single file with validation"""
# Validate the file first
validation = await doc_validator.validate_file(file)
if not validation["valid"]:
raise HTTPException(
status_code=400,
detail={
"message": "File validation failed",
"errors": validation["errors"]
}
)
# Create unique filename to prevent conflicts
file_ext = Path(file.filename).suffix
unique_filename = f"{uuid.uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_filename
try:
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to save file: {str(e)}"
)
return {
"success": True,
"original_filename": file.filename,
"stored_filename": unique_filename,
"content_type": file.content_type, "size": file.size,
"upload_time": datetime.utcnow().isoformat(),
"location": str(file_path) } @app.get("/") async def root(): return {"message": "FastAPI File Upload Service is running"}

Your upload endpoint now validates files before saving them and uses unique filenames to prevent conflicts.

Restart your server:

 
uvicorn main:app --reload

Go to http://localhost:8000/docs and test your /upload/single endpoint with your text file. You should see a successful response.

Now try uploading a file with a different extension (like creating a file named test.jpg). You should get an error:

 
{
  "detail": {
    "message": "File validation failed",
    "errors": [
      "File extension '.jpg' not allowed. Use: .pdf, .txt, or .json"
    ]
  }
}

Screenshot of the file validation failed message

Your validation system is working. It protects your app while giving users clear feedback.

Handling multiple file uploads

Single-file uploads work great, but users often need to upload multiple files simultaneously. Think photo albums, document batches, or backup files. FastAPI makes this easy with a small change to your endpoint.

You need to handle multiple files while keeping the same validation and error handling. Let's add a new endpoint that processes several files at once.

Add this new endpoint to your main.py file:

main.py
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from typing import List
import os
import shutil
import uuid
from pathlib import Path
from datetime import datetime
from validators import DocumentValidator

# Create upload directory
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

app = FastAPI(title="FastAPI File Upload Service")

# Create validator instance
doc_validator = DocumentValidator(max_size=25 * 1024 * 1024)  # 25MB limit

@app.post("/upload/multiple")
async def upload_multiple_files(files: List[UploadFile] = File(...)):
"""Upload multiple files with validation"""
if len(files) > 10: # Limit number of files
raise HTTPException(
status_code=400,
detail="Too many files. Maximum 10 files allowed."
)
results = []
for file in files:
# Validate each file
validation = await doc_validator.validate_file(file)
if not validation["valid"]:
results.append({
"filename": file.filename,
"success": False,
"errors": validation["errors"]
})
continue
# Save valid files
file_ext = Path(file.filename).suffix
unique_filename = f"{uuid.uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_filename
try:
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
results.append({
"filename": file.filename,
"stored_filename": unique_filename,
"success": True,
"location": str(file_path)
})
except Exception as e:
results.append({
"filename": file.filename,
"success": False,
"errors": [f"Failed to save: {str(e)}"]
})
successful = [r for r in results if r["success"]]
failed = [r for r in results if not r["success"]]
return {
"total_files": len(files),
"successful": len(successful),
"failed": len(failed),
"upload_time": datetime.utcnow().isoformat(),
"results": results
}
@app.post("/upload/single") async def upload_single_file(file: UploadFile = File(...)): """Upload a single file with validation""" ... @app.get("/") async def root(): return {"message": "FastAPI File Upload Service is running"}

The new endpoint changes the parameter from UploadFile to List[UploadFile]. This tells FastAPI to expect multiple files. The code validates each file individually and continues processing even if some files fail.

Restart your server:

 
uvicorn main:app --reload

Go to http://localhost:8000/docs and you'll see your new /upload/multiple endpoint. Create a few test files on your computer:

  1. Create test1.txt with some content
  2. Create test2.json with {"message": "test file"}
  3. Create test3.jpg (this should fail validation)

Click on the POST /upload/multiple endpoint and select "Try it out". The file upload interface now allows multiple file by clicking the "Add string item" for each file:

Screenshot of the multiple file upload interface in SwaggerUI showing the ability to select multiple files at once

Select your test files and click "Execute". You should see a response like:

Screenshot of the output

Output
{
  "total_files": 3,
  "successful": 2,
  "failed": 1,
  "upload_time": "2025-07-02T14:20:16.670490",
  "results": [
    {
      "filename": "test1.txt",
      "stored_filename": "2bba438e-f738-4d3b-ab5d-4922d8f5e448.txt",
      "success": true,
      "location": "uploads/2bba438e-f738-4d3b-ab5d-4922d8f5e448.txt"
    },
    {
      "filename": "test2.json",
      "stored_filename": "2a4e588a-ed43-4fc5-bb94-b27a61e8bfb4.json",
      "success": true,
      "location": "uploads/2a4e588a-ed43-4fc5-bb94-b27a61e8bfb4.json"
    },
    {
      "filename": "test3.jpg",
      "success": false,
      "errors": [
        "File extension '.jpg' not allowed. Use: .pdf, .txt, or .json"
      ]
    }
  ]
}

Your multiple file upload system processes each file individually and gives you clear feedback. The valid files get saved while invalid ones are rejected with helpful error messages.

Final thoughts

You've built a complete file upload system using FastAPI that handles single files, multiple files, and validates everything before saving. Your system now protects against dangerous uploads while giving users clear feedback when something goes wrong.

Your upload system includes basic file functionality, validation by extension and size, unique filename generation to prevent conflicts, and multiple file processing with individual validation. When uploads fail, users get clear error messages explaining what went wrong.

Next, you could add features like file deletion endpoints, image resizing, or cloud storage integration. The foundation you've built makes these additions straightforward.

For more advanced features and deployment tips, check out the FastAPI documentation and consider adding a database to track file metadata.

Happy uploading!

Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github