Description
This flask boilerplate was written to help make it easy to iterate on your startup/indiehacker business, thereby increasing your chances of success.
When you're working on a project you're serious about, you want a set of conventions in place to let you develop fast and test different features. The main characteristics of this structure are:
- Predictability
- Readability
- Simplicity
- Upgradability
Includes features like: role based permissions, db migrations, validation, error handling, sanitization, integration tests, and more!
flask_for_startups alternatives and similar packages
Based on the "Flask" category.
Alternatively, view flask_for_startups alternatives based on common mentions on social networks and blogs.
-
pycord
Pycord is a modern, easy to use, feature-rich, and async ready API wrapper for Discord written in Python -
flask-restless
NO LONGER MAINTAINED - A Flask extension for creating simple ReSTful JSON APIs from SQLAlchemy models. -
apispec
A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification).. -
Flask-Muck
🧹 Flask REST framework for generating CRUD APIs and OpenAPI specs in the SQLAlchemy, Marshmallow/Pydantic application stack. -
Cilantropy
DISCONTINUED. :four_leaf_clover: Cilantropy is a Python Package Manager interface created to provide an "easy-to-use" visual and also a command-line interface for Pythonistas. Works great on windows, linux, macos :star:
SaaSHub - Software Alternatives and Reviews
* Code Quality Rankings and insights are calculated and provided by Lumnify.
They vary from L1 to L5 with "L5" being the highest.
Do you think we are missing an alternative of flask_for_startups or a related project?
README
flask_for_startups
This flask boilerplate was written to help make it easy to iterate on your startup/indiehacker business, thereby increasing your chances of success.
Interested in learning how this works? .
Want to show your support? Get me a coffee ☕
Acknowledgements
- Alex Krupp's django_for_startups repo and article
- Miguel Grinberg's flask mega tutorial repo.
Why is this useful?
When you're working on a project you're serious about, you want a set of conventions in place to let you develop fast and test different features. The main characteristics of this structure are:
- Predictability
- Readability
- Simplicity
- Upgradability
For side projects especially, having this structure would be useful because it would let you easily pick up the project after some time.
How is this different from other Flask tutorials?
If you haven't read the above article, what's written here is a summary of the main points, and along with how it contrasts with the Flask structure from other popular tutorials.
To make it simple to see, let's go through the /register
route to see how a user would create an account.
- user goes to
/register
- flask handles this request at
routes.py
: app.add_url_rule('/register', view_func=static_views.register)
- you can see that the route isn't using the usual decorator
@app.route
but instead, the route is connected with aview_func
(aka controller) routes.py
actually only lists theseadd_url_rule
functions connecting a url with a view_func- this makes it very easy for a developer to see exactly what route matches to which view function since it's all in one file. if the urls were split up, you would have to grep through your codebase to find the relevant url
- the view function in file
static_views.py
,register()
simply returns the template
- flask handles this request at
- user enters information on the register form (
register.html
), and submits their info their user details are passed along to route
/api/register
:app.add_url_rule('/api/register', view_func=account_management_views.register_account, methods=['POST'])
- here the view function in file
account_management_views.py
looks like this: ```python def register_account(): unsafe_username = request.json.get("username") unsafe_email = request.json.get("email") unhashed_password = request.json.get("password")
sanitized_username = sanitization.strip_xss(unsafe_username) sanitized_email = sanitization.strip_xss(unsafe_email)
try: user_model = account_management_services.create_account( sanitized_username, sanitized_email, unhashed_password ) except marshmallow.exceptions.ValidationError as e: return get_validation_error_response(validation_error=e, http_status_code=422) except custom_errors.EmailAddressAlreadyExistsError as e: return get_business_requirement_error_response( business_logic_error=e, http_status_code=409 ) except custom_errors.InternalDbError as e: return get_db_error_response(db_error=e, http_status_code=500)
login_user(user_model, remember=True)
return {"message": "success"}, 201
* it shows linearly what functions are called for this endpoint (*readability* and *predictability*) * the user input is always sanitized first, with clear variable names of what's unsafe and what's sanitized * then the actual account creation occurs in a `service`, which is where your business logic happens * if the `account_management_services.create_account` function returns an exception, it's caught here, and an appropriate error response is returned back to the user * otherwise, the user is logged in
so how does the account creation service work?
def create_account(sanitized_username, sanitized_email): fields_to_validate_dict = { "username": sanitized_username, "email": sanitized_email, "password": unhashed_password, } AccountValidator().load(fields_to_validate_dict) if ( db.session.query(User.email).filter_by(email=sanitized_email).first() is not None ): raise custom_errors.EmailAddressAlreadyExistsError() hash = bcrypt.hashpw(unhashed_password.encode(), bcrypt.gensalt()) password_hash = hash.decode() account_model = Account() db.session.add(account_model) db.session.flush() user_model = User( username=sanitized_username, password_hash=password_hash, email=sanitized_email, account_id=account_model.account_id, ) db.session.add(user_model) db.session.commit() return user_model
- first, the user's info has to be validated through
AccountValidator
which checks for things like, does the email exist? - then it checks whether the email exists in the database, and if so, raise a custom error
EmailAddressAlreadyExists
- otherwise, it will add the user to the database and return the
user_model
- notice how the variable is called
user_model
instead of justuser
, making it clear that it's an ORM representation of the user
- first, the user's info has to be validated through
how do these custom errors work?
- so if a user enters a email that already exists, it will raise this custom error from
custom_errors.py
python class EmailAddressAlreadyExistsError(Error): message = "There is already an account associated with this email address." internal_error_code = 40902
- the message is externally displayed to the user, while the
internal_error_code
is more for the frontend to use in debugging. it makes it easy for the frontend to see exactly what error happened and debug it (readability)python def get_business_requirement_error_response(business_logic_error, http_status_code): resp = { "errors": { "display_error": business_logic_error.message, "internal_error_code": business_logic_error.internal_error_code, } } return resp, http_status_code
- error messages are passed back to the frontend via a similar format as above:
display_error
andinternal_error_code
. the validation error message will be different in that it has field errors. (simplicity)
- so if a user enters a email that already exists, it will raise this custom error from
Testing
- the tests are mostly integration tests using a test database
- more work could be done here, but each endpoint should be tested for: permissions, validation errors, business requirement errors, and success conditions
Setup Instructions
Change .sample_flaskenv
to .flaskenv
Database setup
Databases supported:
- PostgreSQL
- MySQL
- SQLite
However, I've only tested using PostgreSQL.
Replace the DEV_DATABASE_URI
with your database uri. If you're wishing to run the tests, update TEST_DATABASE_URI
.
Repo setup
git clone [email protected]:nuvic/flask_for_startups.git
sudo apt-get install python3-dev
(needed to compile psycopg2, the python driver for PostgreSQL)- If using
poetry
for dependency management- `poetry install
- Else use
pip
to install dependenciespython3 -m venv venv
- activate virtual environment:
source venv/bin/activate
- install requirements:
pip install -r requirements.txt
- rename
.sample_flaskenv
to.flaskenv
and update the relevant environment variables in.flaskenv
- initialize the dev database:
alembic -c migrations/alembic.ini -x db=dev upgrade head
- run server:
flask run
Updating db schema
- if you make changes to models.py and want alembic to auto generate the db migration: `./scripts/db_revision_autogen.sh "your_change_here"
- if you want to write your own changes:
./scripts/db_revision_manual.sh "your_change_here"
and find the new migration file inmigrations/versions
Run tests
- if your test db needs to be migrated to latest schema:
alembic -c migrations/alembic.ini -x db=test upgrade head
python -m pytest tests
Dependency management
Using poetry.
Activate poetry shell and virtual environment:
poetry shell
Check for outdated dependencies:
poetry show --outdated
Other details
- Sequential IDs vs UUIDs?
- see brandur's article for a good analysis of UUID vs sequence IDs
- instead of UUID4, you can use a sequential UUID like a tuid