Skip to main content

Flask: Developing an E-Commerce Platform

Flask: Developing an E-Commerce Platform

Building an e-commerce platform with Flask, a lightweight Python web framework powered by Werkzeug and Jinja2, allows developers to create a scalable, customizable online store with features like product listings, shopping carts, user authentication, and payment processing. This tutorial guides you through developing an e-commerce platform, covering setup, key components, and best practices for structuring a maintainable Flask project.


01. Why Build an E-Commerce Platform with Flask?

Flask’s flexibility and simplicity make it an excellent choice for building an e-commerce platform. It supports modular design with blueprints, integrates seamlessly with databases like SQLAlchemy, uses Jinja2 for dynamic templating, and can incorporate third-party services like payment gateways (e.g., Stripe). Flask enables developers to create a tailored e-commerce solution while maintaining performance and extensibility.

Example: Basic E-Commerce Setup

# app/__init__.py
from flask import Flask

def create_app():
    """Initialize the Flask e-commerce application."""
    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'secret-key'

    @app.route('/')
    def home():
        """Render the store homepage."""
        return 'Welcome to the Flask E-Commerce Store!'

    return app

Run Command:

export FLASK_APP=app
flask run

Output: (In browser at http://127.0.0.1:5000)

Welcome to the Flask E-Commerce Store!

Explanation:

  • create_app - Uses an application factory for modularity.
  • Sets up the foundation for an e-commerce platform.

02. Key Components of the E-Commerce Platform

An e-commerce platform in Flask includes routes for product management, cart functionality, checkout, and user authentication, along with a database for storing data and templates for rendering pages. The table below summarizes key components and their purposes:

Component Description Tool/Feature
Routes Handle products, cart, checkout Blueprints, Werkzeug routing
Database Store products, users, orders Flask-SQLAlchemy
Templates Render dynamic HTML Jinja2
Authentication Manage user login Flask-Login
Forms Handle user input Flask-WTF
Payments Process transactions Stripe


2.1 Project Structure

Example: E-Commerce Project Layout

ecommerce/
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── product.py
│   │   ├── user.py
│   │   ├── order.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── store.py
│   │   ├── auth.py
│   │   ├── cart.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── home.html
│   │   ├── product.html
│   │   ├── cart.html
│   │   ├── checkout.html
│   │   ├── login.html
│   ├── static/
│   │   ├── css/
│   │   │   ├── style.css
│   ├── forms.py
├── tests/
│   ├── test_routes.py
├── requirements.txt
├── run.py

Explanation:

  • Organizes code into models, routes, templates, and static files.
  • Separates store, cart, and auth routes for modularity.

2.2 Setting Up the Application

Example: Application Factory with Extensions

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from app.routes.store import store_bp
from app.routes.auth import auth_bp
from app.routes.cart import cart_bp

db = SQLAlchemy()
login_manager = LoginManager()

def create_app():
    """Initialize the Flask e-commerce application."""
    app = Flask(__name__)
    app.config.from_object('app.config.DevelopmentConfig')
    
    db.init_app(app)
    login_manager.init_app(app)
    login_manager.login_view = 'auth.login'
    
    app.register_blueprint(store_bp)
    app.register_blueprint(auth_bp)
    app.register_blueprint(cart_bp)
    
    with app.app_context():
        db.create_all()
    
    return app

# app/config.py
class DevelopmentConfig:
    SECRET_KEY = 'dev-key'
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///store.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    STRIPE_SECRET_KEY = 'your-stripe-secret-key'

Example run.py:

# run.py
from app import create_app

app = create_app()

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

Explanation:

  • Initializes SQLAlchemy for database management and Flask-Login for authentication.
  • Configures Stripe for payments (requires a Stripe account).

2.3 Database Models

Example: Product, User, and Order Models

# app/models/user.py
from app import db, login_manager
from flask_login import UserMixin

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    password = db.Column(db.String(100), nullable=False)

    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))

# app/models/product.py
from app import db

class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Float, nullable=False)
    description = db.Column(db.Text)
    stock = db.Column(db.Integer, default=0)

# app/models/order.py
from app import db
from datetime import datetime

class Order(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    status = db.Column(db.String(20), default='pending')
    user = db.relationship('User', backref='orders')

class OrderItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False)
    product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    order = db.relationship('Order', backref='items')
    product = db.relationship('Product')

