How to upload images easily with Django (and save the links to your database) 🌤️

Photo of Tom Dekan
by Tom Dekan

In 5 small steps, we'll build a Django app that uploads images to the cloud and stores the links in our database. This allows us to show the uploaded images on our app.


  1. get a signed upload url from Cloudinary;
  2. upload the image; and
  3. save the image link to our database with Django

The images will then be available for future processing - e.g., detecting faces, creating captions, or training AI generation models (Personal example 🙂).

Let's begin 🐎

Here's an optional video tutorial (featuring me 🏇🏿) that follows the written guide below:

Edit: Thanks to Alex Goulielmos (from Youtube) for carefully reading this guide and correcting an error in the original version 👍


Setup your Django app

  • Create a new Django and install requirements
pip install django cloudinary python-dotenv
django-admin startproject core .
python startapp sim
  • Register your new app by adding it to your INSTALLED_APPS in


Setup Cloudinary (for storing your images in the cloud)

You can use any cloud storage service you want, but I'm using Cloudinary. It has a free tier and is easy to setup. (Here's a guide I wrote using AWS S3 instead of Cloudinary:

  1. Create an account on Cloudinary
  2. Go to "Programmable Media" > "Product Environment Credentials" where you'll see your "Cloud name", "API Key", and "API Secret".

Setup your environment variables

  • Create a .env file at core/.env
  • Add the following variables to your .env file
  • Add the following code to the top of your file to load your environment variables when your Django app starts.
from dotenv import load_dotenv


1. Add your models

  • Add this to sim/
from django.db import models

class Image(models.Model):
    key = models.CharField(help_text="The public id of the uploaded file", max_length=100)
    url = models.CharField(max_length=100)
    name = models.CharField(max_length=100, help_text='The original name of the uploaded image')
    width = models.IntegerField(help_text='Width in pixels')
    height = models.IntegerField(help_text='Height in pixels')
    format = models.CharField(max_length=10)
    created_at = models.DateTimeField(auto_now_add=True)
  • Create your database by make migrations and migrating
python makemigrations
python migrate

2. Create your templates

Now we'll add the HTML and the JavaScript that will upload images client-side.

This will upload the image to Cloudinary, and return the link to our Django app to save to our database. We'll use Alpine.js (very lightweight and easy to use) to simplify the JavaScript.

  • Create templates folder in sim/templates
  • Create index.html in templates folder
  • Add the following code to index.html
<!DOCTYPE html>
<html lang="en" xmlns:x-on="" xmlns:x-bind="">
    <meta charset="UTF-8">
    <script src="//" defer></script>

