Building a Professional Flask Project Structure

python-flask

Flask is a lightweight and flexible Python web framework, ideal for building web applications and APIs. While Flask’s simplicity makes it great for small projects, as your application grows, you need a structured approach to keep your code organized, maintainable, and scalable. In this article, we’ll explore how to set up a professional Flask project structure by splitting code into modules, using Blueprints, managing configurations, implementing basic logging, and introducing WSGI with Gunicorn for deployment. This guide is written in simple language, with clear explanations and practical examples to help you understand each concept. By the end, you’ll have a solid foundation for building a robust Flask application.


Why a Professional Project Structure Matters

When you start with Flask, a single app.py file is enough for small projects, like a “Hello World” app or a simple API. However, as your application grows—adding features like user authentication, product management, or multiple API endpoints—a single file becomes messy and hard to maintain. A professional project structure:

  • Organizes code: Separates concerns (e.g., routes, configuration, logic) into different files and folders.

  • Improves scalability: Makes it easier to add new features without cluttering the codebase.

  • Enhances collaboration: Allows multiple developers to work on different parts of the project.

  • Simplifies debugging: Makes errors easier to trace with proper logging and modular code.

We’ll build a sample project for an e-commerce API with user and product management to demonstrate these concepts.


Splitting app.py into Modules with __init__.py

To make your Flask project modular, you can split the single app.py file into multiple files and folders, using Python’s module system. The __init__.py file is used to mark a directory as a Python package, allowing you to organize related code into modules.

Basic Project Structure

Here’s a professional project structure for a Flask application:

ecommerce_api/
├── app/
│   ├── __init__.py        # Initializes the Flask app
│   ├── routes/            # Contains route definitions
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   └── products.py
│   ├── models/            # Data models (e.g., database schemas)
│   │   ├── __init__.py
│   │   └── user.py
│   ├── config.py          # Configuration settings
│   └── logs/              # Log files
├── tests/                 # Test cases
│   ├── __init__.py
│   └── test_routes.py
├── requirements.txt       # Project dependencies
├── run.py                 # Entry point to run the app
└── venv/                  # Virtual environment

Step 1: Create the Flask App in __init__.py

The app/__init__.py file is where you initialize the Flask application and set up configurations, extensions, and blueprints (we’ll cover blueprints later). Here’s an example:

# app/__init__.py
from flask import Flask

def create_app(config_name='development'):
    app = Flask(__name__)
    
    # Load configuration (we'll cover this later)
    from .config import config
    app.config.from_object(config[config_name])
    
    # Register blueprints (we'll cover this later)
    from .routes import auth, products
    app.register_blueprint(auth.bp)
    app.register_blueprint(products.bp)
    
    # Basic route for testing
    @app.route('/')
    def index():
        return 'Welcome to the E-commerce API!'
    
    return app
  • create_app: A factory function that creates and configures the Flask app. Using a factory makes it easier to create different app instances (e.g., for testing or production).

  • app.config.from_object: Loads configuration settings (explained in the “Config” section).

  • app.register_blueprint: Registers modular route groups (explained in the “Blueprints” section).

Step 2: Create the Entry Point (run.py)

The run.py file is the entry point to start the Flask app. It imports the create_app function and runs the app:

# run.py
from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=True)

Now, you can run the app with:

python run.py

This structure separates the app initialization (app/__init__.py) from the execution (run.py), making the codebase cleaner and more testable.

Why Use Modules?

  • Organization: Each module (e.g., routes/auth.py, models/user.py) handles a specific part of the app.

  • Reusability: The create_app function can be reused in different contexts, like testing or deployment.

  • Scalability: Adding new features (e.g., a new orders module) is as simple as creating a new file.

Using Blueprints to Manage Multiple Modules

Blueprints in Flask allow you to group related routes and logic into separate modules, making your code more modular and reusable. Think of a blueprint as a mini-Flask app that handles a specific feature, like user authentication or product management. Blueprints are then registered with the main Flask app.

Creating Blueprints

Let’s create two blueprints: one for authentication (auth) and one for products (products).

