iTranslated by AI
Implementing CRUD Functionality in Django Using Postman (Implementation Part)
Overview
In this article, I will create a new UI feature and write about implementing the structure and visualizing the data behavior between the frontend and backend using "Postman" in parallel.
Implementation Features
The feature we will implement this time is a content type called "Tips," which organizes small tips and articles in a knowledge base style.
- API integration and CRUD functionality using Django REST Framework (DRF)
- CRUD functionality referencing foreign keys
- Use of parameters
- Routing in React Router referencing parameters
| No. | Article |
|---|---|
| 1 | Frontend/Backend Data Integration using React + Django + CORS |
| 2 | Customizing the Django Admin Panel |
| 3 | API Server using Django REST framework (DRF) and Data Integration with React |
| 4 | Referencing Foreign Key Models using Serializers in Django REST framework |
| 5 | Verification of Asynchronous Communication using React + Redux / Redux Toolkit |
| 6 | CRUD Implementation with Django using "Postman" Testing Tool (Design) |
| 7 | CRUD Implementation with Django using "Postman" Testing Tool (Implementation) (This article) |
Folder Structure
Extracting the main components used this time.
- Backend (Python / Django)
.
├── backend_django
│ └── settings.py
├── django_app
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ └── views.py
- Frontend (React / Redux)
.
├── features
│ ├── tips
│ │ ├── tipsCategorizeSlice.js
│ │ ├── tipsDetailSlice.js
│ │ ├── tipsEditSlice.js
│ │ └── tipsSlice.js
├── pages
│ ├── tips
│ │ ├── TipsCategorize.js
│ │ ├── TipsCreate.js
│ │ ├── TipsDetail.js
│ │ ├── TipsEdit.js
│ │ └── TipsIndex.js
│ └── DashBoard.js
└── store
└── index.js
Implementation Method
Backend
First, let's share the URL patterns as a structural overview. The configuration is as follows:
- tips/create/: Create Tips
- tips/update/: Edit/Update Tips
- tips/delete/: Delete Tips
- Others: Tips list, category-based list, and Tips details
from django.urls import path, include
from . import views
urlpatterns = [
:
path("tips/", views.tips_contents, name="tips_contents"),
path("tips/create/", views.tips_contents_create.as_view(), name="tips_contents_create"),
path("tips/update/<int:pk>", views.tips_contents_update.as_view(), name="tips_contents_update"),
path("tips/delete/<int:pk>", views.tips_contents_delete.as_view(), name="tips_contents_delete"),
path("tips/<category_path>/", views.tips_category, name="tips_category"),
path("tips/<category_path>/<int:pk>", views.tips_contents_detail, name="tips_contents_detail"),
]
- Review of the previous part
Use
views.function_namefor function-based views andviews.ClassName.as_view()for class-based views.
While it might be better to unify everything into class-based views for better versatility, I have used the following configuration this time for verification purposes:
- Read only: Function-based View
- Includes non-Read operations: Class-based View
Also, for endpoints other than the creation one, category_path and pk are set as path parameters.
One point to note here is the order of the paths; while it is much the same as other languages, it is worth keeping in mind that they are applied in the order they are written.
For example, if tips/<category_path>/ were listed before tips/create/, then accessing the tips/create/ endpoint would be processed as tips/<category_path>/, resulting in errors due to structural differences.
Endpoints with fixed strings should be placed earlier in the sequence.
from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.decorators import api_view
# Tips: Retrieve a single record
def tips_contents_detail(request, category_path, pk):
print("category_path: ", category_path)
print("pk: ", pk)
queryset = TipsContents.objects.get(category__tips_path=category_path, id=pk)
serializer_class = TipsContentsSerializer(queryset)
data = serializer_class.data
return JsonResponse(data, safe=False)
# Tips: Create new
@method_decorator(csrf_exempt, name='dispatch')
class tips_contents_create(APIView):
# GET: For confirmation
def get(self, request):
queryset = TipsContents.objects.all()
serializer_class = TipsContentsSerializer(queryset, many=True)
data = serializer_class.data
return JsonResponse(data, safe=False)
# POST: Execute
def post(self, request):
print("request: ", request)
print("request.data: ", request.data)
serializer_class = TipsContentsSerializer(data=request.data)
if serializer_class.is_valid():
serializer_class.save()
return JsonResponse(serializer_class.data, status=201)
return JsonResponse(serializer_class.errors, status=400)
# Tips: Update
@method_decorator(csrf_exempt, name='dispatch')
class tips_contents_update(APIView):
# GET: Referenced during editing
def get(self, request, pk):
# Retrieve a single record
queryset = TipsContents.objects.get(id=pk)
serializer_class = TipsContentsSerializer(queryset)
data = serializer_class.data
return JsonResponse(data, safe=False)
# POST: Execute
def post(self, request, pk):
print("request: ", request)
print("request.data: ", request.data)
queryset = TipsContents.objects.get(id=pk)
serializer_class = TipsContentsSerializer(queryset, data=request.data)
if serializer_class.is_valid():
serializer_class.save()
return JsonResponse(serializer_class.data, status=201)
return JsonResponse(serializer_class.errors, status=400)
# Tips: Delete
@method_decorator(csrf_exempt, name='dispatch')
class tips_contents_delete(APIView):
# GET: For confirmation
def get(self, request, pk):
# Retrieve a single record
queryset = TipsContents.objects.get(id=pk)
serializer_class = TipsContentsSerializer(queryset)
data = serializer_class.data
return JsonResponse(data, safe=False)
# POST: Execute
def post(self, request, pk):
print("request: ", request)
print("request.data: ", request.data)
queryset = TipsContents.objects.get(id=pk)
queryset.delete()
return JsonResponse({
"message": "delete success",
}, status=201)
As in the previous article, we have added decorators to bypass CSRF.
- Added
@csrf_exemptto function-based views. - Added
@method_decorator(csrf_exempt, name='dispatch')to class-based views.
Path parameters defined in urls.py have been added as arguments to each view, in addition to self and request.
from django.db import models
:
class TipsCategory(BaseMeta):
id = models.AutoField(primary_key=True)
tips_name = models.CharField(max_length=100)
tips_path = models.CharField(max_length=100, null=True, default='tips')
class Meta:
db_table = 'tips_category'
verbose_name_plural = 'Util_TipsCategory'
def __str__(self):
return self.tips_name
class TipsContents(BaseMeta):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=255)
date = models.DateField()
content = models.TextField()
category = models.ForeignKey(TipsCategory, on_delete=models.PROTECT, null=True)
class Meta:
db_table = 'tips'
verbose_name_plural = 'Tips_List'
def __str__(self):
return self.title
Since the basic structure remains the same for each CRUD operation, the model is unified under TipsContents. TipsCategory is used only for foreign key references and is a model that cannot be manipulated by users from the frontend UI.
from rest_framework import serializers
from .models import (
:
TipsContents,
)
class TipsContentsSerializer(serializers.ModelSerializer):
# Get the category of the foreign key
category = TipsCategorySerializer()
class Meta:
model = TipsContents
fields = ('id', 'title', 'date', 'content', 'category', 'created_at', 'updated_at')
def create(self, validated_data):
# Since category is a foreign key, retrieve the tips_path and register it
validated_data['category'] = TipsCategory.objects.get(tips_path=validated_data['category'].get('tips_path'))
return TipsContents.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.date = validated_data.get('date', instance.date)
instance.content = validated_data.get('content', instance.content)
# Since category is a foreign key, retrieve the tips_path and update it
instance.category = TipsCategory.objects.get(tips_path=validated_data.get('category').get('tips_path'))
instance.save()
return instance
def delete(self, instance):
instance.delete()
return instance
The core of this implementation was the serializer part.
When I was initially proceeding with the design with only class Meta defined, I encountered the following message:
AssertionError: The
.update()method does not support writable nested fields by default. Write an explicit.update()method for serializerdjango_app.serializers.TipsContentsSerializer, or setread_only=Trueon nested serializer fields.
This explanation states that update processing is not supported by default when serializer fields are nested. To resolve this, you need to either write an explicit .update() method or set read_only=True on the nested serializer fields.
In this case, the foreign key category is affected, and since this applies not only to UPDATE in the error message but also to CREATE and DELETE, I implemented them together.
The flow of each method is basically as follows:
- validated_data: Stores the data retrieved from the frontend
- instance: Stores the data referenced and formatted from validated_data
- After processing each, return the instance
For more details, please also refer to the DRF official website.
Frontend
Since it covers a wide range of components, I will explain it using the screen for referencing and updating specific Tips as an example.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
isLoading: false,
items: [],
};
const BASE_API_URL = "http://localhost:8000/api";
export const fetchGetTipsToEdit = createAsyncThunk(
"tips_list",
async (params) => {
console.log("params: ", params);
const connect_url = `${BASE_API_URL}/tips/update/${params.tips_id}`;
console.log("connect_url: ", connect_url);
const response = await axios.get(connect_url);
return response.data;
}
);
export const fetchUpdateTips = createAsyncThunk(
"update_tips_list",
async (data) => {
const tips_id = data.category.id;
const connect_url = `${BASE_API_URL}/tips/update/${tips_id}`;
try {
const response = await axios.post(connect_url, data);
console.log("updateTips: ", response);
return response.data;
}
catch (error) {
console.log("updateTips_error: ", error);
}
}
);
// Slices
export const tipsDetailSlice = createSlice({
name: "tips_detail", // Name of the slice
initialState: initialState,
reducers: {},
// Fetching data from external sources
extraReducers: (builder) => {
// TODO: Add error handling
builder
.addCase(fetchGetTipsToEdit.pending, (state) => {
console.log("pending..");
return {
...state,
isLoading: true,
};
})
.addCase(fetchGetTipsToEdit.fulfilled, (state, action) => {
console.log("fulfilled: ", action.payload);
return {
...state,
items: action.payload,
isLoading: false,
};
})
.addCase(fetchGetTipsToEdit.rejected, (state) => {
console.log("rejected..");
return {
...state,
isLoading: false,
};
});
},
});
// Export so that the state can be referenced from each component
export default tipsDetailSlice.reducer;
I have created the following as Slices for asynchronous processing:
- fetchGetTipsToEdit: Referencing the Tips to be edited
- fetchUpdateTips: Process for updating the Tips
import { combineReducers } from 'redux';
import { configureStore } from "@reduxjs/toolkit";
// Reducers
:
import tipsEditReducer from '../features/tips/tipsEditSlice';
const rootReducer = combineReducers({
:
tipsEditReducer,
});
// Store
const store = configureStore({
reducer: rootReducer,
});
export default store;
In the store, I import the created slice and define it as a reducer.
import { Routes, Route, Link } from 'react-router-dom';
import TipsEdit from "./pages/tips/TipsEdit";
const BaseApp = () => {
return (
<div className="app">
:
<Routes>
<Route path="/tips/edit/:tips_id" element={<TipsEdit />} />
</Routes>
</div>
)
}
import React, { useState, useEffect } from 'react'
import Axios from 'axios'
import { useSelector, useDispatch } from 'react-redux'
import { useParams } from 'react-router-dom'
import { fetchGetTipsToEdit, fetchUpdateTips } from '../../features/tips/tipsEditSlice'
const TipsEdit = () => {
const currentTipsDetail = useSelector((state) => state.tipsDetailReducer.items);
const isLoading = useSelector((state) => state.tipsDetailReducer.isLoading);
const dispatch = useDispatch();
const params = useParams(); // Get parameters from the URL
const [tipsState, setTipsState] = useState(currentTipsDetail);
useEffect(() => {
dispatch(fetchGetTipsToEdit(params));
}, []);
useEffect(() => {
setTipsState(currentTipsDetail);
}, [currentTipsDetail]);
const handleSubmit = (e, tipsState) => {
e.preventDefault();
:
dispatch(fetchUpdateTips(tipsState));
}
:
return (
<Container>
:
<Box>
<form method='POST' onSubmit={e => {handleSubmit(e, tipsState)}}>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableBody>
<TableRow >
<TableCell component="th" scope="row">
Title
</TableCell>
<TableCell align="right">
<TextField required id="outlined-basic" label="Required" variant="outlined"
value={tipsState.title}
onChange={e => setTipsState({...tipsState, title: e.target.value})}
/>
</TableCell>
</TableRow>
:
</TableBody>
</Table>
</TableContainer>
<Box className='section-footer'>
<Button variant="contained" color="primary" type='submit'>
Create Tips
</Button>
</Box>
</form>
</Box>
</Container>
)
}
export default TipsEdit
*Note: The component design uses MUI, but I will skip the explanation of that part.
In the screen component, useParams() is used to retrieve parameters from the accessed URL.
Upon loading, fetchGetTipsToEdit(params) is executed, which uses params.tips_id from the URL to access the backend API endpoint via axios.get.
Additionally, during data update, fetchUpdateTips is executed when the form button is clicked. It accesses the same API endpoint as before but uses axios.post, passing the submission data as an argument.
References
- Django REST Framework: Serializer
- DRF Serializer
Discussion