# Building Web APIs with Django Rest Framework: A Beginner's Guide

[Django Rest Framework (DRF)](https://www.django-rest-framework.org/) is a comprehensive toolkit for building Web APIs with Django. It offers serialization, authentication, viewsets, and interactive documentation, making it an excellent choice for developing reliable and maintainable RESTful services.


In this tutorial, you'll build a task management API using Django Rest Framework and SQLite as the database backend. 

[ad-logs]
## Prerequisites
Before getting started, ensure you have:

- Python installed on your system (preferably Python 3.13 or higher)
- A basic understanding of Python, Django, and web development concepts

## Step 1 — Setting up the Django Rest Framework project

In this section, you'll create the directory structure and install the necessary dependencies for the Django Rest Framework project.

First, create a new directory for your project and navigate into it:

```command
mkdir drf-task-api && cd drf-task-api
```

Next, create a virtual environment and activate it:

```command
python3 -m venv venv
```
```command
source venv/bin/activate 
```

Install Django and the Django Rest Framework:

```command
pip install django djangorestframework 
```

Here's a breakdown of these packages:

- `Django` – The web framework that forms the foundation.
- `Django Rest Framework` – The toolkit for building REST APIs with Django.

After that, create a new Django project:

```command
django-admin startproject taskmanager .
```

Next, create a new Django app for handling tasks:

```command
python manage.py startapp tasks
```

Now, update the `settings.py` file to include Django Rest Framework and your new app. Open `taskmanager/settings.py` and find the `INSTALLED_APPS` list:

```python
[label taskmanager/settings.py]
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
[highlight]
    # Third-party apps
    'rest_framework',
    
    # Local apps
    'tasks',
[/highlight]
]

# Rest of settings file...
```

Next, configure Django Rest Framework by adding some settings at the bottom of the `settings.py` file:

```python
[label taskmanager/settings.py]
# ... existing settings ...

# Django Rest Framework settings
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}
```

This configuration sets up some basic defaults:

- `DEFAULT_PERMISSION_CLASSES` determines who can access your API. We're starting with `AllowAny` for simplicity.
- `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` configure automatic pagination of results, limiting responses to 10 items per page.

The default database configuration in `settings.py` uses SQLite, perfect for this tutorial. The configuration looks like this:

```python
[label taskmanager/settings.py]
# ... other settings ...

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# ... more settings ...
```


With that, run initial migrations to set up your database:

```command
python manage.py migrate
```

You should see output similar to:

```text
[output]
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  ...
```

Now run the development server:

```command
python manage.py runserver
```

You should see output similar to:

```text
[output]
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
March 06, 2025 - 10:39:15
Django version 5.1.6, using settings 'taskmanager.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.`
```

Your Django application is now running with SQLite as the database. If you navigate to http://127.0.0.1:8000/ in your browser, you should see the default Django welcome page:

![Screenshot of the default Django welcome page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/e9b5bfb6-4cd0-4b58-59f2-81755abe5a00/orig =3248x1994)

With Django now running, you'll define the data model for tasks in the next step.


## Step 2 — Creating the Task model

With the Django project set up, you will define a model for your tasks. Django models specify your application's data's database schema and business logic.

Start by opening the `tasks/models.py` file generated when you created your app. Modify this file to define the `Task` model:


```python
[label tasks/models.py]
import uuid
from django.db import models


class Task(models.Model):
    """Model representing a task in the system."""
    
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=200, db_index=True)
    description = models.TextField(blank=True, null=True)
    priority = models.IntegerField(default=1)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        """String representation of the task."""
        return self.title
```


This model includes:  
- A `UUIDField` for unique task IDs.  
- A `CharField` for titles, indexed for faster searches.  
- A `TextField` for optional descriptions.  
- An `IntegerField` for priority, defaulting to 1.  
- A `BooleanField` to track completion status.  
- `DateTimeFields` for creation and update timestamps.  
- Default ordering by newest tasks first.  

Now that you've defined your model, you need to create and apply a database migration. Migrations are Django's way of tracking and applying changes to your database schema.

First, create a migration file:

```command
python manage.py makemigrations
```

You should see output similar to:

```text
[output]
Migrations for 'tasks':
  tasks/migrations/0001_initial.py
    + Create model Task
```

This creates a new file in the `tasks/migrations` directory that describes the changes to be made to the database.

Next, apply the migration to create the database table:

```command
python manage.py migrate
```

You should see output that includes:

```text
[output]
Migrations for 'tasks':
  tasks/migrations/0001_initial.py
    + Create model Task
