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 apppip install django boto3 python-dotenv
django-admin startproject core .
python manage.py startapp sim
- Register your new app by adding it to your `INSTALLED_APPS` in `settings.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).
- Click ->
(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 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.py
and 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.html
with 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 affeYour 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>
P.S Fed up with styling Django templates manually, like a weaver before the automated loom? π½
Want to build your Django frontend faster? Probably like you, I'm eager to turn ideas into products asap.
Enter Photon Designer: the visual editor that I'm building to craft Django frontends at light-speed (and much faster than a weaver) π‘