How to Build a Web App with Django in 7 Steps

How to Build a Web App with Django in 7 Steps

Build a production-ready Django web app in 7 steps—models, auth, admin, deployment. Written for solo founders who ship fast without boilerplate.

Django is a Python web framework that enables you to ship a production-ready web app in days, not months. You write less boilerplate, get an admin panel out of the box, and deploy to platforms that support WSGI without custom infrastructure. Need to validate an idea fast? Here's the thing: Django gets you there.

Svelte code displayed on a dark computer screen in a code editor Photo: Ferenc Almasi on Unsplash

Who this is for: Solo founders and indie hackers who know Python basics and want to build data-driven web apps—like SaaS tools, internal dashboards, or membership sites—without reinventing authentication, routing, or database migrations. Even if you're new to Django but can read Python, you'll follow along.

Step 1: Install Django and Start a New Project

Django requires Python 3.8 or later. Create a virtual environment to isolate dependencies:

python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install django

Start a new Django project:

django-admin startproject myapp
cd myapp
python manage.py runserver

Check http://127.0.0.1:8000/ in your browser. See Django's welcome page? That's your installation confirmed.

Django organizes projects by separating configuration (settings.py, urls.py) from app logic. A "project" holds global settings; "apps" contain features. Running multiple apps inside one project is useful if you're building a SaaS with distinct modules.

Step 2: Create Your First App and Define Models

lines of HTML codes Photo: Florian Olivo on Unsplash

Django apps are reusable modules. Create one:

python manage.py startapp core

Open core/models.py and define a data model. Here's a simple Task model for a to-do app:

from django.db import models

class Task(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    completed = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Register the app in myapp/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'core',  # Add this line
]

Run migrations to create the database table:

python manage.py makemigrations
python manage.py migrate

Django uses an ORM (Object-Relational Mapping) that translates Python classes into SQL. You don't write CREATE TABLE statements. Migrations are versioned, so you can roll back schema changes—critical when you're iterating fast and breaking things.

Step 3: Set Up Django Admin and Test CRUD Operations

Django's admin interface is production-ready. Create a superuser:

python manage.py createsuperuser

Follow the prompts. Then register your model in core/admin.py:

from django.contrib import admin
from .models import Task

@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
    list_display = ('title', 'completed', 'created_at')
    list_filter = ('completed',)
    search_fields = ('title',)

Run the server and visit http://127.0.0.1:8000/admin/. Log in with your superuser credentials. You can now create, edit, and delete tasks without writing a single line of frontend code.

Honestly, Django admin can be enough for internal tools that don't require a custom UI. If you're building a B2B SaaS with power users, the admin panel can validate your idea before investing in React or Vue. For more insights on tools for indie hackers, check out our article on the Best Code Deployment Tools for Indie Hackers in 2026.

Step 4: Build Views and URL Routing

Views handle HTTP requests and return responses. Django supports function-based views (FBVs) and class-based views (CBVs). CBVs reduce repetition for common patterns like list/detail pages.

Create a list view in core/views.py:

from django.views.generic import ListView
from .models import Task

class TaskListView(ListView):
    model = Task
    template_name = 'core/task_list.html'
    context_object_name = 'tasks'
    ordering = ['-created_at']

Wire it up in myapp/urls.py:

from django.contrib import admin
from django.urls import path
from core.views import TaskListView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TaskListView.as_view(), name='task-list'),
]

Create a template directory: core/templates/core/task_list.html:

<!DOCTYPE html>
<html>
<head>
    <title>Tasks</title>
</head>
<body>
    <h1>My Tasks</h1>
    <ul>
    {% for task in tasks %}
        <li>
            {{ task.title }} 
            {% if task.completed %}✓{% endif %}
        </li>
    {% empty %}
        <li>No tasks yet.</li>
    {% endfor %}
    </ul>
</body>
</html>

Visit http://127.0.0.1:8000/. You'll see your task list. While Django's template engine may lack the power of Jinja2, it's sufficient for server-rendered HTML. Building an API-first app? Maybe skip templates and use Django REST Framework.

