Mvc Folder structure
π§± Flask MVC Setup β Step 1 (No .env, No External APIs)
Who is this for? You. A developer who wants a clean, working Flask MVC backend using SQLAlchemy, Flask-RESTful, and Blueprints β without any magic, without
.envfiles, without overcomplicating things. Just a solid foundation that works.
π Table of Contents
- What is MVC and Why Should You Care?
- Project Structure β The Full Picture
- File by File β What Each File Does and Why
- Setup Instructions β Step by Step
- The Flask-Migrate Workflow β Every Time
- Seeding the Database
- Testing Your API
- β οΈ Cautions & Gotchas β Read These
- Quick Reference Cheatsheet
1. What is MVC?
MVC stands for ModelβViewβController. It's a way of organising your code so that each part of your app has one clear job. Think of it like a restaurant:
| Layer | Restaurant Analogy | In Your Flask App |
|---|---|---|
| Model | The fridge + ingredients | SQLAlchemy classes, DB schema |
| Controller | The chef | Route handlers, business logic |
| View | The plate given to the customer | JSON response returned to the client |
π‘ In a REST API, there are no HTML templates. The "View" is simply the JSON you send back. So MVC in Flask REST = Models + Controllers + JSON responses.
Why bother?
Without MVC, everything ends up in app.py β routes, models, config, seed data. It works at first. Then it grows. Then it becomes unreadable. MVC forces separation so that:
- Your models never know about HTTP requests
- Your controllers never define database columns
- Your
app.pyis just glue β it connects things together
2. Project Structure
your-project/
βββ Pipfile β dependency definitions
βββ Pipfile.lock β locked dependency versions
βββ server/ β ALL your Python code lives here
βββ app.py β creates the Flask app, registers Blueprints
βββ config.py β database URI and app settings
βββ seed.py β puts test data in the database
βββ migrations/ β auto-generated by flask db init (don't edit)
β βββ versions/ β each migration file lives here
βββ models/
β βββ __init__.py β creates the `db` object (ONE place only)
β βββ user.py β your User SQLAlchemy model
βββ controllers/
βββ __init__.py β empty file, marks folder as a package
βββ user_controller.py β Blueprint + Flask-RESTful Resource for users
β οΈ Do NOT create a
server/__init__.pyfile. If one exists, delete it. It makes Flask treatserver/as a Python package and causesModuleNotFoundError: No module named 'config'β because Python then looks forconfigfrom the parent directory instead of insideserver/.
3. File by File
Why put comments at the top of every file?
# server/models/user.py
When you have 10 files open in VS Code and an error points to line 12, you want to know immediately which file you're looking at without squinting at the tab. It also helps when you share code β paste it anywhere and the location is self-documenting. Always do this.
server/config.py β Settings only
# server/config.py
import os
class Config:
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
JSON_COMPACT = False
What it does: Holds all your app configuration in one class. No Flask logic here. Just settings.
SQLALCHEMY_DATABASE_URI β tells SQLAlchemy where your database is. Right now it defaults to a local SQLite file called app.db inside an instance/ folder. No database server needed for Step 1.
JSON_COMPACT = False β makes your JSON responses pretty-printed (easier to read in Postman).
server/models/__init__.py β The db object lives here
# server/models/__init__.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
This is the most important file you'll never think about.
db is your SQLAlchemy instance. Every model imports db from here. It is created once and only once in this file. Never create SQLAlchemy() anywhere else.
β οΈ Why here and not in
app.py? If you putdb = SQLAlchemy()inapp.py, and your models import fromapp.py, andapp.pyimports from your models β you get a circular import error. Keepingdbinmodels/__init__.pybreaks that cycle cleanly.
server/models/user.py β The Model (data layer)
# server/models/user.py
from sqlalchemy_serializer import SerializerMixin
from . import db
class User(db.Model, SerializerMixin):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False)
def __repr__(self):
return f"<User {self.name}>"
What it does: Defines the users table. Each attribute is a column. SerializerMixin gives you .to_dict() for free β so you can turn a User object into a JSON-friendly dictionary with one call.
Rules for models:
- β Define columns here
- β Define relationships here
- β
Define
serialize_ruleshere - β Never import
requestorjsonifyhere - β Never write route logic here
π‘ VS Code auto-formatter note: Ruff/Black will reorder your imports alphabetically β
from sqlalchemy_serializerbeforefrom . import db. This is fine. It doesn't cause errors. It's just PEP8 style.
server/controllers/__init__.py β Empty on purpose
# server/controllers/__init__.py
Leave it empty. Its only job is to tell Python "this folder is a package so you can import from it." Nothing else goes here in Step 1.
server/controllers/user_controller.py β The Controller (route logic)
# server/controllers/user_controller.py
from flask import Blueprint
from flask_restful import Api, Resource
from models.user import User as UserModel
user_bp = Blueprint("users", __name__, url_prefix="/users")
api = Api(user_bp)
class UserList(Resource):
def get(self):
users = [u.to_dict() for u in UserModel.query.all()]
return users, 200
class UserById(Resource):
def get(self, id):
user = UserModel.query.get(id)
if not user:
return {"error": "User not found"}, 404
return user.to_dict(), 200
api.add_resource(UserList, "/")
api.add_resource(UserById, "/<int:id>")
What it does: Defines your routes using Flask-RESTful Resource classes. Each HTTP method (get, post, patch, delete) is a method on the class.
β οΈ CAUTION 1 β The Name Collision Trap
You imported your SQLAlchemy model as
User. If you then define a class also calledUser, the second definition overwrites the first:from models.user import User # β this User... class User(Resource): # β ...is now gone. Replaced by this. def get(self): User.query.all() # β AttributeError: Resource has no .queryFix option A β Alias the import:
from models.user import User as UserModelFix option B β Rename the Resource class:
class UserList(Resource): # different name, no collisionBoth work. Pick whichever feels more readable.
β οΈ CAUTION 2 β The Double
/usersTrapYour Blueprint already has
url_prefix="/users". The path you pass toapi.add_resource()gets appended to that prefix:# Blueprint prefix: /users # Resource path: /users # Final URL: /users/users β WRONG, gives 404 api.add_resource(UserList, "/users") # β # Blueprint prefix: /users # Resource path: / # Final URL: /users/ β CORRECT api.add_resource(UserList, "/") # β
server/app.py β The Glue
# server/app.py
from config import Config
from controllers.user_controller import user_bp
from flask import Flask
from flask_migrate import Migrate
from models import db
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
# Initialise extensions
db.init_app(app)
Migrate(app, db)
# Tell Flask-Migrate about your models
# Without this, `flask db migrate` creates an empty migration
with app.app_context():
from models import user # noqa: F401
# Register Blueprints (controllers)
app.register_blueprint(user_bp)
return app
app = create_app()
if __name__ == "__main__":
app.run(port=5555, debug=True)
What it does: Creates the Flask app, wires everything together, and registers your controllers (Blueprints).
β οΈ CAUTION 3 β
db.init_app(app)notSQLAlchemy(app)There are two ways to initialise Flask-SQLAlchemy:
# Option A β Application Factory style (what we use) db = SQLAlchemy() # in models/__init__.py db.init_app(app) # in app.py inside create_app() # Option B β Direct style (don't use this with MVC) db = SQLAlchemy(app) # ties db directly to one app instanceUse Option A (the factory pattern). Option B causes circular imports in a multi-file project because
dbneedsappbeforeappis fully built.
β οΈ CAUTION 4 β The Empty Migration Trap
If you forget the
with app.app_context(): from models import userblock, Flask-Migrate won't detect your models. It will generate an empty migration:# What you want to see: INFO Detected added table 'users' β β model was found # What you get without the import: INFO No changes in schema detected β β model was invisibleThe
# noqa: F401comment tells Ruff/Black "yes I know this import is unused, leave it alone." Without it, the formatter will delete the import.
server/seed.py β Test Data
# server/seed.py
from app import app
from models import db
from models.user import User
with app.app_context():
print("Clearing old data...")
User.query.delete()
print("Seeding users...")
users = [
User(name="Alice"),
User(name="Bob"),
User(name="Charlie"),
]
db.session.add_all(users)
db.session.commit()
print("Done! β
")
Run it with: python seed.py (from inside server/)
4. Setup Instructions
Prerequisites
- Python 3.12 via pyenv
- pipenv installed
Step-by-step
# 1. Clone or create your project, then go to the project root
cd your-project
# 2. Create the virtual environment and install dependencies
pipenv install flask flask-sqlalchemy flask-migrate sqlalchemy-serializer \
flask-restful flask-cors psycopg2-binary python-dotenv
# 3. Enter the virtual environment
pipenv shell
# 4. Go into the server directory β ALL flask commands run from here
cd server
# 5. Set environment variables
export FLASK_APP=app.py
export FLASK_RUN_PORT=5555
# 6. Run the migration workflow (see section 5)
flask db init
flask db migrate -m "Initial migration."
flask db upgrade
# 7. Seed the database
python seed.py
# 8. Start the server
flask run
β οΈ CAUTION 5 β Always
cd serverbefore flask commandsFlask reads
FLASK_APP=app.pyand looks forapp.pyin the current directory. If you're at the project root, it won't find it:# β Wrong β you're at the root ~/your-project β― flask run Error: Could not locate a Flask application # β Correct β you're inside server/ ~/your-project/server β― flask run * Running on http://127.0.0.1:5555
β οΈ CAUTION 6 β
flaskcommand not found?This means you're outside the virtual environment:
β― flask run zsh: command not found: flask β you forgot pipenv shell β― pipenv shell β fix: enter the venv first β― flask run β now it works
β οΈ CAUTION 7 β Never delete
server/__init__.py... wait, there shouldn't BE one
server/should NOT have an__init__.py. If one appears (VS Code or a tool created it automatically), delete it immediately:rm server/__init__.pyIf it exists, Flask treats
server/as a Python package and imports everything relative to the parent directory. This causes:ModuleNotFoundError: No module named 'config'Even though
config.pyis right there inserver/.
models/andcontrollers/SHOULD have__init__.pyβ they are sub-packages.server/itself should NOT.
5. The Flask-Migrate Workflow
You run these three commands in this order, every time you change a model:
# Run once per project β creates the migrations/ folder
flask db init
# Run every time you add/change a model column
# The message describes what changed
flask db migrate -m "add email column to users"
# Run to apply the migration to the actual database
flask db upgrade
Think of it like this:
| Command | What it does | Analogy |
|---|---|---|
flask db init | Creates the migrations system | Setting up a kitchen for the first time |
flask db migrate | Writes a migration file (a recipe) | Writing down what to cook |
flask db upgrade | Executes the migration on the DB | Actually cooking the meal |
After flask db migrate, always check the output:
INFO Detected added table 'users' β β
Good β model was found
INFO No changes in schema detected β β Bad β model was invisible, check your imports
If you see "No changes detected" when you expected changes, go back to app.py and confirm the with app.app_context(): from models import user block is there.
6. Seeding the Database
# From inside server/ with venv active
python seed.py
The seed file always starts with User.query.delete() β this wipes existing data so you can re-run it cleanly without duplicate entries.
7. Testing Your API
With the server running (flask run), test these endpoints in Postman or your browser:
| Method | URL | Expected Response |
|---|---|---|
| GET | http://localhost:5555/users/ | Array of all users |
| GET | http://localhost:5555/users/1 | Single user object |
π‘ Notice the trailing slash on
/users/. With Flask-RESTful and Blueprints, the root resource is registered at"/"and the prefix is"/users", making the full URL/users/. Without the slash you may get a redirect (301) or 404 depending on Flask'sstrict_slashessetting.
8. β οΈ Cautions & Gotchas β Master List
Here is every trap consolidated in one place:
C1 β Name collision between model import and Resource class
# β Breaks β class User overwrites the imported User model
from models.user import User
class User(Resource): ...
# β
Fix A β alias the import
from models.user import User as UserModel
class User(Resource): ...
# β
Fix B β rename the Resource class
from models.user import User
class UserList(Resource): ...
C2 β Double URL prefix
# Blueprint prefix is already /users
user_bp = Blueprint("users", __name__, url_prefix="/users")
# β /users + /users = /users/users β 404
api.add_resource(UserList, "/users")
# β
/users + / = /users/ β 200
api.add_resource(UserList, "/")
C3 β db created in the wrong place
# β Creates circular import in multi-file apps
# app.py creates db, models import app.py, app.py imports models... loop
# β
db lives in models/__init__.py β imported from there by everyone
from models import db
C4 β Flask-Migrate generates empty migration
# β Forgot to register models with app context
def create_app():
db.init_app(app)
Migrate(app, db)
# models never imported β migrate sees nothing
# β
Import models inside app context
def create_app():
db.init_app(app)
Migrate(app, db)
with app.app_context():
from models import user # noqa: F401
C5 β Running flask commands from the wrong directory
# β From project root
~/your-project β― flask db migrate
Error: Could not locate a Flask application
# β
From inside server/
~/your-project/server β― flask db migrate
C6 β Flask not found (outside venv)
# β
β― flask run
zsh: command not found: flask
# β
Enter the venv first
β― pipenv shell
β― flask run
C7 β server/__init__.py must NOT exist
# If this file exists, delete it
rm server/__init__.py
# Error it causes:
# ModuleNotFoundError: No module named 'config'
# (even though config.py is right there)
C8 β noqa: F401 on model imports in app.py
# Ruff/Black will delete "unused" imports
# This import IS used β by Flask-Migrate β just not explicitly in the code
with app.app_context():
from models import user # noqa: F401 β tells Ruff: leave this alone
C9 β VS Code reformatting imports is NOT an error
# Your order: # Ruff's order (also fine):
from . import db from sqlalchemy_serializer import SerializerMixin
from sqlalchemy_serializer from . import db
Both are identical at runtime. Ruff just enforces PEP8 alphabetical ordering. It is not breaking your code.
9. Quick Reference Cheatsheet
# === FIRST TIME SETUP ===
cd your-project
pipenv install
pipenv shell
cd server
export FLASK_APP=app.py
export FLASK_RUN_PORT=5555
flask db init
flask db migrate -m "Initial migration."
flask db upgrade
python seed.py
flask run
# === AFTER CHANGING A MODEL ===
flask db migrate -m "describe your change"
flask db upgrade
# === DAILY WORKFLOW ===
cd your-project
pipenv shell
cd server
export FLASK_APP=app.py
flask run
Pipfile (for reference)
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "*"
flask-sqlalchemy = "*"
flask-migrate = "*"
sqlalchemy-serializer = "*"
psycopg2-binary = "*"
python-dotenv = "*"
flask-restful = "*"
flask-cors = "*"
bcrypt = "*"
flask-bcrypt = "*"
flask-jwt-extended = "*"
[dev-packages]
[requires]
python_version = "3.12"
π Step 2 (coming later): Adding
.envfor secrets, integrating external APIs, and adding autils/layer for shared helper functions.
π Resources
π Reading
| Resource | Why it's good |
|---|---|
| Real Python β MVC with Lego analogy | Best conceptual intro, very beginner-friendly |
| DigitalOcean β Flask Blueprints + SQLAlchemy | Hands-on full tutorial, matches your stack exactly |
| Flask Mega-Tutorial Part XV | The gold standard β create_app() factory pattern deep dive |
| Real Python β Flask Blueprints | Deep dive into Blueprints specifically |
| Medium β Minimal Flask MVC | Short, clean walkthrough with diagrams |
π₯ YouTube
| Video | Channel | What to learn |
|---|---|---|
| Flask Application Factory Pattern | Corey Schafer | create_app(), Blueprints, config |
| Flask REST API Full Course | freeCodeCamp | Full REST API with Flask + SQLAlchemy |
| Flask Blueprints Tutorial | Pretty Printed | Blueprints explained simply |
| SQLAlchemy Relationships | Tech With Tim | One-to-many, many-to-many in SQLAlchemy |