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

[FastAPI](https://fastapi.tiangolo.com/) 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.

[ad-logs]

## Prerequisites

You need [Python 3.8](https://www.python.org/downloads/) 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:

```command
mkdir fastapi-file-uploads && cd fastapi-file-uploads
```

```command
python3 -m venv venv
```
```command
source venv/bin/activate
```

Install the packages you need for handling files:

```command
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:

```python
[label 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:

```command
uvicorn main:app --reload
```

```text
[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:

```json
{"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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6d30cb86-be8b-46b1-faad-44ff5d1bed00/md2x =3248x1996)

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:

```python
[label main.py]
[highlight]
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)
    }
[/highlight]

@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:

```command
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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/2758deb9-3222-474a-f411-433cabbef700/lg2x =3248x1996)

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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/704dcd7c-b891-47c4-ec7c-3a8b70f09700/lg2x =3248x1996)


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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/39320672-c2eb-46cb-eb24-8419056ab100/md2x =3248x1996)


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

```json
{
  "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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/86809511-a357-4821-debd-8d8e75395000/public =3248x1996)

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`:

```python
[label 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:

```python
[label main.py]
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from typing import List
import os
import shutil
[highlight]
import uuid
[/highlight]
from pathlib import Path
[highlight]
from datetime import datetime
from validators import DocumentValidator
[/highlight]

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

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

[highlight]
# Create validator instance
doc_validator = DocumentValidator(max_size=25 * 1024 * 1024)  # 25MB limit
[/highlight]

@app.post("/upload/single")
async def upload_single_file(file: UploadFile = File(...)):
[highlight]
    """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)}"
        )
[/highlight]
    
    return {
[highlight]
        "success": True,
        "original_filename": file.filename,
        "stored_filename": unique_filename,
[/highlight]
        "content_type": file.content_type,
        "size": file.size,
[highlight]
        "upload_time": datetime.utcnow().isoformat(),
[/highlight]
        "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:

```command
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:

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

![Screenshot of the file validation failed message](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ff4ea66b-7783-42f6-0543-dd8bb5bdc100/lg1x =3248x1996)

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:

```python
[label 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

[highlight]
@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
    }
[/highlight]

@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:

```command
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](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/4d7ead20-73c1-4cf5-4c00-0a69e5708900/lg1x =3248x1996)

Select your test files and click "Execute". You should see a response like:
![Screenshot of the output](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/7477c90d-3f7f-4891-2185-c4030740c500/lg1x =3248x1996)

```json
[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](https://fastapi.tiangolo.com/) and consider adding a database to track file metadata.

Happy uploading!

