Overview
A recipe sharing application is a great full-stack project because it touches every layer of a modern web app: user authentication, CRUD operations, file uploads (for recipe images), search and filtering, and a consumable API.
This guide walks through the decisions made at each stage — why certain patterns were chosen, what trade-offs exist, and how the pieces connect. You will end up with a working application you can extend and deploy.
Prerequisites
Architecture
Images are stored as files on the server (or S3 in production) with only the file path saved in the database.
Tutorial steps
Plan your data model
Before writing any code, sketch the database schema. A recipe app needs at minimum: users, recipes, ingredients, and tags. Decide on relationships — a recipe belongs to one user, but can have many ingredients and many tags.
users → id, username, email, password_hash
recipes → id, user_id (FK), title, description, image_path, created_at
ingredients→ id, recipe_id (FK), name, quantity, unit
tags → id, name
recipe_tags→ recipe_id (FK), tag_id (FK) [junction table]
Set up the Flask application
Create the project structure and initialise Flask with SQLAlchemy for ORM and Flask-Login for session management.
pip install flask flask-sqlalchemy flask-login werkzeug
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
db = SQLAlchemy()
login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///recipes.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db.init_app(app)
login_manager.init_app(app)
return app
Define the models
Create SQLAlchemy model classes that map to your database tables. Use relationships to enable easy navigation between related records.
class Recipe(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
image_path = db.Column(db.String(300))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
ingredients = db.relationship('Ingredient', backref='recipe', lazy=True, cascade='all, delete')
Build the API routes
Create routes for listing, creating, updating, and deleting recipes. Use Flask's request object to handle JSON payloads and file uploads.
@app.route('/api/recipes', methods=['GET'])
def get_recipes():
recipes = Recipe.query.order_by(Recipe.created_at.desc()).all()
return jsonify([r.to_dict() for r in recipes])
@app.route('/api/recipes', methods=['POST'])
@login_required
def create_recipe():
data = request.get_json()
recipe = Recipe(user_id=current_user.id, title=data['title'], description=data.get('description'))
db.session.add(recipe)
db.session.commit()
return jsonify(recipe.to_dict()), 201
Handle image uploads
Allow users to upload a photo for each recipe. Validate the file type, generate a secure filename, and store it in an uploads folder. In production, swap local storage for S3.
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/api/recipes/<int:id>/image', methods=['POST'])
@login_required
def upload_image(id):
file = request.files.get('image')
if file and allowed_file(file.filename):
filename = secure_filename(f"{id}_{file.filename}")
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
recipe = Recipe.query.get_or_404(id)
recipe.image_path = filename
db.session.commit()
return jsonify({'status': 'uploaded'})
Containerise and deploy
Write a Dockerfile to containerise the application so it runs consistently in any environment. Deploy to a free cloud host like Render or Railway by connecting your GitHub repo.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:create_app()"]