MEDIA_URL
The public URL prefix that browsers use to request uploaded files, such as /media/.
Media files are the user-generated files that your application stores outside the core codebase. In Django, this usually means profile pictures, product images, documents, PDFs, and similar uploads that come from forms, admin, or API requests. These files are different from static files because they are not part of the shipped source code; they are created at runtime and therefore need a separate storage and serving strategy.
MEDIA_URL
The public URL prefix that browsers use to request uploaded files, such as /media/.
MEDIA_ROOT
The filesystem directory where Django saves uploaded files on disk during development.
upload_to
The model field option that decides the subfolder name for each uploaded file inside MEDIA_ROOT.
When Django receives an uploaded file, it does not store the raw file content inside the database by default. Instead, the file is written to a storage backend, and the database keeps a reference to the file path or storage location. This design keeps your database smaller and makes large binary files easier to manage. In a simple local setup, that storage backend is usually your project’s local filesystem. In production, it is often a cloud object store such as S3, Azure Blob Storage, or Google Cloud Storage.
MEDIA_URL and MEDIA_ROOT work together. MEDIA_ROOT is the physical folder on disk, while MEDIA_URL is the public path used to reach files from the browser. If a file is saved at MEDIA_ROOT/uploads/image.jpg, Django can expose it at MEDIA_URL/uploads/image.jpg when development serving is enabled.
If you want to use ImageField, Django needs Pillow for image validation and metadata handling. Without Pillow, Django cannot fully support image-specific fields.
The first step is to define the media URL prefix and the local storage directory in settings.py. Django will use these settings whenever a model field like FileField or ImageField writes a file to disk.
# settings.pyfrom pathlib import Path
MEDIA_URL = "/media/"MEDIA_ROOT = BASE_DIR / "media"MEDIA_URL should always end with a trailing slash, because Django joins file paths underneath it. MEDIA_ROOT should point to a dedicated folder that is outside your app modules, so uploaded files stay separate from the source code. Using Path is the modern and readable approach when your project already relies on pathlib.
Django’s development server can serve media files only when you explicitly attach the media URL pattern in urls.py. This is convenient for local development because you can immediately test file uploads without setting up extra infrastructure. The same pattern should not be treated as a production solution.
# 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)Mermaid flow of a typical upload:
FileField stores any kind of uploaded file, while ImageField is a specialized version for images. Both fields work the same way at the storage level, but ImageField adds image validation and integrates with Pillow. The upload_to argument helps you keep files organized by placing them into a subdirectory.
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 to this model, Django saves the file into the media folder and stores the relative file path in the model record. Later, when you access file.url or image.url, Django resolves that stored path back into a usable URL.
When you have a model that includes file uploads, your views and serializers need to handle the file data correctly. For example, if you have a product model with images, you might want to create a nested route for uploading images related to a specific product. This way, you can keep the product details and its images organized in a clear API structure.
This below example demonstrates how to set up nested routes for a product and its images using Django REST Framework and the drf-nested-routers package. The ProductImageSerializer is designed to handle file uploads, and the ProductImageViewSet ensures that uploaded images are associated with 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;}You now get endpoints like:
GET /products/POST /products/GET /products/{product_pk}/images/POST /products/{product_pk}/images/Django’s file fields automatically validate that the uploaded data is a file, but you can add custom validation logic to check file size, type, or other criteria. For example, you might want to restrict uploads to certain image formats or limit the maximum file size. or you want to upload any specific file type like PDF, DOCX, etc.
You can create a custom validator function to check the file size before saving it. Here’s an example of how to implement a 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, you can 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 validate the file extension to ensure that only specific types of files are uploaded. Here’s how you can create 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, the FileExtensionValidator is used to restrict uploads to PDF, DOCX, and TXT files. If a user tries to upload a file with an extension that is not in the allowed list, Django will raise a validation error.
You can also validate the dimensions of an uploaded image using Pillow. Here’s an example of how to create a custom validator for 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.")