Explanation:

  • User - Supports authentication with Flask-Login.
  • Product - Stores product details like name and price.
  • Order and OrderItem - Manage customer orders and their items.

2.4 Forms for User Input

Example: Login and Cart Forms

# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField, SubmitField
from wtforms.validators import DataRequired, Length, NumberRange

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=3, max=50)])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Login')

class CartForm(FlaskForm):
    quantity = IntegerField('Quantity', validators=[DataRequired(), NumberRange(min=1)])
    submit = SubmitField('Add to Cart')

Explanation:

  • Flask-WTF - Simplifies form handling with validation.
  • Forms ensure secure input for login and cart operations.

2.5 Authentication Routes

Example: User Login

# app/routes/auth.py
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required
from app.forms import LoginForm
from app.models.user import User
import hashlib

auth_bp = Blueprint('auth', __name__, template_folder='templates')

@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    """Handle user login."""
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.password == hashlib.sha256(form.password.data.encode()).hexdigest():
            login_user(user)
            return redirect(url_for('store.home'))
        flash('Invalid credentials')
    return render_template('login.html', form=form)

@auth_bp.route('/logout')
@login_required
def logout():
    """Handle user logout."""
    logout_user()
    return redirect(url_for('store.home'))

Template: login.html


{% extends 'base.html' %}
{% block content %}
<h2>Login</h2>
{% for message in get_flashed_messages() %}
    <p style="color: red;">{{ message }}</p>
{% endfor %}
<form method="post">
    {{ form.hidden_tag() }}
    {{ form.username.label }} {{ form.username() }}<br>
    {{ form.password.label }} {{ form.password() }}<br>
    {{ form.submit() }}
</form>
{% endblock %}

Explanation:

  • Flask-Login - Manages user sessions for authentication.
  • Password hashing ensures secure authentication.

2.6 Store Routes

Example: Product Listing and Viewing

# app/routes/store.py
from flask import Blueprint, render_template
from app.models.product import Product

store_bp = Blueprint('store', __name__, template_folder='templates')

@store_bp.route('/')
def home():
    """Render the store homepage with products."""
    products = Product.query.all()
    return render_template('home.html', products=products)

@store_bp.route('/product/<int:product_id>')
def view_product(product_id):
    """Render a single product page."""
    product = Product.query.get_or_404(product_id)
    return render_template('product.html', product=product)

Template: home.html


{% extends 'base.html' %}
{% block content %}
<h2>Products</h2>
<div class="product-list">
    {% for product in products %}
        <div class="product">
            <h3><a href="{{ url_for('store.view_product', product_id=product.id) }}">{{ product.name }}</a></h3>
            <p>Price: ${{ product.price }}</p>
        </div>
    {% endfor %}
</div>
{% endblock %}

Explanation:

  • Routes display product listings and individual product details.
  • Jinja2 templates render dynamic product data.

2.7 Cart and Checkout Routes

Example: Cart and Payment Processing

# app/routes/cart.py
from flask import Blueprint, render_template, redirect, url_for, session, request, current_app
from flask_login import login_required, current_user
from app import db
from app.models.product import Product
from app.models.order import Order, OrderItem
from app.forms import CartForm
import stripe

cart_bp = Blueprint('cart', __name__, template_folder='templates')

@cart_bp.route('/cart')
def view_cart():
    """Render the shopping cart."""
    cart = session.get('cart', {})
    cart_items = []
    total = 0
    for product_id, quantity in cart.items():
        product = Product.query.get(int(product_id))
        if product:
            cart_items.append({'product': product, 'quantity': quantity, 'subtotal': product.price * quantity})
            total += product.price * quantity
    return render_template('cart.html', cart_items=cart_items, total=total)

@cart_bp.route('/add_to_cart/<int:product_id>', methods=['POST'])
def add_to_cart(product_id):
    """Add a product to the cart."""
    form = CartForm()
    if form.validate_on_submit():
        product = Product.query.get_or_404(product_id)
        if product.stock < form.quantity.data:
            return 'Out of stock', 400
        cart = session.get('cart', {})
        cart[str(product_id)] = cart.get(str(product_id), 0) + form.quantity.data
        session['cart'] = cart
        session.modified = True
    return redirect(url_for('store.home'))

