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
andOrderItem
- 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
Post a Comment