4. Django REST API: CRUD Endpoints
Django REST API – CRUD Endpoints

In this post, we will expand our API by adding full CRUD functionality.
CRUD stands for Create, Read, Update, and Delete, which are the basic operations used by most REST APIs.



 

What is CRUD?
CRUD represents the four main operations that we usually perform when working with data.

Create  → Add new data
Read    → Retrieve existing data
Update  → Modify existing data
Delete  → Remove existing data

In a REST API, these operations are usually mapped to HTTP methods.

POST    → Create
GET     → Read
PUT     → Full update
PATCH   → Partial update
DELETE  → Delete

 

Current API state
In the previous post, we created two read-only endpoints.

GET /api/projects/
GET /api/tasks/

These endpoints allow us to list projects and tasks, but we still cannot create, update, or delete data through the API.

In this post, we will add complete CRUD endpoints for both resources.

 

Updating the project views
Open config/projects/views.py. We will start by updating the project_list view to support both listing and creating projects.

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer


@api_view(['GET', 'POST'])
def project_list(request):
    if request.method == 'GET':
        projects = Project.objects.all()
        serializer = ProjectSerializer(projects, many=True)
        return Response(serializer.data)

    if request.method == 'POST':
        serializer = ProjectSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

This view now supports two operations.

GET   /api/projects/  → List all projects
POST  /api/projects/  → Create a new project

When the request method is POST, the serializer receives request.data, validates it, and saves a new project if the data is valid.

 

Creating a project detail view
Now we need an endpoint that works with a single project.

This view will allow us to retrieve, update, partially update, and delete one project by its ID.