@cart_bp.route('/checkout', methods=['GET', 'POST'])
@login_required
def checkout():
    """Handle checkout and payment."""
    cart = session.get('cart', {})
    if not cart:
        return redirect(url_for('store.home'))
    
    if request.method == 'POST':
        stripe.api_key = current_app.config['STRIPE_SECRET_KEY']
        try:
            # Create Stripe payment intent
            total = sum(Product.query.get(int(pid)).price * qty for pid, qty in cart.items())
            intent = stripe.PaymentIntent.create(
                amount=int(total * 100),  # Amount in cents
                currency='usd',
                metadata={'user_id': current_user.id}
            )
            # Create order
            order = Order(user_id=current_user.id)
            db.session.add(order)
            db.session.flush()
            for product_id, quantity in cart.items():
                product = Product.query.get(int(product_id))
                product.stock -= quantity
                order_item = OrderItem(order_id=order.id, product_id=product_id, quantity=quantity)
                db.session.add(order_item)
            db.session.commit()
            session.pop('cart', None)
            return render_template('checkout.html', client_secret=intent.client_secret)
        except stripe.error.StripeError as e:
            return f'Payment error: {str(e)}', 400
    return render_template('checkout.html')

Template: cart.html


{% extends 'base.html' %}
{% block content %}
<h2>Your Cart</h2>
{% if cart_items %}
    <ul style="padding: 0px 0px 0px 20px; margin-top: 0px;">
    {% for item in cart_items %}
        <li>{{ item.product.name }} - Quantity: {{ item.quantity }} - Subtotal: ${{ item.subtotal }}</li>
    {% endfor %}
    </ul>
    <p>Total: ${{ total }}</p>
    {% if current_user.is_authenticated %}
        <a href="{{ url_for('cart.checkout') }}">Proceed to Checkout</a>
    {% else %}
        <a href="{{ url_for('auth.login') }}">Login to Checkout</a>
    {% endif %}
{% else %}
    <p>Your cart is empty.</p>
{% endif %}
{% endblock %}

Template: checkout.html


{% extends 'base.html' %}
{% block content %}
<h2>Checkout</h2>
{% if client_secret %}
    <form id="payment-form">
        <div id="card-element"></div>
        <button id="submit">Pay</button>
        <div id="error-message"></div>
    </form>
    <script src="https://js.stripe.com/v3/"></script>
    <script>
        var stripe = Stripe('your-stripe-publishable-key');
        var elements = stripe.elements();
        var card = elements.create('card');
        card.mount('#card-element');
        var form = document.getElementById('payment-form');
        form.addEventListener('submit', async (event) => {
            event.preventDefault();
            const {paymentIntent, error} = await stripe.confirmCardPayment('{{ client_secret }}', {
                payment_method: {card: card}
            });
            if (error) {
                document.getElementById('error-message').textContent = error.message;
            } else {
                window.location.href = '{{ url_for('store.home') }}';
            }
        });
    </script>
{% else %}
    <form method="post">
        <button type="submit">Confirm Order</button>
    </form>
{% endif %}
{% endblock %}

Explanation:

  • Cart uses session storage for temporary data.
  • Stripe integration handles secure payments.
  • Orders are saved to the database with stock updates.

2.8 Styling

Example: Basic CSS


/* app/static/css/style.css */
body {
    font-family: Arial, sans-serif;
    margin: 20px;
}
nav {
    margin-bottom: 20px;
}
nav a {
    margin-right: 10px;
}
.product-list {
    display: flex;
    flex-wrap: wrap;
}
.product {
    border: 1px solid #ccc;
    padding: 10px;
    margin: 10px;
    width: 200px;
}

Explanation:

  • Static CSS enhances the store’s appearance.
  • Linked in base.html for consistent styling.

2.9 Testing

Example: Testing Routes

# tests/test_routes.py
import pytest
from app import create_app, db
from app.models.user import User
from app.models.product import Product
import hashlib

@pytest.fixture
def client():
    app = create_app()
    app.config['TESTING'] = True
    with app.test_client() as client:
        with app.app_context():
            db.create_all()
            yield client
            db.drop_all()

