Django and React Tutorial for Beginners | Step‑by‑Step Guide by Abid Hasan

  


Step 1: Setting Up Your Environment

  • Install Python: Ensure you have Python installed on your machine. You can download it from python.org.
  • Install Django: Open your terminal and run:

pip install django or py -m pip install django 

  • Install Node.js: Download and install Node.js from nodejs.org.
  • Install Create React App: This tool helps you set up a new React project easily. Run:

npx create-react-app my-app

Step 2: Create a Django Project

• Start a New Django Project:

django-admin startproject myproject

Final folder structure:

myproject/
├── backend/
│ ├── manage.py
│ ├── config/
│ │ ├── settings.py
│ │ └── urls.py
│ └── products/
│ ├── models.py
│ ├── serializers.py
│ ├── views.py
│ └── urls.py
└── frontend/
├── index.html
└── src/
├── main.jsx
├── api/
├── components/
└── pages/

Django Project & Models

Create the project and app:

django-admin startproject config backend
cd backend && python manage.py startapp products

backend/config/settings.py — key sections:

INSTALLED_APPS = [
# django defaults...
'rest_framework',
'corsheaders',
'products',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # must be first!
'django.middleware.common.CommonMiddleware',
# other middlewares...
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # Vite dev server
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
],
}

backend/products/models.py:

from django.db import models
from django.contrib.auth.models import User
class Product(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products')
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created']
def __str__(self):
return self.name

Run migrations:

python manage.py makemigrations && python manage.py migrate
python manage.py createsuperuser

Step 3 — REST API (Serializers, Views, URLs)

products/serializers.py:

from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
owner_username = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Product
fields = ['id', 'owner_username', 'name', 'description', 'price', 'stock', 'created']
read_only_fields = ['id', 'created', 'owner_username']
def create(self, validated_data):
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)

products/views.py:

from rest_framework import viewsets, permissions
from .models import Product
from .serializers import ProductSerializer
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
permission_classes = [IsOwnerOrReadOnly]
def get_queryset(self):
qs = super().get_queryset()
search = self.request.query_params.get('search')
if search:
qs = qs.filter(name__icontains=search)
return qs

products/urls.py:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ProductViewSet
router = DefaultRouter()
router.register(r'products', ProductViewSet, basename='product')
urlpatterns = [
path('', include(router.urls)),
]

config/urls.py:

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('products.urls')),
path('api/token/', TokenObtainPairView.as_view()),
path('api/token/refresh/', TokenRefreshView.as_view()),
]

The router automatically gives you these endpoints:

  • GET /api/products/ — list all
  • POST /api/products/ — create
  • GET /api/products/1/ — detail
  • PUT /api/products/1/ — update
  • DELETE /api/products/1/ — delete

Step 4 — React App

src/api/axios.js — centralized config with auto token refresh:

import axios from 'axios';

const api = axios.create({
baseURL: '/api/',
});
// Attach JWT token to every request
api.interceptors.request.use(config => {
const token = localStorage.getItem('access');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Auto-refresh token on 401 error
api.interceptors.response.use(
res => res,
async err => {
if (err.response?.status === 401) {
const refresh = localStorage.getItem('refresh');
if (refresh) {
const { data } = await axios.post('/api/token/refresh/', { refresh });
localStorage.setItem('access', data.access);
err.config.headers.Authorization = `Bearer ${data.access}`;
return axios(err.config);
}
}
return Promise.reject(err);
}
);
export default api;

src/main.jsx — routing setup:

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ProductListPage from './pages/ProductListPage';
import ProductDetailPage from './pages/ProductDetailPage';
import LoginPage from './pages/LoginPage';
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<ProductListPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</BrowserRouter>
);

src/pages/ProductListPage.jsx:

import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import api from '../api/axios';
export default function ProductListPage() {
const [products, setProducts] = useState([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
api.get(`products/?search=${search}`)
.then(r => setProducts(r.data))
.finally(() => setLoading(false));
}, [search]);
return (
<div>
<h1>Products</h1>
<input
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
{loading && <p>Loading...</p>}
{products.map(p => (
<div key={p.id}>
<Link to={`/products/${p.id}`}><h2>{p.name}</h2></Link>
<p>{p.description}</p>
<strong>${p.price}</strong> · Stock: {p.stock}
</div>
))}
</div>
);
}

Step 5 — Connect Django & React

frontend/vite.config.js — proxy to avoid CORS in dev:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
});

Run both servers:

# Terminal 1
cd backend && python manage.py runserver

# Terminal 2
cd frontend && npm run dev

Test the connection:

curl http://localhost:8000/api/products/
# should return [] as JSON

Step 6 — JWT Authentication

src/pages/LoginPage.jsx:

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import api from '../api/axios';
export default function LoginPage() {
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState('');
const navigate = useNavigate();
const handleSubmit = async e => {
e.preventDefault();
try {
const { data } = await api.post('/api/token/', form);
localStorage.setItem('access', data.access);
localStorage.setItem('refresh', data.refresh);
navigate('/');
} catch {
setError('Invalid credentials');
}
};
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{error && <p style={{ color: 'red' }}>{error}</p>}
<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>
</form>
);
}

src/components/PrivateRoute.jsx:

import { Navigate } from 'react-router-dom';

export default function PrivateRoute({ children }) {
const token = localStorage.getItem('access');
return token ? children : <Navigate to="/login" replace />;
}

// Usage in main.jsx:
// <Route path="/dashboard" element={<PrivateRoute><Dashboard /></PrivateRoute>} />

Step 7 — Build & Deploy

Build React:

cd frontend && npm run build

Serve React from Django (optional):

# config/settings.py
import os
TEMPLATES[0]['DIRS'] = [os.path.join(BASE_DIR, '../frontend/dist')]
STATICFILES_DIRS = [os.path.join(BASE_DIR, '../frontend/dist/assets')]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# config/urls.py — add this at the bottom (must be last)
from django.views.generic import TemplateView
from django.urls import re_path
urlpatterns += [
re_path(r'^(?!api/).*$', TemplateView.as_view(template_name='index.html')),
]

Install production packages:

pip install gunicorn dj-database-url psycopg2-binary whitenoise python-decouple

Procfile (for Railway / Render / Heroku):

web: gunicorn config.wsgi --log-file -

Deploy options:

  • Railway, Render, Fly.io — easiest for beginners
  • VPS + gunicorn + nginx — standard production setup
  • Switch SQLite → PostgreSQL using dj-database-url
  • Store secrets in a .env file using python-decouple

Important: Set DEBUG = False and use a strong SECRET_KEY from environment variables in production. Never commit .env to git.

 

Post a Comment

0 Comments