Step 5: Add User Authentication

Django includes a full authentication system. Enable login/logout views by adding to myapp/urls.py:

from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', TaskListView.as_view(), name='task-list'),
    path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]

Create core/templates/core/login.html:

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">Login</button>
    </form>
</body>
</html>

Update myapp/settings.py to set login redirect:

LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

Protect views with @login_required:

from django.contrib.auth.mixins import LoginRequiredMixin

class TaskListView(LoginRequiredMixin, ListView):
    model = Task
    template_name = 'core/task_list.html'
    context_object_name = 'tasks'
    ordering = ['-created_at']
    login_url = '/login/'

Now unauthenticated users get redirected to login. Django's auth system handles password hashing, session management, and permissions. No need for Auth0 or Firebase Auth for most solo projects.

According to the Django documentation, the built-in User model supports custom fields via AbstractUser or AbstractBaseUser if you need email-only login or multi-tenancy.

Step 6: Connect a Production Database and Configure Settings

Django defaults to SQLite, which is fine for prototyping but risky in production. PostgreSQL is the standard choice for Django apps.

Install the PostgreSQL adapter:

pip install psycopg2-binary

Update myapp/settings.py:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myapp_db',
        'USER': 'myapp_user',
        'PASSWORD': 'secure_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

For production, use environment variables. Install python-decouple:

pip install python-decouple

Update settings:

from decouple import config

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config('DB_NAME'),
        'USER': config('DB_USER'),
        'PASSWORD': config('DB_PASSWORD'),
        'HOST': config('DB_HOST', default='localhost'),
        'PORT': config('DB_PORT', default='5432'),
    }
}

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)

Create a .env file:

SECRET_KEY=your-secret-key-here
DEBUG=True
DB_NAME=myapp_db
DB_USER=myapp_user
DB_PASSWORD=secure_password
DB_HOST=localhost
DB_PORT=5432

Never commit .env to version control. Add it to .gitignore.

Django's settings module is Python code, so you can import libraries, set conditionals, and organize config however you want. Some developers split settings into base.py, dev.py, and prod.py. Worth noting: keeping one file with environment variables can reduce confusion.

Step 7: Deploy to Production

Django apps run on any WSGI server. Popular options for solo founders:

  • Render: Free tier, deploys from GitHub, managed PostgreSQL.
  • Railway: Simple pricing, built-in databases, one-command deploys.
  • DigitalOcean App Platform: $5/month starter tier, scales with usage.

I'll show Render because it's the fastest path to a live URL.

Create a requirements.txt:

pip freeze > requirements.txt

Add gunicorn (WSGI server):

pip install gunicorn
pip freeze > requirements.txt

Create a render.yaml in your project root:

services:
  - type: web
    name: myapp
    env: python
    buildCommand: pip install -r requirements.txt && python manage.py migrate
    startCommand: gunicorn myapp.wsgi:application
    envVars:
      - key: SECRET_KEY
        generateValue: true
      - key: DEBUG
        value: False
      - key: DATABASE_URL
        fromDatabase:
          name: myapp-db
          property: connectionString

databases:
  - name: myapp-db
    databaseName: myapp_db
    user: myapp_user

Push your code to GitHub. Connect your repo to Render. It auto-detects Django, provisions a PostgreSQL instance, and deploys.

Update myapp/settings.py to parse DATABASE_URL:

pip install dj-database-url
import dj_database_url

DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}

Set ALLOWED_HOSTS:

ALLOWED_HOSTS = [config('ALLOWED_HOSTS', default='localhost')]

In Render's dashboard, add ALLOWED_HOSTS=your-app.onrender.com. Redeploy. Your app is live.

According to Render's documentation, their platform handles SSL certificates, automatic scaling, and zero-downtime deploys. No need to mess with Nginx configs or systemd services.

What Nobody Tells You About Django

The ORM is not magic. Django generates SQL for you, but inefficient queries can still happen. Use select_related() and prefetch_related() to avoid N+1 problems. Production apps making 300+ database queries per page view? Yes, that happens when developers assume the ORM optimizes everything.

