Organizing Django Apps Inside an apps/ Directory

Instead of having all your apps clutter up your project's directory, organizing them in a dedicated 'apps/' directory makes the project more structured.

Starting Clean vs Refactoring an Existing Project As soon as Django projects grow past just one or two apps, many teams move all first-party apps into a dedicated directory: apps/

Instead of:

project/
    blog/
    users/
    billing/
    orders/

they use:

project/
    apps/
        blog/
        users/
        billing/
        orders/

This is a common enterprise Django pattern because it creates a clear separation between: - project configuration - business applications - shared infrastructure - deployment/runtime files

This article explains: 1. How to start a project with an apps/ directory 2. How to refactor an existing Django project safely

Why Use an apps/ Directory?

Default Django structure works well for small projects. But as complexity grows, the root folder often becomes crowded:

project/
    blog/
    users/
    orders/
    billing/
    settings.py
    urls.py
    utils.py
    celery.py
    scripts/

This mixes business domains with framework configuration.

Using an apps/ package gives structure:

project/
    config/
    apps/
    common/

Way easier to navigate.

What Belongs in apps/

Only first-party Django apps that represent business capabilities.

Good examples: - apps/users - apps/blog - apps/orders - apps/billing - apps/projects - apps/support

Bad examples: - apps/utils - apps/helpers - apps/common_functions - apps/random_stuff

Those belong in shared/internal libraries, not Django apps.

project/
│
├── manage.py
│
├── config/
│   ├── settings/
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
│
├── apps/
│   ├── users/
│   ├── blog/
│   ├── billing/
│   ├── orders/
│   └── projects/
│
├── common/
├── requirements/
└── scripts/

We have a full article describing Django Enterprise Structure -> Django Enterprise-scale projects

Part 1 — Using apps/ at Project Start

This is the easiest and cleanest moment to do it.

Step 1: Create Django Project

bash

1
django-admin startproject config .

Now you have:

config/
    settings.py
    urls.py

Step 2: Create apps/

apps/
    __init__.py

Important: apps/init.py makes it a Python package.

Step 3: Create Apps Inside It

bash

1
2
3
python manage.py startapp blog apps/blog
python manage.py startapp users apps/users
python manage.py startapp billing apps/billing

Step 4: Register Apps

In settings:

python

1
2
3
4
5
6
7
8
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",

    "apps.blog",
    "apps.users",
    "apps.billing",
]

Step 5: Fix Each apps.py

Example:

python

1
2
3
4
5
6
from django.apps import AppConfig

class BlogConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "apps.blog"
    label = "blog"
Why Add label

Use:

python

1
label = "blog"

This preserves clean references:

python

1
2
3
4
5
ForeignKey("blog.Post")

# instead of:

ForeignKey("apps.blog.Post")

Highly recommended.

We have a full guide on apps.py -> Django apps.py

Why Starting This Way Is Best

  • No broken imports.
  • No migration surprises.
  • No refactor cost later.

Part 2 — Refactoring an Existing Project into apps/

This is very common.

You have:

project/
    blog/
    users/
    billing/

Now you want:

project/
    apps/
        blog/
        users/
        billing/

This can be done safely.

Important Warning: Do not casually rename apps without understanding migrations. Django stores app labels in migrations and database metadata. Move carefully.

Step 1: Create apps/

apps/
    __init__.py

Step 2: Move Folders

From:

blog/
users/
billing/

To:

apps/blog/
apps/users/
apps/billing/

Step 3: Update apps.py

Before:

python

1
2
class BlogConfig(AppConfig):
    name = "blog"

After:

python

1
2
3
class BlogConfig(AppConfig):
    name = "apps.blog"
    label = "blog"
Why Keep Same Label?

This is critical.

If old app label was 'blog'

keeping:

python

1
label = "blog"

helps preserve: - migration references - DB table names - ForeignKey strings - admin internals

Without it, Django may think it is a new app.

Step 4: Update INSTALLED_APPS From: "blog"

To: "apps.blog"

Step 5: Update Python Imports

Old:

python

1
2
from blog.models import Post
from users.forms import ProfileForm

New:

python

1
2
from apps.blog.models import Post
from apps.users.forms import ProfileForm

This can be the most tedious step since imports can be scattered. Search across the project.

Step 6: Test Startup

bash

1
2
python manage.py check
python manage.py runserver

Step 7: Test Migrations

bash

1
2
python manage.py makemigrations
python manage.py migrate

If labels were preserved correctly, usually no issues.

Migration Safety Notes If your original app was:

python

1
name = "blog"

and you change to:

python

1
2
name = "apps.blog"
label = "blog"

Django usually keeps migration identity stable. This is the safest pattern.

Common Problems During Refactor

1. Forgot init.py

Rquired: apps/init.py

2. Wrong name

Wrong:

python

1
name = "blog"

after move.

Correct:

python

1
name = "apps.blog"

3. Missing label

Can break migration identity.

Use:

python

1
label = "blog"

4. Old Imports Still Exist

python

1
from blog.models import ...

5. Circular Imports Surface

Refactors often expose hidden bad imports.

Use:

python

1
ForeignKey("blog.Post")

or service-layer imports.

Example Refactor Result:

apps/
    blog/
        apps.py
        models.py
        views.py
        migrations/

    users/
    billing/

Should Third-Party Apps Go Inside apps/?

Short answer -> No.

Keep in INSTALLED_APPS normally: "django.contrib.admin" "rest_framework" "channels"

Only your own apps go in /apps.

What About Shared Utilities?

Use another package: - common/ - core/ - shared/ - lib/

Example: - common/email.py - common/storage.py - common/permissions.py

Not Django apps.

Complete Django /apps Refactor Checklist

Every Place Old App Paths Can Break After Moving Apps into an apps/ Directory

When you move apps from:

blog/
users/
billing/

to:

apps/blog/
apps/users/
apps/billing/

you must update every dotted Python path that references the old location.

Example:

python

1
2
3
4
blog.models
blog.views
blog.routing
blog.middleware

must become:

python

1
2
3
4
apps.blog.models
apps.blog.views
apps.blog.routing
apps.blog.middleware

Missing even one causes runtime errors.

Priority Order

Check in this order: 1. apps.py 2. INSTALLED_APPS 3. imports 4. middleware 5. urls 6. Channels / ASGI 7. signals 8. Celery 9. template tags 10. management commands 11. tests 12. migrations 13. admin 14. external deploy config

Red Flags After Refactor

If you see: No module named 'blog'

or: Cannot import 'blog'

or: LookupError: No installed app with label ...

You missed one of the locations above.

Validation Commands after Refactor or possible fixes:

Run:

bash

1
2
3
4
python manage.py check
python manage.py makemigrations --check
python manage.py migrate
python manage.py runserver

Then test: - admin - websocket pages - celery tasks - custom commands - templates - login/logout

If your Django project has 3+ internal apps or 2+ developers, use an apps/ directory early. It becomes increasingly valuable over time.

Join the Newsletter

Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.

Get updates when new guides, learning paths, cheat sheets, and field notes are published.

No spam. Unsubscribe anytime.



There is no third-party involved so don't worry - we won't share your details with anyone.