Auth Blueprint

Create app/routes/auth.py:

# app/routes/auth.py
from flask import Blueprint, jsonify, request

bp = Blueprint('auth', __name__, url_prefix='/auth')

@bp.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if username and password:
        return jsonify({'message': f'Logged in as {username}'}), 200
    return jsonify({'error': 'Invalid credentials'}), 400

@bp.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    username = data.get('username')
    if username:
        return jsonify({'message': f'User {username} registered'}), 201
    return jsonify({'error': 'Username required'}), 400
  • Blueprint(‘auth’, __name__, url_prefix=’/auth’): Creates a blueprint named auth with a URL prefix of /auth. All routes in this blueprint will start with /auth (e.g., /auth/login).

  • The login and register routes handle POST requests for user authentication.

Products Blueprint

Create app/routes/products.py:

# app/routes/products.py
from flask import Blueprint, jsonify

bp = Blueprint('products', __name__, url_prefix='/products')

# Sample data
products = [
    {'id': 1, 'name': 'Laptop', 'price': 999.99},
    {'id': 2, 'name': 'Phone', 'price': 499.99}
]

@bp.route('/', methods=['GET'])
def get_products():
    return jsonify(products)

@bp.route('/<int:id>', methods=['GET'])
def get_product(id):
    for product in products:
        if product['id'] == id:
            return jsonify(product)
    return jsonify({'error': 'Product not found'}), 404
  • This blueprint handles product-related routes, like fetching all products (/products) or a specific product (/products/1).

Registering Blueprints

The blueprints are registered in app/__init__.py (as shown earlier). After registration, you can access:

  • http://127.0.0.1:5000/auth/login for the login route.

  • http://127.0.0.1:5000/products for the products list.

Why Use Blueprints?

  • Modularity: Each blueprint handles a specific feature, making the code easier to manage.

  • Reusability: Blueprints can be reused across multiple Flask apps.

  • Clarity: Separating routes (e.g., auth vs. products) keeps the codebase organized.

For example, if you later add an orders feature, you can create an app/routes/orders.py blueprint and register it without touching other modules.

Managing Configurations: Dev, Prod, Test

Flask applications often need different settings for different environments:

  • Development: Enable debugging, use a local database.

  • Production: Disable debugging, use a secure database, and optimize performance.

  • Testing: Use a separate database for tests to avoid affecting real data.

Flask’s app.config object allows you to manage these settings. We’ll store configurations in app/config.py.

Creating Configuration Classes

Create app/config.py:

# app/config.py
class Config:
    DEBUG = False
    SECRET_KEY = 'your-secret-key'  # Used for secure sessions
    DATABASE_URI = 'sqlite:///default.db'

class DevelopmentConfig(Config):
    DEBUG = True
    DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    DATABASE_URI = 'postgresql://user:password@host:port/prod_db'

class TestingConfig(Config):
    TESTING = True
    DATABASE_URI = 'sqlite:///test.db'

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig
}
  • Config: The base class with shared settings.

  • DevelopmentConfig, ProductionConfig, TestingConfig: Environment-specific settings.

  • config: A dictionary mapping environment names to their classes.

  • SECRET_KEY: A random string for securing sessions and cookies (generate a secure one in production).

  • DATABASE_URI: Specifies the database connection (e.g., SQLite for development, PostgreSQL for production).

Using Configurations

The create_app function in app/__init__.py loads the configuration based on the config_name parameter. For example, running create_app(‘development’) applies DevelopmentConfig.

To switch environments, set an environment variable or pass a different config_name. For example, to run in production mode:

# run.py
from app import create_app

app = create_app('production')

if __name__ == '__main__':
    app.run()

You can also use an environment variable:

export FLASK_ENV=production
python run.py

Why Use Configurations?

  • Flexibility: Easily switch between environments without changing code.

  • Security: Keep sensitive settings (e.g., database credentials) separate.

  • Testing: Use a test database to avoid corrupting production data.

For example, in development, you might use a lightweight SQLite database with DEBUG=True for easy debugging. In production, you’d use a robust database like PostgreSQL and disable debugging for security.

