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-dotenvdjango-admin startproject core .python manage.py startapp sim
- Register your new app by adding it to your
INSTALLED_APPS
insettings.py
.
# settings.pyINSTALLED_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 key
and 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
.env
into 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 Pathfrom dotenv import load_dotenvimport osload_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 osimport boto3def 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 jsonimport osfrom django.views.decorators.csrf import csrf_exemptfrom django.utils.decorators import method_decoratorfrom django.shortcuts import renderfrom django.http import JsonResponsefrom .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 adminfrom django.urls import include, pathurlpatterns = [path('admin/', admin.site.urls),path('', include('sim.urls')),]
- Create
sim/urls.py
and add:
from django.urls import pathfrom . import viewsurlpatterns = [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.html
with the below:
<!doctype html><htmllang="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: '' }"><inputtype="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.filesconst file = files[0]this.file = fileconst 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><htmllang="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><divx-data="{ file: null, presignedData: null, message: '' }"class="uploader"><inputtype="file"x-on:click="message = ''"x-on:change="getPresignedData"class="file-input"/><buttonx-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.filesconst file = files[0]this.file = fileconst 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>