Django Basics #2: Project Setup — uv + django-admin startproject

6 min read

In #1 What is Django you nailed down where Django fits — full-stack. This post takes a single breath from empty directory → a working first page. The tool is the same uv you used in Modern Python FastAPI #1, kept consistent.

Prerequisites — Python and uv #

Django 5.x requires Python 3.10 or higher. This series runs on 3.13. Skip ahead if you already have uv; otherwise:

Install uv (macOS / Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
Install uv (Windows)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

Creating the project #

Make a uv virtual environment in an empty directory and install Django.

Project directory
mkdir myblog && cd myblog
uv init --python 3.13
uv add django

What uv init creates:

  • pyproject.toml — project metadata and dependencies
  • .python-version — the Python version to use
  • .venv/ — virtual environment (auto-created, excluded from git)

After uv add django lands, check the version:

Version check
uv run django-admin --version
# something like 5.1.x

django-admin startproject — generate the project skeleton #

Django has two CLI entry points — django-admin (before the project exists) and manage.py (after). The first skeleton is built with django-admin.

Generate the project skeleton
uv run django-admin startproject config .

The trailing . matters. Without it, you end up nested like config/config/.... Adding . puts it directly into the current directory.

Why name it config? Names like mysite or myproject carry no meaning. It’s better to expose the essence: a collection of settings. It’s a community convention.

The structure that gets created #

myblog/
myblog/
├── .python-version
├── .venv/
├── pyproject.toml
├── uv.lock
├── manage.py             # Django CLI entry point (every command from here on)
└── config/
    ├── __init__.py
    ├── settings.py       # all settings
    ├── urls.py           # top-level URL routing
    ├── asgi.py           # ASGI entry point (async, Channels)
    └── wsgi.py           # WSGI entry point (traditional sync deployment)

What each file does:

  • manage.py — the entry for every command: runserver, migrate, createsuperuser, etc.
  • settings.py — DB, app list, middleware, secret key, and so on
  • urls.py — top-level URL mapping like /admin/, /posts/
  • asgi.py / wsgi.py — server entry points. You don’t touch these during development

First run — runserver #

Run the dev server
uv run python manage.py runserver

It boots on the default port 8000. Open http://127.0.0.1:8000 in a browser and you’ll see a rocket page that reads “The install worked successfully! Congratulations!”

On the first run, you may see a red warning in the console like You have N unapplied migrations. The next step takes care of it.

Change the port: uv run python manage.py runserver 8080 Expose to the LAN: uv run python manage.py runserver 0.0.0.0:8000

First migration — tables for the built-in apps #

Django activates internal apps as soon as it’s installed (auth, sessions, admin, contenttypes, messages, staticfiles). Their tables (auth_user, django_session, etc.) need to be created in the DB.

First migration
uv run python manage.py migrate

The default DB is SQLite, and a db.sqlite3 file is auto-created at the project root. Switching to PostgreSQL or MySQL is covered in Intermediate #6.

Once migration finishes, the console fills with logs like Applying auth.0001_initial... OK, and the red warning goes away.

First app — startapp blog #

In Django, an app is a reusable unit of functionality. Multiple apps inside one project — for example, blog, accounts, payments.

Create the blog app
uv run python manage.py startapp blog

The structure that’s generated:

blog/
blog/
├── __init__.py
├── admin.py          # Admin registration (#7)
├── apps.py           # app metadata
├── migrations/
│   └── __init__.py
├── models.py         # model definitions (#3)
├── tests.py          # tests
└── views.py          # views (#4)

(The templates / static / urls directories are not auto-created — you add them yourself, as covered in #4 and #5.)

Register in INSTALLED_APPS #

For Django to notice the app you made, register it in settings.py.

config/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "blog",                         # added
]

A single "blog" line at the end of the list. More precisely, you can write "blog.apps.BlogConfig" (the class in apps.py), but the short "blog" form works.

settings.py core settings #

Items worth touching first:

config/settings.py — excerpt
# Security — never expose externally
SECRET_KEY = "django-insecure-..."

# Debug — must be False in production
DEBUG = True

# Which hosts to allow when DEBUG=False
ALLOWED_HOSTS = []

# Installed apps
INSTALLED_APPS = [...]

# Middleware (request/response pipeline)
MIDDLEWARE = [...]

# Top-level URL module
ROOT_URLCONF = "config.urls"

# Template settings
TEMPLATES = [...]

# Database — default SQLite
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

# Internationalization
LANGUAGE_CODE = "ko-kr"      # default en-us → Korean
TIME_ZONE = "Asia/Seoul"     # UTC → Seoul
USE_I18N = True
USE_TZ = True                # store as UTC in DB, convert when displaying

# Static files (#5)
STATIC_URL = "static/"

DEBUG=True is for development only. Push it to production as-is and your error pages expose code and environment variables. Production deployment is in Advanced #7.

Separating environment variables — django-environ #

Just like you used pydantic-settings in FastAPI #1, separating secrets into environment variables is the standard for Django too.

Install
uv add django-environ
top of config/settings.py
import environ
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env(
    DEBUG=(bool, False),
)
environ.Env.read_env(BASE_DIR / ".env")

SECRET_KEY = env("SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["127.0.0.1", "localhost"])
.env (excluded from git)
SECRET_KEY=django-insecure-replace-me-in-production
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost

Adding .env to .gitignore is a must.

A first view — the starting point of the series #

The smallest possible view in blog/views.py:

blog/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, blog!")

Create blog/urls.py (Django doesn’t auto-create it, remember).

blog/urls.py
from django.urls import path

from . import views

app_name = "blog"

urlpatterns = [
    path("", views.index, name="index"),
]

Include the blog URLs from the top-level config/urls.py.

config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]

Restart the server and open http://127.0.0.1:8000/blog/Hello, blog! appears.

More on URL / view patterns in #4.

Frequently used manage.py commands #

Commands worth memorizing:

CommandPurpose
runserverRun the development server
startapp <name>Create a new app
makemigrationsModel changes → migration files
migrateMigrations → apply to DB
createsuperuserCreate an admin superuser (#7)
shellA Python REPL with Django context
dbshellDirect DB shell
collectstaticCollect static files (production, #5)
checkSettings/model integrity check
testRun tests (Intermediate #7)

uv run python manage.py shell is especially handy. Import models and play with the ORM directly.

Recap #

What this post nailed down:

  • uv init + uv add django for dependency separation
  • django-admin startproject config . — project skeleton
  • manage.py — every command from here on
  • runserver — dev server with auto-reload
  • migrate — create tables for the built-in apps
  • startapp blog — create the first app, register in INSTALLED_APPS
  • Core settings.py items — DEBUG, INSTALLED_APPS, DATABASES, LANGUAGE_CODE, TIME_ZONE
  • django-environ to separate secrets, .env excluded from git
  • A first view — views.py + per-app urls.py + top-level include

In the next post (#3 Models and ORM basics), you’ll define a Post model in blog/models.py, walk through the makemigrationsmigrate flow, and play with Django ORM’s QuerySet for the first time.

X