Implementing Basic Logging in Flask

Logging is the process of recording information about your application’s behavior, such as errors, user actions, or system events. Flask uses Python’s built-in logging module, which you can configure to track issues and monitor your app.

Setting Up Logging

Add logging to app/__init__.py:

# app/__init__.py
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask

def create_app(config_name='development'):
    app = Flask(__name__)
    
    # Load configuration
    from .config import config
    app.config.from_object(config[config_name])
    
    # Set up logging
    if not app.debug:
        # Log to a file in production
        handler = RotatingFileHandler('app/logs/app.log', maxBytes=10000, backupCount=3)
        handler.setLevel(logging.INFO)
        formatter = logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
        )
        handler.setFormatter(formatter)
        app.logger.addHandler(handler)
    
    # Log a startup message
    app.logger.info('E-commerce API starting up')
    
    # Register blueprints
    from .routes import auth, products
    app.register_blueprint(auth.bp)
    app.register_blueprint(products.bp)
    
    @app.route('/')
    def index():
        app.logger.info('Accessed root endpoint')
        return 'Welcome to the E-commerce API!'
    
    return app

Explanation

  • RotatingFileHandler: Saves logs to app/logs/app.log, rotating files when they reach 10KB to prevent them from growing too large.

  • setLevel(logging.INFO): Logs messages at INFO level or higher (INFO, WARNING, ERROR, etc.).

  • formatter: Defines the log format, including timestamp, level, message, and file location.

  • app.logger: Flask’s built-in logger for logging messages.

  • Logging is disabled in debug mode (if not app.debug) to avoid cluttering the terminal.

Adding Logs to Routes

In app/routes/auth.py, add logging:

# app/routes/auth.py
from flask import Blueprint, jsonify, request

bp = Blueprint('auth', __name__, url_prefix='/auth')

@bp.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    if username and password:
        bp.logger.info(f'User {username} attempted login')
        return jsonify({'message': f'Logged in as {username}'}), 200
    bp.logger.warning('Invalid login attempt')
    return jsonify({'error': 'Invalid credentials'}), 400

Testing Logging

  1. Create the app/logs/ folder.

  2. Run the app in production mode (FLASK_ENV=production python run.py).

  3. Make a request to /auth/login with invalid credentials.

  4. Check app/logs/app.log for entries like:

2025-09-28 17:35:00,123 WARNING: Invalid login attempt [in /app/routes/auth.py:12]

Why Use Logging?

  • Debugging: Logs help you trace errors (e.g., why a login failed).

  • Monitoring: Track user activity or system performance.

  • Auditing: Record important events, like user registrations or product updates.

For example, if a user reports an issue, you can check the logs to see what happened, such as a failed login attempt due to invalid credentials.

Introduction to WSGI and Running with Gunicorn

What is WSGI?

WSGI (Web Server Gateway Interface) is a standard protocol in Python for connecting web applications (like Flask) to web servers. Flask’s built-in development server (app.run()) is great for testing but not suitable for production because it’s slow and not secure. In production, you use a WSGI server like Gunicorn to handle requests efficiently.

Why Gunicorn?

  • Performance: Gunicorn can handle multiple requests concurrently, unlike Flask’s development server.

  • Scalability: It supports multiple worker processes to handle high traffic.

  • Production-ready: Gunicorn is designed for real-world deployment, often behind a reverse proxy like Nginx.

Setting Up Gunicorn

  1. Install Gunicorn in your virtual environment:

    pip install gunicorn
  2. Update requirements.txt:

    pip freeze > requirements.txt
  3. Run the app with Gunicorn:

    gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app('production')"
    • -w 4: Uses 4 worker processes for handling requests.

    • -b 0.0.0.0:8000: Binds to port 8000, accessible externally.

    • “app:create_app(‘production’)”: Specifies the module (app) and factory function (create_app) with the production config.

  4. Access the app at http://127.0.0.1:8000.

Example: Deploying the E-commerce API

