Build a simple file uploader with Django (and Alpine.js) ๐๏ธ๐
Many Django tutorials don't get file uploads right.
Even the official docs โ typically very good โ demonstrate server-side uploads. This is bad for a live application: without extra technologies, your server will freeze if users upload multiple large files server-side at the same time.
Instead, the effective way is to upload files straight from the user's web browser (client-side) to your file hosting service, such as AWS S3.
I've shown how to do this with HTMX before.
This guide uses Alpine.js and is even faster to add. I challenge you to complete the guide in under 4 minutes and 30 seconds ๐
Optional video tutorial (featuring me ๐๐ฟ) is here:
0. Setup
Create your Django app:
- Install Django and create a new Django app
pip install django boto3 python-dotenv
django-admin startproject core .
python manage.py startapp sim- Register your new app by adding it to your INSTALLED_APPSinsettings.py.
# settings.py
 
INSTALLED_APPS = [
    ...
    'sim',
    ...
]1. Connect our file hosting service (AWS S3)
Joke: Why did the file apply for a job at AWS?
It was on its S3 bucket list.
Connecting your AWS account (and S3 bucket ๐ชฃ) is fast to do. Each step is very small.
1.1 Create/login account
- Create/login to an AWS account at http://console.aws.amazon.com. There is a free tier.
1.2 Create an IAM user with S3 access
- 
Visit the IAM service (Click on the IAM service) 
- 
Create an IAM user to access your account ( IAM>Users>Create user). Use any username.
- 
On 'Set Permissions':- 
Click -> 'Attach policies directly'
- 
Find and select -> AmazonS3FullAccess(full S3 read/write access to your IAM role).
 
- 
(Search for AmazonS3FullAccess , rather than scrolling through the 1131 available permissions policies).
 
- Click to 'Create user'
1.3 Get your AWS access keys for your new user
- Click on your newly created user (Trivia: My user's username is "BananaGod" ๐๐).
- Under, Access keys, clickCreate access key
- Click any use case
- Ignore the Set description tag
- Click Create access keyand leave this page open. We'll need the key shortly.
1.4 Create an S3 bucket to store your uploads
- Go to the S3 service at https://s3.console.aws.amazon.com/
- Click the button 'Create bucket'
- Enter a globally unique name for your bucket.
- The other settings don't matter for us. Click to 'Create bucket' ๐ชฃ
1.5 Update your S3 bucket permissions to allow our upload
- Go to your new bucket's permission (Click on it -> 'Permissions')
- Scroll down to the "Cross-origin resource sharing (CORS)" section and click "Edit".
- Paste the below :
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]- Save the changes.
1.6 Store your AWS access keys in Django
- Create a file called .envinto your project atcore/.env
- Add your AWS access keys and bucket name. Don't wrap the contents with speech marks.
AWS_ACCESS_KEY_ID=123123-eefse-your-key
AWS_SECRET_ACCESS_KEY=123123-eefse-another-key
BUCKET_NAME=your-bucket-name
- Click 'Done' on the AWS Access keys page
Note: these keys allow you to access your aws account. Don't upload them to github. If you want to share your repo, add your .env file to a .gitignore file to make git ignore the file with your keys.
1.7 Add your AWS keys to your app
- Add these lines to the top of yourย core/settings.pyย to load the AWS keys from yourย.envย file into your Django app as environment variables when you run your Django server.
from pathlib import Path
from dotenv import load_dotenv
import os
 
load_dotenv()
 
# The below are optional checks that we've connected the keys.
if not os.getenv('AWS_ACCESS_KEY_ID'):
    print('Missing your AWS_ACCESS_KEY_ID')
if not os.getenv('AWS_SECRET_ACCESS_KEY'):
    print('Missing your AWS_SECRET_ACCESS_KEY')
if not os.getenv('BUCKET_NAME'):
    print('Missing your BUCKET_NAME')2. Generate your upload url with Django (aka 'Now the fun begins')
Generate a presigned URL
- In sim/services.py, create a view to handle generating a presigned URL:
import os
import boto3
 
def generate_presigned_post(bucket_name, filename, expiration=600):
    """
    Docs: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/generate_presigned_post.html#generate-presigned-post
    """
    s3_client = boto3.client(
        's3',
        aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'),
        aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY')
    )
    return s3_client.generate_presigned_post(
        Bucket=bucket_name, Key=filename,
        ExpiresIn=expiration,
    )3. Create a view to render the template
- In sim/views.py, add:
import json
import os
 
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.shortcuts import render
from django.http import JsonResponse
from .services import generate_presigned_post
 
@method_decorator(csrf_exempt, name='dispatch')
def uploader(request):
    if request.method == 'GET':
        return render(request, 'upload.html')
 
    elif request.method == 'POST':
        body = json.loads(request.body)
        file = body.get('file')
        if not file:
            return JsonResponse({'error': 'Missing file in request body'}, status=400)
 
        presigned_data = generate_presigned_post(
            bucket_name=os.getenv('BUCKET_NAME'), filename=file['name']
        )
        return JsonResponse(presigned_data)Add your urls routing
- Add the below to core/urls.py:
from django.contrib import admin
from django.urls import include, path
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]- Create sim/urls.pyand add:
from django.urls import path
from . import views
 
urlpatterns = [
    path('uploader/', views.uploader, name='generate_presigned_url'),
]
 4. Add your template and Alpine.js file uploader:
- Create a template folder at sim/templates
- Add a HTML file at sim/templates/upload.htmlwith the below:
<!doctype html>
<html
  lang="en"
  xmlns:x-on="http://www.w3.org/1999/xhtml"
  xmlns:x-bind="http://www.w3.org/1999/xhtml"