def test_add_to_cart(client):
    # Create a test user and product
    password = hashlib.sha256('password'.encode()).hexdigest()
    user = User(username='testuser', password=password)
    product = Product(name='Test Product', price=10.0, stock=5)
    db.session.add_all([user, product])
    db.session.commit()
    
    # Log in
    client.post('/login', data={'username': 'testuser', 'password': 'password'})
    
    # Add to cart
    response = client.post(f'/add_to_cart/{product.id}', data={'quantity': 2})
    assert response.status_code == 302
    assert session['cart']['1'] == 2

Output: (When running pytest)

collected 1 item
tests/test_routes.py .                                            [100%]

Explanation:

  • Tests validate cart functionality and database integration.
  • Ensures reliability of core e-commerce features.

2.10 Incorrect Practices

Example: Monolithic E-Commerce App

# app.py
from flask import Flask, render_template

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///store.db'

# All routes, models, and logic in one file
@app.route('/')
def home():
    return render_template('home.html')

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

Explanation:

  • Monolithic structure is hard to maintain and scale.
  • Solution: Use blueprints, separate models, and modular design.

03. Effective Usage

3.1 Recommended Practices

  • Use blueprints, application factories, and testing for maintainability.

Example: Complete E-Commerce Setup

# app/__init__.py (as shown in 2.2)
# app/routes/store.py, cart.py, auth.py (as shown in 2.6, 2.7, 2.5)
# app/models/product.py, user.py, order.py (as shown in 2.3)
# app/forms.py (as shown in 2.4)
# app/templates/base.html, home.html, etc. (as shown in 2.6, 2.7)
# Requirements
# requirements.txt
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-WTF==1.2.1
stripe==7.0.0
pytest==8.3.3

Setup Command:

pip install -r requirements.txt
export FLASK_APP=run
flask run
  • Modular design with blueprints and separate concerns.
  • Secure authentication, payment processing, and form validation.
  • Tests ensure reliability.

3.2 Practices to Avoid

  • Avoid insecure payment handling or skipping stock validation.

Example: Missing Stock Validation

# app/routes/cart.py
@cart_bp.route('/add_to_cart/<int:product_id>', methods=['POST'])
def add_to_cart(product_id):
    form = CartForm()
    if form.validate_on_submit():
        cart = session.get('cart', {})
        cart[str(product_id)] = cart.get(str(product_id), 0) + form.quantity.data
        session['cart'] = cart
        session.modified = True
    return redirect(url_for('store.home'))
  • No stock check allows overselling.
  • Solution: Validate product.stock >= quantity.

04. Common Use Cases

4.1 Displaying Products

Render a list of products with links to individual pages.

Example: Product Listing

# app/routes/store.py (as shown in 2.6)
@store_bp.route('/')
def home():
    products = Product.query.all()
    return render_template('home.html', products=products)

Output: (In browser at /)


<h2>Products</h2>
<div class="product">
    <h3><a href="/product/1">Test Product</a></h3>
    <p>Price: $10.0</p>
</div>

Explanation:

  • Dynamic rendering with Jinja2 displays products.
  • Links to individual product pages enhance navigation.

4.2 Secure Checkout

Process payments securely with user authentication.

Example: Checkout

# app/routes/cart.py (as shown in 2.7)
@cart_bp.route('/checkout', methods=['GET', 'POST'])
@login_required
def checkout():
    cart = session.get('cart', {})
    if not cart:
        return redirect(url_for('store.home'))
    if request.method == 'POST':
        stripe.api_key = current_app.config['STRIPE_SECRET_KEY']
        # Payment and order logic (as shown)

Explanation:

  • login_required - Restricts checkout to authenticated users.
  • Stripe ensures secure payment processing.

Conclusion

Developing an e-commerce platform with Flask, leveraging Werkzeug, Jinja2, and extensions like Flask-SQLAlchemy, Flask-Login, Flask-WTF, and Stripe, enables developers to create a secure, scalable, and user-friendly online store. Key takeaways:

  • Use blueprints and application factories for modular design.
  • Implement secure authentication, payment processing, and stock validation.
  • Write tests to ensure reliability of core features.
  • Avoid monolithic code and insecure practices like missing stock checks.

With these practices, you can build a robust Flask e-commerce platform that is easy to maintain and extend!

Comments