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 backend

Step 2: Create virtual environment

python -m venv venv

Activate it:

Windows:

venv\Scripts\activate

Linux / Mac:

source venv/bin/activate

👉 You should see (venv) in your terminal

Step 3: Install backend packages

pip install django djangorestframework django-filter pillow

Save dependencies (IMPORTANT)

pip freeze > requirements.txt

Step 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 createsuperuser

Step 8: Run backend server

python manage.py runserver

👉 Open:

http://127.0.0.1:8000/

⚛️ 4. Frontend Setup (React + Vite)

Now go back to root folder:

cd ..

Step 1: Create frontend app with Vite

npm create vite@latest frontend

Choose:

  • Framework → React
  • Variant → JavaScript

Step 2: Go into frontend

cd frontend

Step 3: Install dependencies

npm install

Step 4: Install React plugin (important)

npm install @vitejs/plugin-react

Step 5: Run frontend

npm run dev

👉 Open:

http://localhost:5173/

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