Migrations break in ways the docs don't warn about. Modifying a model, running makemigrations, then changing your mind and editing the migration file manually? That can mess up Django's migration graph. Always run migrate immediately after makemigrations in development. Test migrations on a staging database first in production.

Django's async support is incomplete. Django 3.1 added ASGI support, but most third-party packages assume WSGI. Need WebSockets or long-running tasks? Use Celery with Redis or RabbitMQ. Running async views in production without load testing? Be prepared—apps might crash due to non-async-safe middleware.

The admin panel has limits. Customizing complex workflows in the admin requires overriding templates and writing admin actions. Need a polished UI for non-technical users? Build a real frontend. The admin is best suited for internal tools and power users who can tolerate less polished interfaces.

Django REST Framework is a separate learning curve. DRF is the standard for building APIs, but it introduces serializers, viewsets, and router concepts. New to Django? Build a simple CRUD app with templates first. Adding DRF without understanding Django's ORM or request/response cycle might confuse you.

Common Mistakes

Skipping environment variables in development. Developers hardcode DEBUG=True and SECRET_KEY in settings.py, then forget to change them before deploying. Use .env files from day one. Five minutes of setup prevents production disasters.

Ignoring database indexes. Django doesn't auto-index foreign keys on all databases. If filtering by a field frequently—like Task.objects.filter(user=request.user)—add db_index=True:

user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True)

Not using get_object_or_404. Beginners write:

task = Task.objects.get(pk=task_id)

This raises DoesNotExist if the object is missing, which crashes the app. Instead, use:

from django.shortcuts import get_object_or_404

task = get_object_or_404(Task, pk=task_id)

It returns a 404 response automatically.

Mixing business logic in views. Views should handle HTTP logic only—validation, redirects, context. Move calculations, external API calls, and data processing into model methods or service modules. Fat models, thin views.

FAQ

Can I use Django for a single-page app with React or Vue?

Yes. Django becomes your API backend. Install Django REST Framework, build serializers for your models, and expose endpoints. Your frontend consumes JSON. Honestly, this is how most projects ship—Django for auth and database logic, React for the UI.

How do I handle file uploads in Django?

Use FileField or ImageField in your model:

avatar = models.ImageField(upload_to='avatars/')

Django saves files to MEDIA_ROOT by default. In production, use S3 or Cloudflare R2 with django-storages. Serving files from your app server is slow and wastes memory.

Is Django slower than Flask or FastAPI?

For most solo projects, the difference doesn't matter. Django includes more batteries—ORM, admin, auth—so there's some overhead. Building a high-traffic API with sub-10ms response times? FastAPI is faster. Building a SaaS with forms, user accounts, and a database? Django ships quicker because you write less code.

How do I schedule background tasks in Django?

Use Celery with Redis. Install Celery:

pip install celery redis

Configure it in myapp/celery.py, then define tasks:

from celery import shared_task

@shared_task
def send_email(user_id):
    # Email logic here
    pass

Call it with .delay():

send_email.delay(user.id)

This runs the task asynchronously. Don't run long tasks in Django views—users wait for the response, and your app hangs.

Conclusion

Django trades flexibility for speed. You get authentication, migrations, and an admin panel without configuration. Validating a SaaS idea and need to ship in days? Django is the right tool.

Your next step: clone the starter repo or scaffold a new project, define one model, and deploy it to Render or Railway tonight. Stop reading tutorials and ship something live. Learn more in production than in 50 articles. For additional resources, consider exploring the Best Online Payment Solutions for Indie Hackers in 2026.


Editorial note: This article was produced with AI assistance and reviewed by Javier Valencia. Verified facts are distinguished from editorial opinion throughout the text. External sources linked are independent of NewsTide.

Sources

  1. Svelte code displayed on a dark computer screen in a code editor
  2. Ferenc Almasi
  3. lines of HTML codes
  4. Florian Olivo
  5. Django documentation

🇪🇸 Also available in Spanish: Leer en español

𝕏in