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/
├── 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 backendcd 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 modelsfrom django.contrib.auth.models import Userclass 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 migratepython manage.py createsuperuser
Step 3 — REST API (Serializers, Views, URLs)
products/serializers.py:
from rest_framework import serializersfrom .models import Productclass ProductSerializer(serializers.ModelSerializer):owner_username = serializers.ReadOnlyField(source='owner.username')class Meta:model = Productfields = ['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'].userreturn super().create(validated_data)
products/views.py:
from rest_framework import viewsets, permissionsfrom .models import Productfrom .serializers import ProductSerializerclass IsOwnerOrReadOnly(permissions.BasePermission):def has_object_permission(self, request, view, obj):if request.method in permissions.SAFE_METHODS:return Truereturn obj.owner == request.userclass ProductViewSet(viewsets.ModelViewSet):queryset = Product.objects.all()serializer_class = ProductSerializerpermission_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, includefrom rest_framework.routers import DefaultRouterfrom .views import ProductViewSetrouter = DefaultRouter()router.register(r'products', ProductViewSet, basename='product')urlpatterns = [path('', include(router.urls)),]
config/urls.py:
from django.contrib import adminfrom django.urls import path, includefrom rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshViewurlpatterns = [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 allPOST /api/products/— createGET /api/products/1/— detailPUT /api/products/1/— updateDELETE /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 requestapi.interceptors.request.use(config => {const token = localStorage.getItem('access');if (token) config.headers.Authorization = `Bearer ${token}`;return config;});// Auto-refresh token on 401 errorapi.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><inputplaceholder="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 1cd backend && python manage.py runserver# Terminal 2cd 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.pyimport osTEMPLATES[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 TemplateViewfrom django.urls import re_pathurlpatterns += [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
.envfile usingpython-decouple
Important: Set
DEBUG = Falseand use a strongSECRET_KEYfrom environment variables in production. Never commit.envto git.
0 Comments