(venv) stanley@MACOOKs-MacBook-Pro drf-task-api % python manage.py migrate

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, tasks
Running migrations:
  Applying tasks.0001_initial... OK
```
 
Verify your model is working correctly by creating a task through Django's shell:

```command
python manage.py shell
```

In the shell, paste these commands:

```python
from tasks.models import Task
task = Task(title="Test Task", description="This is a test task", priority=3)
task.save()
print(f"Created task: {task.id} - {task.title}")
exit()
```

You should see a message confirming the task was created, with a UUID and the title "Test Task":

```text
[output]
>>> from tasks.models import Task
>>> task = Task(title="Test Task", description="This is a test task", priority=3)
>>> task.save()
print(f"Created task: {task.id} - {task.title}")
exit()
>>> print(f"Created task: {task.id} - {task.title}")
Created task: 82f3534a-4f8a-4c82-a7d2-69dfd6758c58 - Test Task
>>> exit()
now exiting InteractiveConsole...
```

Now, register your model with Django's admin interface so you can easily view and edit tasks:

```python
[label tasks/admin.py]
from django.contrib import admin
from .models import Task

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ('title', 'priority', 'completed', 'created_at')
    list_filter = ('completed', 'priority')
    search_fields = ('title', 'description')
    readonly_fields = ('id', 'created_at', 'updated_at')
```

This sets up an admin interface for the Task model with:

- A customized list view showing key fields
- Filters for completion status and priority
- Search capability across title and description
- Protection for system-managed fields like IDs and timestamps

To access the admin interface, you'll need to create a superuser:

```command
python manage.py createsuperuser
```

Follow the prompts to create an admin account, then start the development server again:

```command
python manage.py runserver
```

You can visit `http://127.0.0.1:8000/admin/` and log in with your superuser credentials to view and manage tasks:

![Screenshot of the Django admin](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1c3a40d2-e77e-408f-adec-800fdc467800/public =3248x1994)

Now that you have defined and worked on your data model, the next step is creating serializers to convert Django models and JSON representations suitable for your API.



## Step 3 — Creating serializers for the Task model

Django Rest Framework uses serializers to convert complex data types, like Django model instances, to Python data types easily rendered into JSON, XML, or other content types. Serializers also handle deserialization, allowing parsed data to be converted into complex types after validation.

To create serializers for your Task model, first create a new file in the tasks app:

```command
touch tasks/serializers.py
```

Now, open the newly created `serializers.py` file and add the following code:

```python
[label tasks/serializers.py]
from rest_framework import serializers
from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'priority', 'completed', 'created_at', 'updated_at']
        read_only_fields = ['id', 'created_at', 'updated_at']
    
    def validate_priority(self, value):
        """Validate that priority is between 1 and 5."""
        if value < 1 or value > 5:
            raise serializers.ValidationError("Priority must be between 1 and 5.")
        return value
```

This serializer automatically generates fields based on the `Task` model and ensures that specific fields, such as `id`, `created_at`, and `updated_at`, remain read-only. It also includes custom validation to restrict the priority value between 1 and 5.  

While you could use this single serializer for all operations, it's often better to create specialized serializers for different actions. 

Add two more serializers to handle creating and updating tasks specifically:

```python
[label tasks/serializers.py]
from rest_framework import serializers
from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    
    ...

[highlight]
class TaskCreateSerializer(serializers.ModelSerializer):
    """Serializer for creating tasks."""
    
    class Meta:
        model = Task
        fields = ['title', 'description', 'priority', 'completed']
    
    def validate_priority(self, value):
        """Validate that priority is between 1 and 5."""
        if value < 1 or value > 5:
            raise serializers.ValidationError("Priority must be between 1 and 5.")
        return value


class TaskUpdateSerializer(serializers.ModelSerializer):
    """Serializer for updating tasks."""
    
    class Meta:
        model = Task
        fields = ['title', 'description', 'priority', 'completed']
        extra_kwargs = {
            'title': {'required': False},
            'description': {'required': False},
            'priority': {'required': False},
            'completed': {'required': False},
        }
    
    def validate_priority(self, value):
        """Validate that priority is between 1 and 5."""
        if value < 1 or value > 5:
            raise serializers.ValidationError("Priority must be between 1 and 5.")
        return value
[/highlight]
```

These serializers serve specific purposes:  

- `TaskCreateSerializer` is used to create new tasks, including only relevant fields and enforce priority validation.  
- `TaskUpdateSerializer` is designed for updates, making all fields optional so clients can modify only what they need.  


You can test these serializers in the Django shell:

```command
python manage.py shell
```

In the shell, try converting a task to and from JSON by paste the following:

```python
from tasks.models import Task
from tasks.serializers import TaskSerializer
from rest_framework.renderers import JSONRenderer

# Get a task from the database
task = Task.objects.first()

# Serialize the task
serializer = TaskSerializer(task)
print(serializer.data)

# Convert to JSON
json_data = JSONRenderer().render(serializer.data)
print(json_data)

# Exit the shell
exit()
```

The output should include the serialized task data in Python dict format, followed by the JSON representation:

```text
[output]
>>> from rest_framework.renderers import JSONRenderer
....
>>> 
>>> # Get a task from the database
>>> task = Task.objects.first()
>>> 
>>> # Serialize the task
>>> serializer = TaskSerializer(task)
>>> print(serializer.data)
{'id': '82f3534a-4f8a-4c82-a7d2-69dfd6758c58', 'title': 'Test Task', 'description': 'This is a test task', 'priority': 3, 'completed': False, 'created_at': '2025-03-06T10:50:40.845868Z', 'updated_at': '2025-03-06T10:50:40.846292Z'}
>>> 
>>> # Convert to JSON
>>> json_data = JSONRenderer().render(serializer.data)
>>> print(json_data)
b'{"id":"82f3534a-4f8a-4c82-a7d2-69dfd6758c58","title":"Test Task","description":"This is a test task","priority":3,"completed":false,"created_at":"2025-03-06T10:50:40.845868Z","updated_at":"2025-03-06T10:50:40.846292Z"}'
>>> 
>>> # Exit the shell
>>> exit()
now exiting InteractiveConsole...
```
The output demonstrates serializing a `Task` instance using Django Rest Framework. First, it retrieves a task from the database and serializes it into a Python dictionary. 

Then, the data is converted into a JSON string using `JSONRenderer`. The final output includes the dictionary and JSON representation, which are ready for API responses.

Your serializers are now ready to convert Django models and API responses. In the next step, you'll implement views and URLs to expose your API endpoints.



## Step 4 — Implementing views and URLs for task operations

Django Rest Framework provides several types of views to handle API requests. In this step, you'll implement `ViewSets`, which combine the logic for multiple related operations into a single class, simplifying your API implementation.

Start by creating the views for your task operations. Open the `tasks/views.py` file and replace its contents with:

```python
[label tasks/views.py]
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Task
from .serializers import TaskSerializer, TaskCreateSerializer, TaskUpdateSerializer


class TaskViewSet(viewsets.ModelViewSet):
    """ViewSet for handling Task CRUD operations."""
    
    queryset = Task.objects.all()
    
    def get_serializer_class(self):
        """Return appropriate serializer class based on the request method."""
        if self.action == 'create':
            return TaskCreateSerializer
        elif self.action in ['update', 'partial_update']:
            return TaskUpdateSerializer
        return TaskSerializer
    
    def get_queryset(self):
        """Filter queryset based on query parameters."""
        queryset = Task.objects.all()
        
        # Filter by completion status if provided
        completed = self.request.query_params.get('completed', None)
        if completed is not None:
            completed = completed.lower() == 'true'
            queryset = queryset.filter(completed=completed)
        
        return queryset
```

This `TaskViewSet` serves as the main entry point for handling task-related API operations. It retrieves all tasks by default but allows filtering based on completion status when a query parameter is provided. 

The `get_serializer_class()` method dynamically selects the appropriate serializer depending on the action. When creating a new task, it uses `TaskCreateSerializer`, while updates rely on `TaskUpdateSerializer`. 

For all other actions, such as listing or retrieving tasks, it defaults to `TaskSerializer`.  


Next, you must set up the URL patterns to expose your API endpoints. Create the `tasks/urls.py` file with the following:

```python
[label tasks/urls.py]
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet

# Create a router and register our viewset
router = DefaultRouter()
router.register(r'tasks', TaskViewSet)

# The API URLs are now determined automatically by the router
urlpatterns = [
    path('', include(router.urls)),
]
```

The `DefaultRouter` automatically creates the following URL patterns:

- `GET /tasks/` - List all tasks (with optional filtering)
- `POST /tasks/` - Create a new task
- `GET /tasks/{id}/` - Retrieve a specific task
- `PUT /tasks/{id}/` - Update a task completely
- `PATCH /tasks/{id}/` - Update a task partially
- `DELETE /tasks/{id}/` - Delete a task

Finally, update the main URLs file to ensure your app's URLs are included in the project. Open `taskmanager/urls.py` and make sure it includes the tasks app URLs:

```python
[label taskmanager/urls.py]
from django.contrib import admin
[highlight]
from django.urls import path, include
[/highlight]

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('tasks.urls')),
]
```

With your views and URLs configured, your API is now ready for testing. Start the development server:

```command
python manage.py runserver
```

Now you can test your API using `curl` or any API client. 

You can create a new task by sending a POST request to the `/api/tasks/` endpoint:

```command
curl -X POST http://127.0.0.1:8000/api/tasks/ \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn Django Rest Framework","description":"Complete the DRF tutorial and build a project","priority":2}' \
  | python3 -m json.tool
```

You should receive a response with the newly created task:

```json
[output]
{
    "title": "Learn Django Rest Framework",
    "description": "Complete the DRF tutorial and build a project",
    "priority": 2,
    "completed": false
}
```
To retrieve a list of all tasks, send a GET request to the `/api/tasks/` endpoint:

```command
curl http://127.0.0.1:8000/api/tasks/ | python3 -m json.tool
```

This should return a paginated response with all your tasks:

```json
[output]
[
    {
        "id": "792235cd-0406-4ffe-a6b7-0ad825e5b02d",
        "title": "Learn Django Rest Framework",
        "description": "Complete the DRF tutorial and build a project",
        "priority": 2,
        "completed": false,
        "created_at": "2025-03-06T11:13:16.390528Z",
        "updated_at": "2025-03-06T11:13:16.390594Z"
    },
    {
        "id": "82f3534a-4f8a-4c82-a7d2-69dfd6758c58",
        "title": "Test Task",
        "description": "This is a test task",
        "priority": 3,
        "completed": false,
        "created_at": "2025-03-06T10:50:40.845868Z",
        "updated_at": "2025-03-06T10:50:40.846292Z"
    }
]
```

Retrieve a specific task (replace with your actual task ID):

```command
curl "http://127.0.0.1:8000/api/tasks/<your_task_id>/" | python3 -m json.tool
```
Remember to replace `<your_task_id>` with an actual UUID from your database. You should receive a response with the requested task:

```text
[output]
{
    "id": "792235cd-0406-4ffe-a6b7-0ad825e5b02d",
    "title": "Learn Django Rest Framework",
    "description": "Complete the DRF tutorial and build a project",
    "priority": 2,
    "completed": false,
    "created_at": "2025-03-06T11:13:16.390528Z",
    "updated_at": "2025-03-06T11:13:16.390594Z"
}
```

Update an existing task by sending a PUT request to the `/api/tasks/<task_id>/` endpoint:

```command
curl -X PUT "http://127.0.0.1:8000/api/tasks/<your_task_id>/" \
  -H "Content-Type: application/json" \
  -d '{"title":"Update API Documentation","description":"Document the API endpoints, models, and authentication","priority":4,"completed":true}' \
  | python3 -m json.tool
```

```text
[output]
{
    "title": "Update API Documentation",
    "description": "Document the API endpoints, models, and authentication",
    "priority": 4,
    "completed": true
}
```
Remove a task by sending a DELETE request to the `/api/tasks/<task_id>/` endpoint:

```command
curl -X DELETE "http://127.0.0.1:8000/api/tasks/792235cd-0406-4ffe-a6b7-0ad825e5b02d/" -v
```

```text
[output]
*   Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> DELETE /api/tasks/792235cd-0406-4ffe-a6b7-0ad825e5b02d/ HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
[highlight]
< HTTP/1.1 204 No Content
[/highlight]
< Date: Thu, 06 Mar 2025 11:28:26 GMT
< Server: WSGIServer/0.2 CPython/3.13.2
.... 
* Connection #0 to host 127.0.0.1 left intact
```
The `204` status code indicates the task was successfully deleted, and no content is returned in the response body.

Django Rest Framework also provides a browsable API you can access through your web browser. Visit http://127.0.0.1:8000/api/tasks/ to see and interact with your API directly in the browser:

![Screenshot of the DRF browsable API](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/588cabe7-35ba-4040-1518-52e4bc0c6800/md1x =3248x1994)

You've implemented a fully functional API with endpoints for creating, reading, updating, and deleting tasks.

## Final thoughts
This tutorial has walked you through creating a task management API with Django Rest Framework and SQLite. Django Rest Framework's elegant, Pythonic design makes it ideal for API development.

For more insights into advanced topics like throttling, permissions, and authentication, check the official docs for [Django](https://docs.djangoproject.com/), and [Django Rest Framework](https://www.django-rest-framework.org/).


 Thanks for reading, and happy coding!