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 .env files, without overcomplicating things. Just a solid foundation that works.


πŸ“– Table of Contents

  1. What is MVC and Why Should You Care?
  2. Project Structure β€” The Full Picture
  3. File by File β€” What Each File Does and Why
  4. Setup Instructions β€” Step by Step
  5. The Flask-Migrate Workflow β€” Every Time
  6. Seeding the Database
  7. Testing Your API
  8. ⚠️ Cautions & Gotchas β€” Read These
  9. 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:


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__.py file. If one exists, delete it. It makes Flask treat server/ as a Python package and causes ModuleNotFoundError: No module named 'config' β€” because Python then looks for config from the parent directory instead of inside server/.


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 put db = SQLAlchemy() in app.py, and your models import from app.py, and app.py imports from your models β€” you get a circular import error. Keeping db in models/__init__.py breaks 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:

πŸ’‘ VS Code auto-formatter note: Ruff/Black will reorder your imports alphabetically β€” from sqlalchemy_serializer before from . 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 called User, 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 .query

Fix option A β€” Alias the import:

from models.user import User as UserModel

Fix option B β€” Rename the Resource class:

class UserList(Resource):   # different name, no collision

Both work. Pick whichever feels more readable.

⚠️ CAUTION 2 β€” The Double /users Trap

Your Blueprint already has url_prefix="/users". The path you pass to api.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) not SQLAlchemy(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 instance

Use Option A (the factory pattern). Option B causes circular imports in a multi-file project because db needs app before app is fully built.

⚠️ CAUTION 4 β€” The Empty Migration Trap

If you forget the with app.app_context(): from models import user block, 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 invisible

The # noqa: F401 comment 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

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 server before flask commands

Flask reads FLASK_APP=app.py and looks for app.py in 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 β€” flask command 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__.py

If 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.py is right there in server/.

models/ and controllers/ 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's strict_slashes setting.


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 .env for secrets, integrating external APIs, and adding a utils/ 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
Powered by Forestry.md