<section class="h-screen flex bg-gray-100">
    <div class="m-auto grid image-uploader p-4 rounded-lg shadow-lg bg-white">
        <div class="" x-data="{ file: null, message: '', imagePreview: ''}" x-cloak>
            <div class="preview-container" x-on:click="$">
                <img class="preview-image" x-show="imagePreview" :src="imagePreview" alt="Placeholder image">
                <div x-show="!imagePreview" class="text-gray-400 preview-text" >
                    Click to choose an image
            <input id="file-upload" type="file" style="display: none;" x-on:click="message = ''" x-ref="fileInput" x-on:change="showPreview">
            <button class="button" x-on:click="upload" x-bind:disabled="!file">Upload</button>
            <div x-text="message" class="message"></div>
        <div class="image-grid-wrapper">
            <div class="image-grid">
                {% for image in images %}
                <div class="image-wrapper">
                    <a class="image-link" href="{{ image.url }}" target="_blank">
                        <img src="{{ image.url }}" alt="{{ }}">
                {% endfor %}

    async function upload() {
        Upload a file to S3 using the presigned URL.
        const formData = new FormData();
        formData.append('signature', "{{ signature }}");
        formData.append('api_key', "{{ api_key }}");
        formData.append('timestamp', "{{ timestamp }}");
        formData.append('file', this.file);
        const response = await fetch("{{ upload_url }}", { method: 'POST', body: formData });

        if (response.ok){
            this.message = '✅ Upload successful'
            const data = await response.json()
            await saveLink(data)
            this.message = `❌ Upload failed: The error message is ${response.statusText}`


    async function saveLink(uploadedData){
        console.log("uploadedData = ", uploadedData)
        const response = await fetch("", {
                method: 'POST',
                body: JSON.stringify(uploadedData),
                headers: {
                    'X-CSRFToken': "{{ csrf_token }}"
        if (response.ok) {
        else {
            const data = await response.json();
            console.error('Error:', data.error);

    function showPreview() {
        const file = this.$refs.fileInput.files[0];
        this.file = file;
        if (file) {
            this.imagePreview = URL.createObjectURL(file);
        } else {
            this.imagePreview = '';

        :root {
            --border-radius: 10px;

        .h-screen {
            height: 100vh;

        .flex {
            display: flex;

        .bg-gray-100 {
            background-color: #f7fafc;

        .m-auto {
            margin: auto;

        .p-4 {
            padding: 1rem;

        .rounded-lg {
            border-radius: 0.5rem;

        .shadow-lg {
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);

        .bg-white {
            background-color: #ffffff;

        .text-gray-400 {
            color: #cbd5e0;

        .image-uploader {
            place-content: center;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            border-radius: 15px;

        .image-grid-wrapper {
            height: 240px;
            overflow-y: scroll;

        .image-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 10px;
            overflow-y: auto;

        .image-wrapper {
            position: relative;
            overflow: hidden;
            display: flex;
            justify-content: space-evenly;

        .image-wrapper img {
            width: 100px;
            object-fit: cover;
            display: block;
            border-radius: var(--border-radius);
            transition: transform 0.3s ease-in-out;

        .image-wrapper:hover img {
            transform: scale(1.1);
            cursor: pointer;

        .image-link {
            display: flex;

        .preview-container {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 200px;
            width: 100%;
            border: 1px dashed #ddd;
            background: #fafafa;
            border-radius: var(--border-radius);

        .preview-text {
            font-family: Arial, Helvetica, sans-serif;
            cursor: pointer;
            color: #cbd5e0; /* Tailwind text-gray-400 */

        .preview-image {
            max-width: 200px;
            max-height: 200px;
            border: 1px dotted #ddd;
            object-fit: cover;
            display: block;
            background: #ddd;

        .button {
            padding: 8px 32px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 16px;
            margin: 4px 2px;
            border-radius: var(--border-radius);
            transition: 0.3s ease-out;
            width: 100%;

            border: 1px solid slategray;
            color: #4f5a65;

        .button:not(:disabled):hover {
            cursor: pointer;
            background-color: slategray;
            color: white;

        .message {
            text-align: center;

        [x-cloak] {
            display: none !important;

3. Generate the signed upload url

  • Create in sim folder containing:
from datetime import datetime
import cloudinary
import os


def generate_signature() -> dict:
    Generate a signed Cloudinary upload url.
    timestamp =
    params_to_sign = {"timestamp": timestamp}
    signature = cloudinary.utils.api_sign_request(params_to_sign, cloudinary.config().api_secret)
    return {
        'signature': signature,
        'api_key': cloudinary.config().api_key,
        'timestamp': timestamp,
        'upload_url': f"{cloudinary.config().cloud_name}/image/upload",

4. Create your views

  • Create or update in sim folder to have:
import json
from django.http import HttpResponse
from django.shortcuts import render
from .models import Image
from .services import generate_signature

def index(request) -> HttpResponse:
    if request.method == 'GET':
        images = Image.objects.all().order_by('-created_at')
        return render(
            request, 'index.html', context={
                'images': images, **generate_signature()
    elif request.method == 'POST':
        body = request.body.decode('utf-8')
        data = json.loads(body)
            key=data['public_id'], url=data['secure_url'],
            width=data['width'], height=data['height'],
            format=data['format'], name=data['original_filename'],
        return HttpResponse(status=201)

5. Create your urls

  • Create in sim folder
  • Add the following code to
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),

  • Update in core folder to have:
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('sim.urls')),

Upload! 🌤️

  • Run your Django app
python runserver
  • Go to http://localhost:8000/ and upload an image 🚀

P.S Photon Designer - This is how I build Django apps

Even for this small guide, styling the frontend took me way too much time (over an hour, and I'm a pro). Probably like you, I want to go from idea to elegant Django frontend as fast as possible (preferably instantly).

So, that's why I'm building Photon Designer.

Photon Designer lets me produce Django frontend visually and extremely quickly - like a painter sweeping his brush across the page 🖌️. You drag and drop elements, style them visually in any way you'd like, and export clean code to your Django app. It's like Photoshop but for Django apps. And much, much faster. 💡

Let's get visual.

Do you want to create beautiful frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).
Copied link to clipboard 📋

Made with care by Tom Dekan

© 2024 Photon Designer