MEDIA_URL
The public URL that browsers use to ask for an uploaded file, something like /media/.
Media files are files that your users upload themselves - things like profile pictures, product images, documents, PDFs, and so on. They come from forms, the admin panel, or API requests. These files are different from static files, because they are not part of your project’s code. They get created while the app is running, so they need their own way of being stored and served.
MEDIA_URL
The public URL that browsers use to ask for an uploaded file, something like /media/.
MEDIA_ROOT
The folder on your computer’s disk where Django actually saves uploaded files during development.
upload_to
A setting on the model field that decides which subfolder each uploaded file goes into, inside MEDIA_ROOT.
When Django gets an uploaded file, it doesn’t save the actual file content inside the database. Instead, the file gets written to a storage location, and the database just keeps a note of where that file is (its path). This keeps your database small and makes it much easier to manage big files. On a simple local setup, this storage location is usually just your project’s own filesystem (your computer’s disk). In production (when your app is live), it’s often a cloud storage service like S3, Azure Blob Storage, or Google Cloud Storage.
MEDIA_URL and MEDIA_ROOT work as a pair. MEDIA_ROOT is the real folder on disk where the file lives, while MEDIA_URL is the public web address used to reach that file from a browser. So if a file is saved at MEDIA_ROOT/uploads/image.jpg, Django can show it to the browser at MEDIA_URL/uploads/image.jpg, as long as development serving is turned on.
If you want to use ImageField, Django needs a package called Pillow to check and read image files properly. Without Pillow, Django can’t fully support image fields.
The first step is to set the media URL and the local storage folder inside settings.py. Django uses these settings whenever a model field like FileField or ImageField saves a file to disk.
# settings.pyfrom pathlib import Path
MEDIA_URL = "/media/"MEDIA_ROOT = BASE_DIR / "media"MEDIA_URL should always end with a slash (/), because Django adds file paths right after it. MEDIA_ROOT should point to its own dedicated folder, kept outside your app’s code folders, so uploaded files stay separate from your source code. Using Path is the cleaner, more modern way to write this, especially if your project already uses pathlib.
Django’s local development server can only show media files if you tell it to, by adding the media URL pattern inside urls.py. This is handy while you’re developing locally, because you can test file uploads right away without setting up any extra infrastructure. But this same setup should never be used as your solution in production.
# urls.pyfrom django.conf import settingsfrom django.conf.urls.static import staticfrom django.contrib import adminfrom django.urls import path
urlpatterns = [ path("admin/", admin.site.urls),]
if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)Here’s a simple flow of what happens when someone uploads a file:
FileField can store any kind of uploaded file, while ImageField is made specifically for images. Both fields work the same way when it comes to storing the file, but ImageField also checks that the file is a real image, using Pillow behind the scenes. The upload_to setting helps you stay organized by placing files into their own subfolder.
from django.db import models
class Document(models.Model): title = models.CharField(max_length=100) file = models.FileField(upload_to="documents/") image = models.ImageField(upload_to="images/", blank=True, null=True)When a user uploads a file through this model, Django saves the file inside the media folder, and stores the file’s relative path in the database record. Later on, when you use file.url or image.url, Django turns that saved path into a usable web address.
When your model includes file uploads, your views and serializers need to handle that file data the right way. For example, if you have a product model with images, you might want a nested route just for uploading images that belong to one specific product. This way, your product details and its images stay neatly organized in your API.
The example below shows how to set up nested routes for a product and its images, using Django REST Framework along with the drf-nested-routers package. The ProductImageSerializer is built to handle file uploads, and the ProductImageViewSet makes sure each uploaded image gets linked to the correct product.
# models.pyfrom django.db import models
class Product(models.Model): name = models.CharField(max_length=150) description = models.TextField(blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): return self.name
class ProductImage(models.Model): product = models.ForeignKey( Product, related_name="images", on_delete=models.CASCADE, ) image = models.ImageField(upload_to="products/images/")
def __str__(self): return f"Image for {self.product.name}"# serializers.pyfrom rest_framework import serializersfrom .models import Product, ProductImage
class ProductImageSerializer(serializers.ModelSerializer): class Meta: model = ProductImage fields = ["id", "product", "image"] read_only_fields = ["id", "product"]
def create(self, validated_data): product_pk = self.context.get("product_pk") return ProductImage.objects.create(product_id=product_pk, **validated_data)
class ProductSerializer(serializers.ModelSerializer): images = ProductImageSerializer(many=True, read_only=True)
class Meta: model = Product fields = ["id", "name", "description", "price", "created_at", "images"] read_only_fields = ["id", "created_at"]# views.pyfrom rest_framework import viewsetsfrom .models import Product, ProductImagefrom .serializers import ProductSerializer, ProductImageSerializer
class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.prefetch_related("images").all() serializer_class = ProductSerializer
class ProductImageViewSet(viewsets.ModelViewSet): serializer_class = ProductImageSerializer
def get_queryset(self): return ProductImage.objects.filter(product_id=self.kwargs["product_pk"])
def get_serializer_context(self): context = super().get_serializer_context() context["product_pk"] = self.kwargs.get("product_pk") return context# urls.pyfrom django.urls import include, pathfrom rest_framework.routers import DefaultRouterfrom rest_framework_nested import routersfrom .views import ProductViewSet, ProductImageViewSet
router = DefaultRouter()router.register("products", ProductViewSet, basename="products")
products_router = routers.NestedDefaultRouter(router, "products", lookup="product")products_router.register("images", ProductImageViewSet, basename="product-images")
urlpatterns = [ path("", include(router.urls)), path("", include(products_router.urls)),]# admin.pyfrom django.contrib import adminfrom .models import Product, ProductImagefrom django.utils.html import format_html
class ProductImageInline(admin.TabularInline): model = ProductImage readonly_fields = ["thumbnail"] extra = 3
def thumbnail(self, obj): if obj.image: return format_html(f'<img src="{obj.image.url}" class="thumbnail" />') return "No image"
class Media: css = { "all": ["app_name/css/style.css"] }
@admin.register(Product)class ProductAdmin(admin.ModelAdmin): list_display = ["name", "price", "created_at"] inlines = [ProductImageInline] readonly_fields = ["created_at"]/* static/app_name/css/style.css */.thumbnail { max-width: 100px; max-height: 100px; overflow: hidden; border-radius: 4px; border: 1px solid #ddd;}Now you’ll have endpoints like these:
GET /products/POST /products/GET /products/{product_pk}/images/POST /products/{product_pk}/images/Django’s file fields already check that the uploaded data is in fact a file, but you can add your own validation rules too, to check things like file size, file type, or anything else you need. For example, you might want to only allow certain image formats, set a maximum file size, or only allow specific file types like PDF or DOCX.
You can write your own validator function to check the file’s size before it gets saved. Here’s an example of a simple file size validator:
# validators.pyfrom django.core.exceptions import ValidationError
def validate_file_size(file): max_size_kb = 500 # Maximum file size in KB if file.size > max_size_kb * 1024: raise ValidationError(f"File size should not exceed {max_size_kb} KB.")To use this validator, just add it to your model field:
from django.db import modelsfrom .validators import validate_file_size
class Document(models.Model): title = models.CharField(max_length=100) image = models.ImageField(upload_to="images/", validators=[validate_file_size])You can also check the file’s extension, so only certain file types are allowed to be uploaded. Here’s how to set up a validator for allowed file extensions:
# models.pyfrom django.db import modelsfrom django.core.validators import FileExtensionValidator
class Document(models.Model): title = models.CharField(max_length=100) file = models.FileField( upload_to="documents/", validators=[FileExtensionValidator(allowed_extensions=["pdf", "docx", "txt"])] )In this example, FileExtensionValidator only allows PDF, DOCX, and TXT files to be uploaded. If someone tries to upload a file with any other extension, Django will show a validation error and block the upload.
You can also check the size (dimensions) of an uploaded image using Pillow. Here’s an example of a custom validator that checks image dimensions:
# validators.pyfrom django.core.exceptions import ValidationErrorfrom PIL import Image
def validate_image_dimensions(image): WIDTH_MIN = 300 HEIGHT_MIN = 300 try: img = Image.open(image) width, height = img.size if width < WIDTH_MIN or height < HEIGHT_MIN: raise ValidationError(f"Image dimensions should be at least {WIDTH_MIN}x{HEIGHT_MIN} pixels.") except Exception as e: raise ValidationError("Invalid image file.")