MofidAPI Hub — Full Project From Scratch
This project combines the topics covered in Tutorials 1 to 10:
- Introduction to Django REST Framework
- Building your first API
- CRUD APIs
- ViewSets and Routers
- Authentication
- Permissions
- Pagination, Filtering, and Search
- Uploading Files via API
- API Testing
- Connecting Django to Frontend Frameworks
The stack used here is:
- Backend: Django + Django REST Framework + django-filter
- Frontend: React (Vite)
- Database: SQLite for simplicity
- Auth: Session authentication
- Uploads: Multipart uploads with media files
- Tests: DRF APITestCase
🧱 1. Prerequisites (What you must install first)
Before creating anything, make sure you have:
🔹 Backend requirements
- Python (>= 3.10 recommended)
pip(comes with Python)venv(comes with Python)
🔹 Frontend requirements
- Node.js (>= 18 recommended)
- npm (comes with Node.js)
👉 Check installations:
python --version
pip --version
node -v
npm -v📁 2. Create the Project Folder
mkdir mofidapi-hub
cd mofidapi-hub🐍 3. Backend Setup (Django + DRF)
Step 1: Create backend folder
mkdir backend
cd backendStep 2: Create virtual environment
python -m venv venvActivate it:
Windows:
venv\Scripts\activateLinux / Mac:
source venv/bin/activate👉 You should see (venv) in your terminal
Step 3: Install backend packages
pip install django djangorestframework django-filter pillowSave dependencies (IMPORTANT)
pip freeze > requirements.txtStep 4: Create Django project
django-admin startproject config .Step 5: Create apps
python manage.py startapp accounts
python manage.py startapp posts
python manage.py startapp uploads
Step 6: Run migrations
python manage.py makemigrations
python manage.py migrate
Step 7: Create admin user
python manage.py createsuperuserStep 8: Run backend server
python manage.py runserver👉 Open:
⚛️ 4. Frontend Setup (React + Vite)
Now go back to root folder:
cd ..Step 1: Create frontend app with Vite
npm create vite@latest frontendChoose:
- Framework → React
- Variant → JavaScript
Step 2: Go into frontend
cd frontendStep 3: Install dependencies
npm installStep 4: Install React plugin (important)
npm install @vitejs/plugin-reactStep 5: Run frontend
npm run dev👉 Open:
5. Project structure
mofidapi-hub/
├── backend/
│ ├── manage.py
│ ├── requirements.txt
│ ├── config/
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ ├── wsgi.py
│ │ └── asgi.py
│ ├── accounts/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── migrations/__init__.py
│ ├── posts/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── permissions.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── migrations/__init__.py
│ ├── uploads/
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ └── migrations/__init__.py
│ └── tests/
│ ├── __init__.py
│ ├── test_auth.py
│ ├── test_posts.py
│ └── test_uploads.py
└── frontend/
├── package.json
├── vite.config.js
├── index.html
└── src/
├── main.jsx
├── App.jsx
├── api.js
├── styles.css
└── components/
├── LoginForm.jsx
├── RegisterForm.jsx
├── PostList.jsx
├── CreatePostForm.jsx
└── UploadDocumentForm.jsx
6. Backend files
backend/requirements.txt
Django>=5.0,<6.0
djangorestframework>=3.15
django-filter>=24.0
Pillow>=10.0
backend/manage.py
#!/usr/bin/env python
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and available on your "
"PYTHONPATH environment variable? Did you forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
backend/config/init.py
backend/config/settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-change-this-in-production'
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'accounts',
'posts',
'uploads',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
ASGI_APPLICATION = 'config.asgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 5,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
backend/config/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from posts.views import PostViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('accounts.urls')),
path('api/uploads/', include('uploads.urls')),
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
backend/config/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()
backend/config/asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_asgi_application()
7. Accounts app
backend/accounts/apps.py
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
backend/accounts/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
def __str__(self):
return self.username
backend/accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
admin.site.register(User, UserAdmin)
backend/accounts/serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=6)
password_confirm = serializers.CharField(write_only=True, min_length=6)
class Meta:
model = User
fields = ['id', 'username', 'email', 'password', 'password_confirm']
def validate(self, attrs):
if attrs['password'] != attrs['password_confirm']:
raise serializers.ValidationError({'password_confirm': 'Passwords do not match.'})
return attrs
def create(self, validated_data):
validated_data.pop('password_confirm')
return User.objects.create_user(**validated_data)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
backend/accounts/views.py
from django.contrib.auth import authenticate, login, logout
from rest_framework import generics, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import RegisterSerializer, UserSerializer
class RegisterView(generics.CreateAPIView):
serializer_class = RegisterSerializer
permission_classes = [AllowAny]
class LoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(request, username=username, password=password)
if user is None:
return Response({'detail': 'Invalid credentials.'}, status=status.HTTP_400_BAD_REQUEST)
login(request, user)
return Response({
'message': 'Login successful.',
'user': UserSerializer(user).data,
})
class LogoutView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
logout(request)
return Response({'message': 'Logout successful.'})
class MeView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response(UserSerializer(request.user).data)
backend/accounts/urls.py
from django.urls import path
from .views import RegisterView, LoginView, LogoutView, MeView
urlpatterns = [
path('register/', RegisterView.as_view(), name='register'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('me/', MeView.as_view(), name='me'),
]
backend/accounts/migrations/init.py
8. Posts app
backend/posts/apps.py
from django.apps import AppConfig
class PostsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'posts'
backend/posts/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
CATEGORY_CHOICES = [
('django', 'Django'),
('drf', 'DRF'),
('python', 'Python'),
('frontend', 'Frontend'),
]
title = models.CharField(max_length=200)
content = models.TextField()
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='drf')
image = models.ImageField(upload_to='post_images/', blank=True, null=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='posts')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
backend/posts/admin.py
from django.contrib import admin
from .models import Post
admin.site.register(Post)
backend/posts/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.author == request.user
backend/posts/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author_username = serializers.ReadOnlyField(source='author.username')
class Meta:
model = Post
fields = [
'id',
'title',
'content',
'category',
'image',
'author',
'author_username',
'created_at',
'updated_at',
]
read_only_fields = ['author', 'created_at', 'updated_at']
backend/posts/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Post
from .permissions import IsOwnerOrReadOnly
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author').all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
search_fields = ['title', 'content', 'author__username']
filterset_fields = ['category', 'author']
ordering_fields = ['created_at', 'title']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
backend/posts/urls.py
# Not used because posts are routed directly in config/urls.py
backend/posts/migrations/init.py
9. Uploads app
backend/uploads/apps.py
from django.apps import AppConfig
class UploadsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'uploads'
backend/uploads/models.py
from django.conf import settings
from django.db import models
class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(upload_to='documents/')
uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='documents')
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-uploaded_at']
def __str__(self):
return self.title
backend/uploads/admin.py
from django.contrib import admin
from .models import Document
admin.site.register(Document)
backend/uploads/serializers.py
from rest_framework import serializers
from .models import Document
class DocumentSerializer(serializers.ModelSerializer):
uploaded_by_username = serializers.ReadOnlyField(source='uploaded_by.username')
class Meta:
model = Document
fields = ['id', 'title', 'file', 'uploaded_by', 'uploaded_by_username', 'uploaded_at']
read_only_fields = ['uploaded_by', 'uploaded_at']
backend/uploads/views.py
from rest_framework import generics
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.permissions import IsAuthenticated
from .models import Document
from .serializers import DocumentSerializer
class DocumentListCreateView(generics.ListCreateAPIView):
queryset = Document.objects.select_related('uploaded_by').all()
serializer_class = DocumentSerializer
parser_classes = [MultiPartParser, FormParser]
permission_classes = [IsAuthenticated]
search_fields = ['title']
def perform_create(self, serializer):
serializer.save(uploaded_by=self.request.user)
backend/uploads/urls.py
from django.urls import path
from .views import DocumentListCreateView
urlpatterns = [
path('documents/', DocumentListCreateView.as_view(), name='document-list-create'),
]
backend/uploads/migrations/init.py
10. Tests
backend/tests/init.py
backend/tests/test_auth.py
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APITestCase
User = get_user_model()
class AuthTests(APITestCase):
def test_register_user(self):
response = self.client.post(reverse('register'), {
'username': 'newuser',
'email': 'new@example.com',
'password': 'secret123',
'password_confirm': 'secret123',
}, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(User.objects.count(), 1)
def test_login_user(self):
User.objects.create_user(username='tester', password='secret123')
response = self.client.post(reverse('login'), {
'username': 'tester',
'password': 'secret123',
}, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['user']['username'], 'tester')
backend/tests/test_posts.py
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APITestCase
from posts.models import Post
User = get_user_model()
class PostTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='author', password='secret123')
self.other = User.objects.create_user(username='other', password='secret123')
self.post = Post.objects.create(
title='DRF Basics',
content='Learning DRF step by step.',
category='drf',
author=self.user,
)
def test_list_posts(self):
response = self.client.get(reverse('post-list'))
self.assertEqual(response.status_code, 200)
self.assertIn('results', response.data)
def test_create_post_authenticated(self):
self.client.login(username='author', password='secret123')
response = self.client.post(reverse('post-list'), {
'title': 'New Post',
'content': 'Post content',
'category': 'django',
}, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(Post.objects.count(), 2)
def test_create_post_anonymous_denied(self):
response = self.client.post(reverse('post-list'), {
'title': 'Anonymous',
'content': 'Should fail',
'category': 'python',
}, format='json')
self.assertIn(response.status_code, [401, 403])
def test_owner_can_update_post(self):
self.client.login(username='author', password='secret123')
response = self.client.patch(reverse('post-detail', args=[self.post.id]), {
'title': 'Updated Title'
}, format='json')
self.assertEqual(response.status_code, 200)
self.post.refresh_from_db()
self.assertEqual(self.post.title, 'Updated Title')
def test_non_owner_cannot_update_post(self):
self.client.login(username='other', password='secret123')
response = self.client.patch(reverse('post-detail', args=[self.post.id]), {
'title': 'Hack Title'
}, format='json')
self.assertEqual(response.status_code, 403)
def test_search_posts(self):
response = self.client.get(reverse('post-list'), {'search': 'DRF'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
backend/tests/test_uploads.py
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from rest_framework.test import APITestCase
from uploads.models import Document
User = get_user_model()
class UploadTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='uploader', password='secret123')
def test_upload_document(self):
self.client.login(username='uploader', password='secret123')
file_obj = SimpleUploadedFile('test.txt', b'hello world', content_type='text/plain')
response = self.client.post(reverse('document-list-create'), {
'title': 'My Document',
'file': file_obj,
}, format='multipart')
self.assertEqual(response.status_code, 201)
self.assertEqual(Document.objects.count(), 1)
self.assertEqual(Document.objects.first().title, 'My Document')
11. Frontend files
frontend/package.json
{
"name": "mofidapi-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"vite": "^5.4.8"
}
}
frontend/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
},
})
frontend/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MofidAPI Hub</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
frontend/src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
frontend/src/api.js
const API_BASE = 'http://127.0.0.1:8000/api'
async function request(url, options = {}) {
const response = await fetch(url, {
credentials: 'include',
...options,
})
const contentType = response.headers.get('content-type') || ''
const data = contentType.includes('application/json')
? await response.json()
: await response.text()
if (!response.ok) {
throw new Error(typeof data === 'string' ? data : JSON.stringify(data))
}
return data
}
export async function register(payload) {
return request(`${API_BASE}/auth/register/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function login(payload) {
return request(`${API_BASE}/auth/login/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function logout() {
return request(`${API_BASE}/auth/logout/`, {
method: 'POST',
})
}
export async function getCurrentUser() {
return request(`${API_BASE}/auth/me/`)
}
export async function getPosts(search = '') {
const query = search ? `?search=${encodeURIComponent(search)}` : ''
return request(`${API_BASE}/posts/${query}`)
}
export async function createPost(payload) {
return request(`${API_BASE}/posts/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
}
export async function uploadDocument(formData) {
return request(`${API_BASE}/uploads/documents/`, {
method: 'POST',
body: formData,
})
}
frontend/src/App.jsx
import { useEffect, useState } from 'react'
import { getCurrentUser, getPosts } from './api'
import LoginForm from './components/LoginForm'
import RegisterForm from './components/RegisterForm'
import PostList from './components/PostList'
import CreatePostForm from './components/CreatePostForm'
import UploadDocumentForm from './components/UploadDocumentForm'
export default function App() {
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
const [search, setSearch] = useState('')
const [error, setError] = useState('')
async function loadPosts(value = '') {
try {
const data = await getPosts(value)
setPosts(data.results || data)
} catch (err) {
setError(err.message)
}
}
async function loadUser() {
try {
const data = await getCurrentUser()
setUser(data)
} catch {
setUser(null)
}
}
useEffect(() => {
loadUser()
loadPosts()
}, [])
return (
<div className="container">
<header className="hero">
<h1>MofidAPI Hub</h1>
<p>A full-stack Django + DRF + React project covering Tutorials 61 to 70.</p>
</header>
{error && <div className="alert">{error}</div>}
<div className="grid two">
<LoginForm onLoginSuccess={loadUser} />
<RegisterForm />
</div>
<section className="card">
<h2>Current user</h2>
<p>{user ? `${user.username} (${user.email || 'no email'})` : 'Not logged in'}</p>
</section>
<div className="grid two">
<CreatePostForm onCreated={() => loadPosts(search)} />
<UploadDocumentForm />
</div>
<section className="card">
<div className="row between">
<h2>Posts</h2>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search posts..."
/>
<button onClick={() => loadPosts(search)}>Search</button>
</div>
<PostList posts={posts} />
</section>
</div>
)
}
frontend/src/components/LoginForm.jsx
import { useState } from 'react'
import { login } from '../api'
export default function LoginForm({ onLoginSuccess }) {
const [form, setForm] = useState({ username: '', password: '' })
const [message, setMessage] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await login(form)
setMessage('Login successful')
setForm({ username: '', password: '' })
onLoginSuccess()
} catch (err) {
setMessage(err.message)
}
}
return (
<section className="card">
<h2>Login</h2>
<form onSubmit={handleSubmit} className="stack">
<input
placeholder="Username"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
<button type="submit">Login</button>
{message && <p>{message}</p>}
</form>
</section>
)
}
frontend/src/components/RegisterForm.jsx
import { useState } from 'react'
import { register } from '../api'
export default function RegisterForm() {
const [form, setForm] = useState({
username: '',
email: '',
password: '',
password_confirm: '',
})
const [message, setMessage] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await register(form)
setMessage('Registration successful')
setForm({ username: '', email: '', password: '', password_confirm: '' })
} catch (err) {
setMessage(err.message)
}
}
return (
<section className="card">
<h2>Register</h2>
<form onSubmit={handleSubmit} className="stack">
<input
placeholder="Username"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
/>
<input
placeholder="Email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
<input
type="password"
placeholder="Confirm password"
value={form.password_confirm}
onChange={(e) => setForm({ ...form, password_confirm: e.target.value })}
/>
<button type="submit">Register</button>
{message && <p>{message}</p>}
</form>
</section>
)
}
frontend/src/components/PostList.jsx
export default function PostList({ posts }) {
if (!posts.length) {
return <p>No posts found.</p>
}
return (
<div className="stack">
{posts.map((post) => (
<article key={post.id} className="post">
<h3>{post.title}</h3>
<p><strong>Author:</strong> {post.author_username}</p>
<p><strong>Category:</strong> {post.category}</p>
<p>{post.content}</p>
{post.image && <img src={post.image} alt={post.title} className="post-image" />}
</article>
))}
</div>
)
}
frontend/src/components/CreatePostForm.jsx
import { useState } from 'react'
import { createPost } from '../api'
export default function CreatePostForm({ onCreated }) {
const [form, setForm] = useState({ title: '', content: '', category: 'drf' })
const [message, setMessage] = useState('')
async function handleSubmit(e) {
e.preventDefault()
try {
await createPost(form)
setMessage('Post created successfully')
setForm({ title: '', content: '', category: 'drf' })
onCreated()
} catch (err) {
setMessage(err.message)
}
}
return (
<section className="card">
<h2>Create Post</h2>
<form onSubmit={handleSubmit} className="stack">
<input
placeholder="Title"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
/>
<textarea
placeholder="Content"
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
rows="6"
/>
<select
value={form.category}
onChange={(e) => setForm({ ...form, category: e.target.value })}
>
<option value="drf">DRF</option>
<option value="django">Django</option>
<option value="python">Python</option>
<option value="frontend">Frontend</option>
</select>
<button type="submit">Create</button>
{message && <p>{message}</p>}
</form>
</section>
)
}
frontend/src/components/UploadDocumentForm.jsx
import { useState } from 'react'
import { uploadDocument } from '../api'
export default function UploadDocumentForm() {
const [title, setTitle] = useState('')
const [file, setFile] = useState(null)
const [message, setMessage] = useState('')
async function handleSubmit(e) {
e.preventDefault()
if (!file) {
setMessage('Please select a file.')
return
}
const formData = new FormData()
formData.append('title', title)
formData.append('file', file)
try {
await uploadDocument(formData)
setMessage('Document uploaded successfully')
setTitle('')
setFile(null)
} catch (err) {
setMessage(err.message)
}
}
return (
<section className="card">
<h2>Upload Document</h2>
<form onSubmit={handleSubmit} className="stack">
<input
placeholder="Document title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input type="file" onChange={(e) => setFile(e.target.files[0])} />
<button type="submit">Upload</button>
{message && <p>{message}</p>}
</form>
</section>
)
}
frontend/src/styles.css
:root {
--bg: #0f172a;
--panel: #111827;
--card: #1f2937;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #38bdf8;
--border: #334155;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 24px;
}
.hero {
margin-bottom: 24px;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
}
.grid.two {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.row.between {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
}
input, textarea, select, button {
padding: 12px;
border-radius: 10px;
border: 1px solid var(--border);
font: inherit;
}
input, textarea, select {
background: #0b1220;
color: var(--text);
}
button {
background: var(--accent);
color: #001018;
font-weight: bold;
cursor: pointer;
}
.alert {
background: #7f1d1d;
color: white;
border-radius: 10px;
padding: 12px;
margin-bottom: 20px;
}
.post {
padding: 16px;
border: 1px solid var(--border);
border-radius: 12px;
background: #0b1220;
}
.post-image {
max-width: 100%;
border-radius: 10px;
margin-top: 12px;
}
12. How to run the backend
cd backend
python -m venv venv
source venv/bin/activate # on Windows: venv\Scripts\activate
pip install -r requirements.txt
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
Run tests:
python manage.py test
13. How to run the frontend
cd frontend
npm install
npm run dev
14. What this project demonstrates
This single project covers Tutorials 1 to 10 by including:
- DRF introduction and setup
- first API patterns
- CRUD endpoints
- ViewSets and Routers
- session-based authentication
- permissions with ownership rules
- pagination, filtering, and search
- file upload via multipart requests
- DRF API tests
- frontend integration with React
💬 Comments
No comments yet. Be the first to comment!
Login to comment.