With Gunicorn running, you can test the API:

  • curl http://127.0.0.1:8000/products to get the product list.

  • curl -X POST -H “Content-Type: application/json” -d ‘{“username”: “alice”, “password”: “123”}’ http://127.0.0.1:8000/auth/login to test login.

Notes on WSGI and Gunicorn

  • In production, place Gunicorn behind a reverse proxy like Nginx to handle static files and load balancing.

  • Use environment variables for sensitive settings (e.g., SECRET_KEY, database credentials).

  • Monitor Gunicorn logs for issues, similar to Flask’s logging setup.

Why Use WSGI/Gunicorn?

  • Reliability: Gunicorn handles high traffic better than Flask’s development server.

  • Security: It’s designed for production, reducing risks like request timeouts.

  • Compatibility: WSGI ensures your Flask app works with various servers (e.g., uWSGI, Waitress).

Example: Putting It All Together

Let’s test the e-commerce API to see how the structure, blueprints, configs, logging, and Gunicorn work together.

Full Project Code

Here’s the complete app/__init__.py for reference:

# app/__init__.py
import logging
from logging.handlers import RotatingFileHandler
from flask import Flask

def create_app(config_name='development'):
    app = Flask(__name__)
    
    # Load configuration
    from .config import config
    app.config.from_object(config[config_name])
    
    # Set up logging
    if not app.debug:
        handler = RotatingFileHandler('app/logs/app.log', maxBytes=10000, backupCount=3)
        handler.setLevel(logging.INFO)
        formatter = logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
        )
        handler.setFormatter(formatter)
        app.logger.addHandler(handler)
    
    app.logger.info('E-commerce API starting up')
    
    # Register blueprints
    from .routes import auth, products
    app.register_blueprint(auth.bp)
    app.register_blueprint(products.bp)
    
    @app.route('/')
    def index():
        app.logger.info('Accessed root endpoint')
        return 'Welcome to the E-commerce API!'
    
    return app

Testing the API

  1. Start the app with Gunicorn:

    gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app('production')"
  2. Test endpoints:

    • Get products: curl http://127.0.0.1:8000/products

      [
          {"id": 1, "name": "Laptop", "price": 999.99},
          {"id": 2, "name": "Phone", "price": 499.99}
      ]
    • Login: curl -X POST -H “Content-Type: application/json” -d ‘{“username”: “alice”, “password”: “123”}’ http://127.0.0.1:8000/auth/login

      {"message": "Logged in as alice"}
  3. Check app/logs/app.log for logs like:

    2025-09-28 17:35:00,123 INFO: E-commerce API starting up [in /app/__init__.py:20]
    2025-09-28 17:35:05,456 INFO: Accessed root endpoint [in /app/__init__.py:30]
    2025-09-28 17:35:10,789 INFO: User alice attempted login [in /app/routes/auth.py:10]

Adding a New Feature

To add an orders blueprint:

  1. Create app/routes/orders.py:

    # app/routes/orders.py
    from flask import Blueprint, jsonify
    
    bp = Blueprint('orders', __name__, url_prefix='/orders')
    
    @bp.route('/', methods=['GET'])
    def get_orders():
        return jsonify([{'id': 1, 'product_id': 1, 'quantity': 2}])
  2. Register it in app/__init__.py:

    from .routes import auth, products, orders
    app.register_blueprint(orders.bp)
  3. Test: curl http://127.0.0.1:8000/orders.

This demonstrates how easy it is to extend a modular Flask project.

Conclusion

A professional Flask project structure is essential for building scalable, maintainable, and collaborative applications. By splitting app.py into modules with __init__.py, you organize your code logically. Blueprints allow you to group related routes, making features like authentication and product management modular. Configurations let you tailor settings for development, production, and testing environments. Logging helps you monitor and debug your app, while WSGI servers like Gunicorn ensure your app is production-ready. The e-commerce API example shows how these components work together to create a robust backend. With this foundation, you can confidently build complex Flask applications, adding features like databases, authentication, or advanced logging as needed.

Leave a Reply

Your email address will not be published. Required fields are marked *