Skip to content

File Upload#

2024 iThome Ironman Contest

This is the 23rd article in the Django Ninja tutorial series. We are starting to introduce advanced features!

In modern Web services, file uploading is a common scenario.

Whether it is users uploading photos or enclosing attachments, file uploading is an indispensable feature.

This article introduces how to implement image upload functionality in Django Ninja, using a user "uploading avatar" (referred to as avatar below) API as an example to guide you through the process step-by-step.

For all code changes in this article, please refer to this PR.

GitHub Example Project#

👉 Django-Ninja-Tutorial


Before we begin, however, we should understand what topics this chapter covers.

Chapter 6 "API Advanced Features" Introduction#

For API projects, advanced features help us meet the challenges of complex scenarios and large-scale projects.

Although this is an introductory guide, we will still cover some common advanced features. These features not only improve API flexibility but also enhance system performance and user experience.

This chapter consists of 5 articles introducing 3 common advanced features:

These techniques are not only critical for large projects but also enable you to respond effectively to changing requirements in API development.


With the focus of this chapter in mind, let's start with the first feature—file upload.

The Core of File Uploads - UploadedFile#

In Django Ninja, we can use UploadedFile to receive uploaded files. It is a re-packaging of Django's UploadedFile, and the two are basically the same.

UploadedFile Overview#

UploadedFile is the core object for Django to handle file uploads. When a user uploads a file, Django automatically wraps the file into an UploadedFile instance, making it convenient for us to perform subsequent file processing and storage.

The UploadedFile object has many attributes, among which the more commonly used are:

  • name: The name of the uploaded file. This can be used to access the original filename of the file.
  • size: The size of the file (in bytes). We can use this to validate file size.
  • content_type: The MIME type of the file. This is very useful for validating the format of uploaded files, such as ensuring the file is in an image format. We will use this later!
  • read(): Used to read the contents of the file. When custom file processing is needed, we can use this method to obtain the binary data of the file.
  • chunks(): When the file is very large, using this method can read the file in chunks, avoiding excessive memory consumption.

These features make UploadedFile highly flexible, capable of meeting various upload needs from simple image uploading to large file processing.

OK, for file uploading, understanding this core component UploadedFile is sufficient.

Before starting to implement the code for the "upload avatar" API, we need to perform some "preparations".


File upload has many parts that are actually more related to Django rather than Django Ninja. Therefore, I will highlight only the key points.

Django Project Configuration#

Before implementing the file upload feature, we need to tell Django how to handle uploaded files. This involves configuring MEDIA_URL and MEDIA_ROOT.

MEDIA_URL and MEDIA_ROOT Configuration#

  • MEDIA_URL: This is the URL prefix for files; all uploaded files will be accessed via this URL.
  • MEDIA_ROOT: This is the actual path where uploaded files are stored inside the Django server.

Add the following code to the project's settings.py:

# NinjaForum/settings.py
...

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

As a result, uploaded files will be stored in the media folder of the project root directory and accessed through the /media/ path.

File Access in Development Environment#

We need the static method provided by Django to allow the development environment to directly access these files.

Add this line to the project's urls.py:

# NinjaForum/urls.py
from django.conf import settings
from django.conf.urls.static import static
...

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', api.urls),
    # Allow access to uploaded files in the development environment, for development use only
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

This code allows Django to serve static files in a development environment.

Creating an ImageField Field#

ImageField is a field Django dedicates to storing images; it actually stores the file path of the image. A similar field is FileField.

The code is as follows:

# user/models.py
class User(AbstractUser):
    ...
    avatar = models.ImageField(upload_to='avatars/', null=True)

Combined with the previous settings.py configuration, the avatar field will store uploaded images in the media/avatars/ folder—this path is determined jointly by MEDIA_ROOT and the field's upload_to.

After switching to this branch, there is a new migration file in the project. Remember to migrate the database:

python manage.py migrate
# or
make migrate

By the way, ImageField depends on the third-party package—Pillow. This package provides image processing features for the field. You must install it first for the field to work properly:

pip install Pillow
# or
poetry add pillow

For readers using Poetry, simply run poetry install after merging the branch.


Implementation: Uploading Avatar#

With the preparations completed, we can finally move on to the main event.

Here is the complete "upload avatar" feature:

from ninja import File, Router, UploadedFile
from ninja.errors import HttpError
...

@router.post('/users/{int:user_id}/avatar/', summary='Upload Avatar')
def upload_avatar(
    request: HttpRequest,
    user_id: int,
    avatar_file: UploadedFile = File()
) -> dict[str, str]:
    """
    Upload avatar
    """
    # Check file type
    if not avatar_file.content_type.startswith('image/'):
        raise HttpError(400, 'The uploaded file must be an image')

    user = User.objects.get(id=user_id)
    user.avatar = avatar_file
    user.save()
    return {'detail': 'Image uploaded successfully'}

Here is a breakdown of the key points in this code.

1. UploadedFile Parameter Definition#

In the signature of the view function, UploadedFile is used as a type hint, and the avatar_file parameter represents the uploaded file.

You can name this parameter anything you want, such as file in the documentation example. I specifically used a different name to emphasize that its name is completely customizable.

But! No matter what name you choose, when sending a request, the key in the body must also use the same name.

At this time, the HTTP request should look like this: (Note the avatar_file in the body)

POST /users/1/avatar/
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

# Body content below
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar_file"; filename="example.jpg"
Content-Type: image/jpeg

(binary image data here)
------WebKitFormBoundary7MA4YWxkTrZu0gW--

The Content-Type in the Header is multipart/form-data, and each key-value pair starts with Content-Disposition: form-data;. This format allows sending multiple different types of data simultaneously in a single request, including text and binary files.

For details, you can refer to the article multipart/form-data 初探 (Exploring multipart/form-data).

2. The File Function#

Defining = File() is intended to tell Django Ninja that this parameter should be retrieved from the "uploaded file" part of the HTTP request. This is similar to Query which we mentioned in the 11th article.

Without this marker, the framework might not correctly identify and process the uploaded content.

3. Checking File Type#

We use the content_type attribute of UploadedFile to obtain the file type of this body content, confirm it is an image, and then proceed.

# Check file type
if not avatar_file.content_type.startswith('image/'):
    raise HttpError(400, 'The uploaded file must be an image')

Although this method is simple, it is sufficient for basic image uploading functionality.

Result of testing a plain text file upload:

// 400 Bad Request
{
    "detail": "The uploaded file must be an image"
}

In a production environment, you need stricter checks, such as using specialized image processing libraries to validate file content.

4. Saving the Image#

Finally, we assign the image to the avatar field of User and call the save() method.

Django automatically handles file storage. If the name is a duplicate, it will also automatically generate a unique filename, and then put the file in the location we specified earlier.

After uploading the exact same avatar twice through the API, we use the tree command in the project root directory to inspect the results:

 tree media
media
└── avatars
    ├── my-avatar.png
    └── my-avatar_gVwgCiG.png  # Second upload with the same filename, automatically renamed

2 directories, 2 files

As we can see, the second uploaded image was "automatically renamed". This guarantees the uniqueness of filenames.

In a production environment, it is best to customize a uniform naming format for files to ensure better management and security.


Summary and Next Steps#

This article explained how to implement File Uploads in Django Ninja, from project configuration to the API itself, with a detailed look at UploadedFile.

Next, we will introduce another advanced feature—Pagination, which helps you effectively improve performance and user experience when returning large amounts of data.