Building a Solid Foundation: Project Structure for Web Applications
Welcome to our session on Python project structure for web applications! Today, we'll explore one of the most fundamental but often overlooked aspects of successful web development: how to organize your code and files effectively.
As we prepare to dive into web frameworks in the coming weeks, understanding how to structure your Python web applications becomes crucial. The organization of your project can greatly impact its maintainability, scalability, and the ease with which new team members can understand and contribute to it.
Why Project Structure Matters
Analogy: Think of your project structure as the blueprint for a building. Without a clear, well-thought-out blueprint, even the finest materials and skilled workers will struggle to create a functional, durable structure. Similarly, without a good project organization, even excellent code and talented developers will struggle to build a maintainable application.
The benefits of a well-structured project include:
- Improved maintainability: Everyone knows where to find specific components
- Better collaboration: Teams can work on different parts without conflicts
- Easier onboarding: New developers can quickly understand the architecture
- Simpler testing: Modular components are easier to test in isolation
- Streamlined deployment: Clear separation of configuration and code
- Enhanced scalability: Well-organized code can grow without becoming chaotic
Real-world Impact: Major tech companies, including those building Python web applications like Instagram, Dropbox, and Pinterest, have dedicated significant effort to creating optimal project structures that allow hundreds of developers to work effectively on the same codebase.
Common Python Project Structure Patterns
Let's explore the most common patterns for structuring Python web applications:
The Simple Flask Application
For small, straightforward applications, a simple structure might look like this:
my_flask_app/
├── app.py # Application entry point
├── config.py # Configuration settings
├── requirements.txt # Dependencies
├── static/ # Static files (CSS, JS, images)
│ ├── css/
│ ├── js/
│ └── images/
├── templates/ # HTML templates
│ ├── base.html
│ ├── index.html
│ └── other_pages/
└── venv/ # Virtual environment (not in version control)
When to use: Simple applications, prototypes, learning projects, or microservices that perform a single function.
Limitations: Quickly becomes unwieldy as the application grows. Logic and presentation become mixed, making maintenance challenging.
The Package-Based Structure
As applications grow, organizing code into a proper Python package provides more structure:
my_web_app/
├── myapp/ # Main package
│ ├── __init__.py # Package initialization
│ ├── models.py # Data models
│ ├── routes.py # URL route definitions
│ ├── forms.py # Form definitions
│ ├── static/ # Static files
│ │ ├── css/
│ │ ├── js/
│ │ └── images/
│ └── templates/ # Templates
│ ├── base.html
│ └── pages/
├── config.py # Configuration
├── requirements.txt # Dependencies
├── run.py # Application entry point
└── README.md # Documentation
When to use: Medium-sized applications where a simple structure is becoming insufficient, but full modularity isn't yet necessary.
Advantages: Clear separation of code into logical files, better import mechanisms as a package.
The Modular Blueprint Structure
For larger applications, breaking functionality into modules or "blueprints" (in Flask terminology) provides better organization:
my_web_app/
├── myapp/
│ ├── __init__.py # App initialization, blueprint registration
│ ├── extensions.py # Flask extensions initialization
│ ├── models/ # Database models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── product.py
│ ├── auth/ # Authentication module
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ ├── models.py
│ │ ├── forms.py
│ │ └── templates/
│ │ └── auth/
│ ├── admin/ # Admin panel module
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── templates/
│ │ └── admin/
│ ├── public/ # Public-facing module
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── templates/
│ │ └── public/
│ ├── static/
│ └── templates/
│ └── base.html
├── migrations/ # Database migrations
├── tests/ # Test suite
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_auth.py
│ └── test_models.py
├── config.py
├── requirements.txt
├── requirements-dev.txt
├── run.py
└── README.md
When to use: Larger applications with distinct, separable features or when multiple developers work on different parts of the application.
Advantages: Logical separation of concerns, ability to work on modules independently.
The Domain-Driven Design Structure
For complex applications, organizing by business domain rather than technical function can be more effective:
my_web_app/
├── myapp/
│ ├── __init__.py
│ ├── extensions.py
│ ├── core/ # Shared core functionality
│ │ ├── __init__.py
│ │ ├── database.py
│ │ ├── security.py
│ │ └── utils.py
│ ├── users/ # Users domain
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── services.py # Business logic
│ │ ├── schemas.py # Serialization/validation
│ │ ├── views.py # API/route handlers
│ │ └── tests/ # Domain-specific tests
│ ├── products/ # Products domain
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── services.py
│ │ ├── schemas.py
│ │ ├── views.py
│ │ └── tests/
│ ├── orders/ # Orders domain
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── services.py
│ │ ├── schemas.py
│ │ ├── views.py
│ │ └── tests/
│ ├── static/
│ └── templates/
├── migrations/
├── tests/ # Integration tests
├── config.py
├── requirements/ # Split requirements
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── wsgi.py # WSGI entry point
└── README.md
When to use: Complex enterprise applications with clear business domains and complex business logic.
Advantages: Focuses on business domains rather than technical layers, making it easier to understand the business purpose of code.
The Hexagonal Architecture
For applications with complex business logic and multiple interfaces (web, API, CLI), a hexagonal (ports and adapters) structure can be effective:
my_web_app/
├── myapp/
│ ├── __init__.py
│ ├── domain/ # Business logic, domain models, and rules
│ │ ├── __init__.py
│ │ ├── user/
│ │ │ ├── __init__.py
│ │ │ ├── entities.py # Domain models
│ │ │ ├── repositories.py # Abstract repositories
│ │ │ ├── services.py # Business logic
│ │ │ └── exceptions.py
│ │ └── order/
│ │ ├── __init__.py
│ │ ├── entities.py
│ │ ├── repositories.py
│ │ ├── services.py
│ │ └── exceptions.py
│ ├── interfaces/ # User interfaces (ports)
│ │ ├── __init__.py
│ │ ├── web/ # Web interface
│ │ │ ├── __init__.py
│ │ │ ├── routes.py
│ │ │ ├── forms.py
│ │ │ └── templates/
│ │ └── api/ # API interface
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── schemas.py
│ └── infrastructure/ # Technical implementations (adapters)
│ ├── __init__.py
│ ├── database.py
│ ├── repositories/ # Concrete repositories
│ │ ├── __init__.py
│ │ ├── sql_user_repository.py
│ │ └── sql_order_repository.py
│ ├── security.py
│ └── external_services.py # 3rd party services
├── config/ # Configuration
│ ├── __init__.py
│ ├── development.py
│ ├── production.py
│ └── testing.py
├── tests/
├── requirements/
├── wsgi.py
└── README.md
When to use: Complex applications with rich domain logic and multiple interfaces/entry points.
Advantages: Strong separation of concerns, business logic isolated from technical details, adaptable to changing requirements.
Metaphor: Think of this architecture as a hexagon with your core business logic in the center, protected from the outside world. All interactions with external systems (databases, web, API) go through well-defined ports (interfaces) that are implemented by adapters. This keeps your business logic pure and free from external dependencies.
Shared Components Across Structures
Regardless of the structure you choose, certain components are commonly found in Python web applications:
Entry Points
Files that start the application:
# run.py (for development)
from myapp import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
# wsgi.py (for production)
from myapp import create_app
app = create_app()
# Used by WSGI servers like Gunicorn
# gunicorn wsgi:app
Application Factory
A function that creates and configures the application:
# myapp/__init__.py
from flask import Flask
def create_app(config_object='config.ProductionConfig'):
"""Application factory function."""
app = Flask(__name__)
app.config.from_object(config_object)
# Initialize extensions
from myapp.extensions import db, migrate
db.init_app(app)
migrate.init_app(app, db)
# Register blueprints
from myapp.auth.routes import auth_bp
from myapp.public.routes import public_bp
app.register_blueprint(auth_bp)
app.register_blueprint(public_bp)
return app
Advantages: Allows creating multiple instances with different configurations (e.g., testing vs. production), easier extension initialization.
Configuration
Separating configuration from code:
# config.py
import os
class Config:
"""Base configuration."""
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-please-change')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
"""Development configuration."""
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'
class ProductionConfig(Config):
"""Production configuration."""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
class TestingConfig(Config):
"""Testing configuration."""
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
Best Practice: Use environment variables for sensitive values and production settings.
Models
Representing database entities:
# myapp/models/user.py
from myapp.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
"""User model for authentication."""
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
Views/Routes
Handling HTTP requests:
# myapp/auth/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from myapp.auth.forms import LoginForm, RegisterForm
from myapp.models.user import User
from myapp.extensions import db
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user)
return redirect(url_for('main.index'))
flash('Invalid username or password')
return render_template('auth/login.html', form=form)
Templates
Separating presentation from logic:
<!-- myapp/templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Web App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
</head>
<body>
<header>
<nav>
<ul>
<li><a href="{{ url_for('public.index') }}">Home</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
{% endif %}
</ul>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ now.year }} My Web App</p>
</footer>
</body>
</html>
Static Files
Organizing CSS, JavaScript, and images:
static/
├── css/
│ ├── main.css # Main stylesheets
│ ├── components/ # Component-specific styles
│ └── vendor/ # Third-party CSS
├── js/
│ ├── main.js # Main JavaScript
│ ├── components/ # Component-specific scripts
│ └── vendor/ # Third-party scripts
└── images/
├── logo.png
├── icons/
└── backgrounds/
Best Practice: Consider using a frontend build system for complex static assets.
Tests
Organizing tests to mirror the application structure:
tests/
├── conftest.py # Shared fixtures
├── test_config.py # Configuration tests
├── functional/ # End-to-end tests
│ ├── test_auth.py
│ └── test_public.py
└── unit/ # Unit tests
├── test_models.py
└── test_forms.py
# tests/conftest.py
import pytest
from myapp import create_app
from myapp.extensions import db as _db
@pytest.fixture
def app():
"""Create and configure a Flask app for testing."""
app = create_app('config.TestingConfig')
with app.app_context():
_db.create_all()
yield app
_db.session.remove()
_db.drop_all()
@pytest.fixture
def client(app):
"""A test client for the app."""
return app.test_client()
@pytest.fixture
def db(app):
"""Database for testing."""
return _db
Framework-Specific Structure Considerations
Different frameworks have their own conventions and best practices for project structure:
Flask
- Flexibility: Flask is minimal and allows many different structures
- Blueprints: Key to modularizing Flask applications
- Application Factory: Recommended for anything beyond trivial apps
- Extensions: Often initialized in a separate module
Recommended Structure: The Modular Blueprint structure works well for most Flask applications.
Django
Django follows a more opinionated structure:
my_django_project/
├── manage.py # Command-line utility
├── myproject/ # Project package
│ ├── __init__.py
│ ├── settings/ # Split settings
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py # Main URL configuration
│ ├── asgi.py # ASGI configuration
│ └── wsgi.py # WSGI configuration
├── apps/ # Custom application modules
│ ├── users/ # Users app
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── migrations/
│ │ ├── models.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ └── views.py
│ └── products/ # Products app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations/
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── templates/ # Global templates
│ └── base.html
├── static/ # Global static files
├── media/ # User-uploaded files
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
└── README.md
Key Differences:
- Django encourages "apps" as the unit of modularity
- Each app includes its own models, views, templates, and static files
- Project settings are centralized
- Admin interface is a built-in feature with configuration
FastAPI
FastAPI works well with modern, modular structures:
my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI application creation and configuration
│ ├── core/ # Core functionality
│ │ ├── __init__.py
│ │ ├── config.py # Settings using Pydantic
│ │ ├── security.py # Authentication and permissions
│ │ └── database.py # Database connection
│ ├── api/ # API endpoints
│ │ ├── __init__.py
│ │ ├── dependencies.py # Shared dependencies
│ │ ├── v1/ # API version 1
│ │ │ ├── __init__.py
│ │ │ ├── endpoints/ # Grouped by resource
│ │ │ │ ├── users.py
│ │ │ │ └── items.py
│ │ │ └── router.py # Combined router
│ ├── models/ # SQLAlchemy models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── schemas/ # Pydantic schemas/models
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ ├── crud/ # CRUD operations
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── item.py
│ └── services/ # Business logic
│ ├── __init__.py
│ └── user_service.py
├── tests/
├── alembic/ # Migrations
├── .env # Environment variables
├── requirements.txt
└── README.md
Key Features:
- API-first design with endpoints organized by resource
- Clear separation between data models (SQLAlchemy) and API schemas (Pydantic)
- Often includes API versioning from the start
- Dependency injection used heavily for shared components
Real-world Example: The FastAPI official project generator (fastapi-users) follows a similar structure to make it easy to get started with best practices.
Making Structure Decisions
Choosing the right structure depends on various factors:
Factors to Consider
- Team Size: Larger teams need more explicit structure
- Project Lifespan: Long-lived projects benefit from clearer organization
- Project Complexity: More features require more modularization
- Team Experience: Match complexity to team expertise
- Framework Conventions: Follow established patterns when possible
Decision Framework
Use this simplified framework to decide on structure:
| Project Type | Recommended Structure |
|---|---|
| Simple web app, single developer | Simple Flask Application |
| Medium app, small team | Package-Based Structure |
| Complex app, multiple developers | Modular Blueprint Structure |
| Enterprise app, complex domains | Domain-Driven Design or Hexagonal Architecture |
Metaphor: Think of project structure like clothing. A simple t-shirt (simple structure) works for a quick errand, but you'll need a specialized outfit (complex structure) for extreme mountain climbing. Choose the structure that fits your project's complexity and needs.
Evolution, Not Revolution
It's important to recognize that project structures evolve:
- Start with simpler structures and refactor as needed
- Identify pain points in your current structure
- Gradually migrate toward better organization
- Use code reviews to maintain structural integrity
Real-world Example: Flask itself started as a simple application and evolved into a blueprint structure as its complexity grew. Many successful web applications follow this pattern of progressive organization.
Best Practices for Project Structure
Regardless of the specific structure you choose, follow these principles:
Separation of Concerns
Keep different types of functionality in separate modules:
- Presentation: Templates, forms, views/routes
- Business Logic: Services, domain models
- Data Access: Database models, repositories
- Configuration: Settings, environment variables
Analogy: Like keeping ingredients, cooking utensils, and dining plates in different kitchen cabinets - it makes everything easier to find and use.
Don't Repeat Yourself (DRY)
Organize code to minimize duplication:
- Extract common functionality into utility functions
- Use inheritance and composition for shared behaviors
- Create centralized configuration
# myapp/core/utils.py
def paginate_query(query, page, per_page):
"""Paginate a SQLAlchemy query."""
return query.paginate(page=page, per_page=per_page, error_out=False)
# In multiple views
from myapp.core.utils import paginate_query
# Later in a view
users = paginate_query(User.query, page, 20)
Consistency is Key
- Use consistent naming conventions
- Follow patterns throughout the codebase
- Document structural decisions
- Create templates for new components
Naming Conventions Example:
- Modules: Singular, lowercase with underscores (
user.py,product_category.py) - Classes: Singular, PascalCase (
User,ProductCategory) - Blueprints: Named with purpose and suffix (
auth_bp,admin_bp) - Packages: Plural, lowercase with underscores (
models,views,api_endpoints)
Configuration Management
- Separate configuration from code
- Use environment variables for sensitive values
- Create different configs for different environments
- Never commit sensitive keys to version control
# .env (not in version control)
SECRET_KEY=your-very-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
# Using the config
import os
from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env
SECRET_KEY = os.environ.get('SECRET_KEY')
DATABASE_URL = os.environ.get('DATABASE_URL')
Configuration vs. Code: What Goes Where
Clear guidelines on what should be configuration vs. code:
| Configuration (config.py, .env) | Code (Python modules) |
|---|---|
| Database connection strings | Database model definitions |
| API keys and secrets | API client implementation |
| Feature flags | Feature implementation |
| Environment-specific settings | Application logic |
| Logging levels and destinations | Logging setup code |
Project Structure in a Team Environment
When working with teams, project structure becomes even more important:
Documentation
Document your structure for team members:
# README.md section on project structure
## Project Structure
This project follows a modular blueprint structure:
- `myapp/`: Main package
- `__init__.py`: Application factory
- `auth/`: Authentication module
- `admin/`: Admin panel module
- `public/`: Public-facing pages
- `models/`: Database models
- `static/`: Static assets
- `templates/`: HTML templates
- `migrations/`: Database migrations
- `tests/`: Test suite
- `config.py`: Configuration classes
### Module Structure
Each module (auth, admin, public) follows this structure:
- `__init__.py`: Blueprint creation
- `routes.py`: View functions and routes
- `forms.py`: WTForms definitions
- `templates/`: Module-specific templates
Onboarding
Make it easy for new team members to understand the structure:
- Include structure documentation in onboarding materials
- Create a visual diagram of the architecture
- Explain the reasoning behind structural decisions
- Highlight where common tasks should be implemented
Contribution Guidelines
Set clear rules for maintaining the structure:
# CONTRIBUTING.md section
## Code Organization
- New features should be implemented as blueprints in their own module
- Keep models in the `models/` directory, one model per file
- Place shared utilities in `core/utils.py`
- Add tests for all new functionality
- Follow existing naming conventions
- Update documentation when changing structure
Code Reviews
Use code reviews to enforce structural consistency:
- Check that new code follows the established structure
- Look for code that belongs in a different module
- Identify opportunities for refactoring to improve organization
- Ensure documentation is updated
Practical Example: Refactoring for Better Structure
Let's look at a practical example of refactoring a simple Flask application into a better structure.
Starting Point: Simple Flask App
# app.py - Everything in one file
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
return redirect(url_for('index'))
flash('Invalid username or password')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Username already exists')
else:
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registration successful')
return redirect(url_for('login'))
return render_template('register.html')
if __name__ == '__main__':
db.create_all()
app.run(debug=True)
Refactored Structure: Package-Based
Reorganize into a proper package structure:
my_flask_app/
├── myapp/
│ ├── __init__.py
│ ├── models.py
│ ├── auth.py
│ ├── routes.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── index.html
│ │ ├── login.html
│ │ └── register.html
│ └── static/
├── config.py
├── requirements.txt
└── run.py
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
# myapp/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import Config
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
login_manager.init_app(app)
from myapp import models
from myapp.auth import auth_bp
app.register_blueprint(auth_bp)
from myapp.routes import main_bp
app.register_blueprint(main_bp)
return app
# myapp/models.py
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from myapp import db, login_manager
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# myapp/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required
from myapp import db
from myapp.models import User
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
login_user(user)
return redirect(url_for('main.index'))
flash('Invalid username or password')
return render_template('login.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('main.index'))
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if User.query.filter_by(username=username).first():
flash('Username already exists')
else:
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registration successful')
return redirect(url_for('auth.login'))
return render_template('register.html')
# myapp/routes.py
from flask import Blueprint, render_template
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
return render_template('index.html')
# run.py
from myapp import create_app, db
app = create_app()
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Key Improvements
- Separation of Concerns: Models, views, and configuration are now separate
- Extensibility: Easy to add new blueprints for additional features
- Application Factory: Allows for different configurations and testing
- Blueprint Organization: Routes are grouped by functionality
- Configuration Management: Environment variables and config classes
Result: The application is now more maintainable, testable, and easier to extend with new features.
Further Evolution: Moving to Blueprints
As the application grows, you might refactor further to a modular blueprint structure:
my_flask_app/
├── myapp/
│ ├── __init__.py
│ ├── extensions.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ ├── forms.py
│ │ └── templates/
│ │ └── auth/
│ │ ├── login.html
│ │ └── register.html
│ ├── main/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── templates/
│ │ └── main/
│ │ └── index.html
│ ├── templates/
│ │ └── base.html
│ └── static/
├── migrations/
├── tests/
├── config.py
├── requirements.txt
└── run.py
With this structure, each feature area becomes fully encapsulated with its own routes, forms, and templates.
Conclusion
A well-designed project structure is a fundamental building block of successful web applications. As we've explored:
- Various structural patterns exist, from simple to complex
- The right structure depends on your project's specific needs
- Structure should evolve as your application grows
- Consistency and separation of concerns are key principles
- Documentation helps teams maintain structural integrity
As we move forward in our web development journey, the structures we've discussed today will serve as templates for organizing our applications. In the coming weeks, as we dive into Flask, Django, and other frameworks, we'll see these patterns in action and understand how they contribute to building maintainable, scalable web applications.
Final Analogy: Think of project structure as architecture. A well-designed building isn't just about aesthetics—it's about creating spaces that people can navigate intuitively, that can withstand time and changing needs, and that make the intended activities efficient and pleasant. A good project structure serves the same purpose for your code and the developers who work with it.