@api_view(['GET', 'PUT', 'PATCH', 'DELETE'])
def project_detail(request, pk):
    try:
        project = Project.objects.get(pk=pk)
    except Project.DoesNotExist:
        return Response(
            {'error': 'Project not found'},
            status=status.HTTP_404_NOT_FOUND
        )

    if request.method == 'GET':
        serializer = ProjectSerializer(project)
        return Response(serializer.data)

    if request.method == 'PUT':
        serializer = ProjectSerializer(project, data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'PATCH':
        serializer = ProjectSerializer(project, data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'DELETE':
        project.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

This gives us the complete set of CRUD operations for a single project.

GET     /api/projects/1/  → Retrieve project with ID 1
PUT     /api/projects/1/  → Fully update project with ID 1
PATCH   /api/projects/1/  → Partially update project with ID 1
DELETE  /api/projects/1/  → Delete project with ID 1

 

PUT vs PATCH
Both PUT and PATCH are used to update data, but they are not exactly the same.

PUT    → Replaces the full resource
PATCH  → Updates only the provided fields

For example, with PATCH, we can update only the project name without sending the description again.

{
    "name": "Updated Project Name"
}

In the view, this is possible because we use partial=True.

serializer = ProjectSerializer(project, data=request.data, partial=True)

 

Updating the task views
Now let’s add the same CRUD functionality for tasks.

In config/projects/views.py, update the task_list view:

@api_view(['GET', 'POST'])
def task_list(request):
    if request.method == 'GET':
        tasks = Task.objects.all()
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

    if request.method == 'POST':
        serializer = TaskSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

This endpoint now supports listing and creating tasks.

GET   /api/tasks/  → List all tasks
POST  /api/tasks/  → Create a new task

 

Creating a task detail view
Now add the detail view for a single task.

@api_view(['GET', 'PUT', 'PATCH', 'DELETE'])
def task_detail(request, pk):
    try:
        task = Task.objects.get(pk=pk)
    except Task.DoesNotExist:
        return Response(
            {'error': 'Task not found'},
            status=status.HTTP_404_NOT_FOUND
        )

    if request.method == 'GET':
        serializer = TaskSerializer(task)
        return Response(serializer.data)

    if request.method == 'PUT':
        serializer = TaskSerializer(task, data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'PATCH':
        serializer = TaskSerializer(task, data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'DELETE':
        task.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

This gives us full CRUD functionality for tasks.

GET     /api/tasks/1/  → Retrieve task with ID 1
PUT     /api/tasks/1/  → Fully update task with ID 1
PATCH   /api/tasks/1/  → Partially update task with ID 1
DELETE  /api/tasks/1/  → Delete task with ID 1

 

Complete views.py file
At this point, your config/projects/views.py file should look like this:

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

from .models import Project, Task
from .serializers import ProjectSerializer, TaskSerializer


@api_view(['GET', 'POST'])
def project_list(request):
    if request.method == 'GET':
        projects = Project.objects.all()
        serializer = ProjectSerializer(projects, many=True)
        return Response(serializer.data)

    if request.method == 'POST':
        serializer = ProjectSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET', 'PUT', 'PATCH', 'DELETE'])
def project_detail(request, pk):
    try:
        project = Project.objects.get(pk=pk)
    except Project.DoesNotExist:
        return Response(
            {'error': 'Project not found'},
            status=status.HTTP_404_NOT_FOUND
        )

    if request.method == 'GET':
        serializer = ProjectSerializer(project)
        return Response(serializer.data)

    if request.method == 'PUT':
        serializer = ProjectSerializer(project, data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'PATCH':
        serializer = ProjectSerializer(project, data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'DELETE':
        project.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)


@api_view(['GET', 'POST'])
def task_list(request):
    if request.method == 'GET':
        tasks = Task.objects.all()
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

    if request.method == 'POST':
        serializer = TaskSerializer(data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET', 'PUT', 'PATCH', 'DELETE'])
def task_detail(request, pk):
    try:
        task = Task.objects.get(pk=pk)
    except Task.DoesNotExist:
        return Response(
            {'error': 'Task not found'},
            status=status.HTTP_404_NOT_FOUND
        )

    if request.method == 'GET':
        serializer = TaskSerializer(task)
        return Response(serializer.data)

    if request.method == 'PUT':
        serializer = TaskSerializer(task, data=request.data)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'PATCH':
        serializer = TaskSerializer(task, data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    if request.method == 'DELETE':
        task.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

 

Updating the URLs
Now we need to connect the new detail views to our API URLs.

Open config/projects/urls.py and update it like this:

from django.urls import path
from . import views

urlpatterns = [
    path('projects/', views.project_list, name='project-list'),
    path('projects/<int:pk>/', views.project_detail, name='project-detail'),

    path('tasks/', views.task_list, name='task-list'),
    path('tasks/<int:pk>/', views.task_detail, name='task-detail'),
]

Now our API has both list and detail endpoints.

GET     /api/projects/
POST    /api/projects/
GET     /api/projects/1/
PUT     /api/projects/1/
PATCH   /api/projects/1/
DELETE  /api/projects/1/

GET     /api/tasks/
POST    /api/tasks/
GET     /api/tasks/1/
PUT     /api/tasks/1/
PATCH   /api/tasks/1/
DELETE  /api/tasks/1/

 

Testing GET requests in the browser
Run the development server:

python manage.py runserver

Then open the following URLs in your browser:

Projects: http://localhost:8000/api/projects/
Tasks: http://localhost:8000/api/tasks/

You can also open a detail endpoint:

Project Detail: http://localhost:8000/api/projects/1/
Task Detail: http://localhost:8000/api/tasks/1/

 

Creating data with POST
To create a new project, send a POST request to:

POST /api/projects/

Example JSON body:

{
    "name": "New Website",
    "description": "Build a new company website"
}

If the request is successful, the API should return a 201 Created response.

{
    "id": 1,
    "name": "New Website",
    "description": "Build a new company website",
    "created_at": "2026-05-05T12:00:00Z"
}

 

Creating a task with POST
To create a new task, send a POST request to:

POST /api/tasks/

Example JSON body:

{
    "project": 1,
    "title": "Create homepage",
    "description": "Create the first version of the homepage",
    "completed": false
}

The project field receives the ID of the project this task belongs to.

 

Updating data with PUT
To fully update a project, send a PUT request to the detail endpoint.

PUT /api/projects/1/

Example JSON body:

{
    "name": "Updated Website",
    "description": "Updated project description"
}

With PUT, we usually send all required fields for that resource.

 

Updating data with PATCH
To partially update a project, send a PATCH request.

PATCH /api/projects/1/

Example JSON body:

{
    "description": "Only the description was updated"
}

With PATCH, we only send the fields we want to change.

 

Deleting data with DELETE
To delete a project, send a DELETE request to the detail endpoint.

DELETE /api/projects/1/

If the deletion is successful, the API returns a 204 No Content response.

This means the request was successful, but there is no response body to return.

 

Testing with the browsable API
Django REST Framework includes a browsable API, which makes it easier to test endpoints directly in the browser.

When you open an endpoint such as:

http://localhost:8000/api/projects/

You should see an interface where you can inspect responses and submit data using forms.

This is very useful while learning and developing APIs.

 

What we have built
In this post, we added complete CRUD functionality to our API.

Listed projects and tasks
Created projects and tasks
Retrieved individual resources
Updated resources with PUT
Partially updated resources with PATCH
Deleted resources
Returned proper HTTP status codes

Our API is now much more useful because clients can fully interact with the project and task resources.

 

What comes next?
In the next post, we will improve this code by using more powerful Django REST Framework abstractions.

Instead of manually writing every CRUD operation, we will compare APIView, generic views, and ViewSets.

Django REST API: APIView vs Generic Views vs ViewSets