>
  <head>
    <meta charset="UTF-8" />
    <title>Upload</title>
    <script src="//unpkg.com/alpinejs" defer></script>
  </head>
 
  <body>
    <div x-data="{ file: null, presignedData: null, message: '' }">
      <input
        type="file"
        x-on:click="message = ''"
        x-on:change="getPresignedData"
      />
      <button x-on:click="upload" x-bind:disabled="!presignedData">
        Upload
      </button>
      <div x-text="message"></div>
 
      <script>
        async function getPresignedData(event) {
          /*
                Create a presigned URL for uploading a file to S3.
                */
          const files = event.target.files
          const file = files[0]
          this.file = file
          const response = await fetch('/uploader/', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'X-CSRFToken': '{{ csrf_token }}',
            },
            body: JSON.stringify({
              file: {
                name: file.name,
                content_type: file.type,
              },
            }),
          })
          if (!response.ok) {
            const data = await response.json()
            console.error('Error:', data.error)
          } else {
            this.presignedData = await response.json()
          }
        }
 
        async function upload() {
          /*
                Upload a file to S3 using the presigned URL.
                 */
          const formData = new FormData()
          for (const [key, value] of Object.entries(
            this.presignedData.fields,
          )) {
            formData.append(key, value)
          }
          formData.append('file', this.file)
 
          const response = await fetch(this.presignedData.url, {
            method: 'POST',
            body: formData,
          })
          this.message = response.ok
            ? 'โ
 Upload successful'
            : `โ Upload failed: The error message is ${response.statusText}`
        }
      </script>
    </div>
  </body>
</html>5. Run your Django Server:
Run your Django server locally from your terminal:
python manage.py runserver
 - Navigate to http://127.0.0.1:8000/in your web browser, select a file, and click the "Upload" button to upload the file directly to your S3 bucket using the presigned URL.
Errors? Some tips here:
* If you get a 403 error, check your
AWS keys are correct and that your bucket name is correct. * If you get a 400
error, check your CORS settings are correct. * If you get a 500 error, check
your IAM user has the correct permissions. * If you get something like
"Uncaught TypeError: Failed to fetch at Proxy.upload", try doing a hard
refresh (Ctrl + Shift + R on MacOS) to clear your browser cache. (This affe
Your bonus for finishing: Make the frontend elegant ๐
- Update your template with styling:
<!doctype html>
<html
  lang="en"
  xmlns:x-on="http://www.w3.org/1999/xhtml"
  xmlns:x-bind="http://www.w3.org/1999/xhtml"
>
  <head>
    <meta charset="UTF-8" />
    <title>Upload</title>
    <script src="//unpkg.com/alpinejs" defer></script>
    <style>
      body {
        background-color: #fafafa;
        font-family: 'Roboto', sans-serif;
      }
 
      .uploader {
        padding: 30px;
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
        width: 300px;
        margin: auto;
        border-radius: 16px;
      }
 
      .file-input {
        border: 1px solid #e0e0e0;
        padding: 12px;
        border-radius: 8px;
        margin-bottom: 20px;
        width: calc(100% - 24px);
        transition:
          border-color 0.3s ease-in-out,
          box-shadow 0.3s ease-in-out;
      }
 
      .file-input:focus {
        border-color: #007aff;
        box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.25);
      }
 
      .upload-button {
        background-color: #007aff;
        padding: 12px 20px;
        border-radius: 8px;
        transition:
          background-color 0.3s ease-in-out,
          box-shadow 0.3s ease-in-out;
        width: 100%;
        border: none;
      }
 
      .upload-button:hover {
      }
 
      .upload-button:hover:not(:disabled) {
        background-color: #005bb5;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        color: white;
      }
 
      .upload-button:disabled {
        background-color: #e0e0e0;
      }
 
      .message {
        margin-top: 20px;
        font-size: 14px;
        color: #2c2c2c;
        text-align: center;
      }
    </style>
  </head>
 
  <body>
    <div
      x-data="{ file: null, presignedData: null, message: '' }"
      class="uploader"
    >
      <input
        type="file"
        x-on:click="message = ''"
        x-on:change="getPresignedData"
        class="file-input"
      />
      <button
        x-on:click="upload"
        x-bind:disabled="!presignedData"
        class="upload-button"
      >
        Upload
      </button>
      <div x-text="message" class="message"></div>
 
      <script>
        async function getPresignedData(event) {
          /*
                Create a presigned URL for uploading a file to S3.
                */
          const files = event.target.files
          const file = files[0]
          this.file = file
          const response = await fetch('/uploader/', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'X-CSRFToken': '{{ csrf_token }}',
            },
            body: JSON.stringify({
              file: {
                name: file.name,
                content_type: file.type,
              },
            }),
          })
          if (!response.ok) {
            const data = await response.json()
            console.error('Error:', data.error)
          } else {
            this.presignedData = await response.json()
          }
        }
 
        async function upload() {
          /*
                Upload a file to S3 using the presigned URL.
                 */
          const formData = new FormData()
          for (const [key, value] of Object.entries(
            this.presignedData.fields,
          )) {
            formData.append(key, value)
          }
          formData.append('file', this.file)
 
          const response = await fetch(this.presignedData.url, {
            method: 'POST',
            body: formData,
          })
          this.message = response.ok
            ? 'โ
 Upload successful'
            : `โ Upload failed: The error message is ${response.statusText}`
        }
      </script>
    </div>
  </body>
</html>