├── .gitignore ├── README.md ├── config.py ├── env_example.txt ├── main.py ├── manage.py ├── requirements.txt ├── src ├── __init__.py ├── api │ ├── __init__.py │ └── resources │ │ ├── __init__.py │ │ ├── admin │ │ └── __init__.py │ │ └── auth.py ├── middlewares │ ├── __init__.py │ └── admin.py ├── models │ ├── __init__.py │ ├── basemodel.py │ ├── schemas │ │ ├── __init__.py │ │ ├── base.py │ │ └── user.py │ └── user.py ├── routes │ ├── __init__.py │ └── api.py └── utils │ ├── __init__.py │ ├── helpers.py │ ├── security.py │ └── validators.py └── tests ├── __init__.py └── test_app.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | idea/ 3 | .idea 4 | .venv 5 | .code 6 | vscode 7 | .vscode 8 | .env 9 | .DS_Store 10 | .__pycache__/ 11 | __pycache__/ 12 | tests/__pycache__/ 13 | .pytest_cache/ 14 | migrations/ 15 | **.db 16 | instance 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Rest API Setup 2 | 3 | Initial scaffolding for a flask rest API development. Starter template for building your Flask and Flask Rest API applications. 4 | 5 | ## Features 6 | - Gunicorn server setup 7 | - Database migration setup for SQLAlchemy Models 8 | - JWT authentication at `/api/v1/login` 9 | - Url to register your admin and users at `/api/v1/signup` 10 | - Provisioned route file for all your routes with admin blueprints 11 | - Test environment setup 12 | 13 | ## Built With 14 | 15 | This App was developed with the following stack: 16 | 17 | - Python==3.12 18 | - Flask==3.10 19 | - Flask-restful==0.3.10 20 | - Flask-Script==2.0.6 21 | - Flask-SQLAlchemy==3.1.1 22 | - Postgres DB / SQlite 23 | - Gunicorn Web Server 24 | 25 | ## Requirements 26 | - Python 3.12+ 27 | - Python pip 28 | - Postgres / SQlite 29 | 30 | ## Installation 31 | - fork this repository 32 | - create a .env file as shown in the env_example file 33 | - setup your database 34 | - on the terminal cd into the app folder 35 | - run `pip install -r requirements.txt` to install required modules 36 | - run `flask --app manage db init ` to setup alembic migrations 37 | - run `flask --app manage db migrate -m=''` to create migration files 38 | - then run `flask --app python manage db upgrade` to create tables 39 | 40 | ## Running the App 41 | - on the terminal run `gunicorn main:app` 42 | - To run app on a specific port use `gunicorn -127.0.0.1:port main:app` 43 | 44 | ## Usage 45 | - `src/api/resources` --- flask-restful resources for your project 46 | - `src/models` --- SQLAlchemy models and schema 47 | - `src/routes/api` --- contains all your route definition 48 | - `src/utils` --- contains validations, security and helper files 49 | - `src/middlewares` --- define your middleware files here 50 | - You can modify the app to suit your need. 51 | - Happy usage. 52 | 53 | ## Update Information 54 | - Flask-JWT has been replaced with Flask-JWT-extended 55 | - Flask-Scripts and dependencies removed. 56 | - Future versions may use Pydantic instead of Marshmallow 57 | 58 | ## Contributions 59 | - Contributors are needed to keep developing this template with updates to its dependencies. You can reach me on my email. 60 | 61 | ## Credits 62 | solnsumei@gmail.com 63 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | 7 | app_environment = os.environ.get('FLASK_ENV') 8 | JWT_AUTH_URL_RULE = '/api/v1/login' 9 | JWT_AUTH_USERNAME_KEY = 'email' 10 | JWT_SECRET_KEY = os.environ.get('SECRET_KEY') 11 | 12 | if app_environment == 'development': 13 | SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URI") 14 | 15 | if app_environment == 'production': 16 | SQLALCHEMY_DATABASE_URI = os.environ.get("PROD_DATABASE_URI") 17 | 18 | DEBUG = os.environ.get("DEBUG") 19 | SQLALCHEMY_TRACK_MODIFICATIONS = False 20 | -------------------------------------------------------------------------------- /env_example.txt: -------------------------------------------------------------------------------- 1 | FLASK_ENV=development 2 | 3 | DATABASE_URI= 4 | 5 | PROD_DATABASE_URI= 6 | 7 | DEBUG=True 8 | 9 | SECRET_KEY= -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint 2 | from flask_restful import Api 3 | from flask_jwt_extended import JWTManager 4 | from src.models.basemodel import db 5 | 6 | 7 | def initdb(flask_app): 8 | db.init_app(flask_app) 9 | 10 | 11 | def create_app(): 12 | from src.routes.api import routes, admin_routes 13 | 14 | flask_app = Flask(__name__) 15 | flask_app.config.from_pyfile('config.py') 16 | 17 | bp = Blueprint('api', __name__, static_url_path="assets") 18 | 19 | admin_bp = Blueprint('admin', __name__, static_url_path="assets") 20 | 21 | api = Api(bp) 22 | admin = Api(admin_bp) 23 | 24 | routes(api) 25 | admin_routes(admin) 26 | 27 | flask_app.register_blueprint(bp, url_prefix="/api/v1") 28 | 29 | flask_app.register_blueprint(admin_bp, url_prefix="/api/v1/admin") 30 | 31 | @flask_app.route('/') 32 | def index(): 33 | return 'Welcome to Flask Rest API Setup!' 34 | 35 | initdb(flask_app) 36 | 37 | return flask_app 38 | 39 | 40 | app = create_app() 41 | jwt = JWTManager(app) 42 | 43 | 44 | if __name__ == "__main__": 45 | app.run() 46 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | from flask_migrate import Migrate 4 | from src.models.basemodel import db 5 | from main import create_app 6 | 7 | app = create_app() 8 | 9 | 10 | MODELS_DIRECTORY = "models" 11 | EXCLUDE_FILES = ["__init__.py"] 12 | 13 | 14 | def scan_models(): 15 | for dir_path, dir_names, file_names in os.walk(MODELS_DIRECTORY): 16 | for file_name in file_names: 17 | if file_name.endswith("py") and file_name not in EXCLUDE_FILES: 18 | file_path_wo_ext, _ = os.path.splitext((os.path.join(dir_path, file_name))) 19 | module_name = file_path_wo_ext.replace(os.sep, ".") 20 | importlib.import_module(module_name) 21 | 22 | 23 | migrate = Migrate(app, db) 24 | 25 | if __name__ == "__main__": 26 | scan_models() 27 | 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.14.1 2 | aniso8601==10.0.0 3 | attrs==25.1.0 4 | bcrypt==4.2.1 5 | blinker==1.9.0 6 | cffi==1.17.1 7 | click==8.1.8 8 | dnspython==2.7.0 9 | eventlet==0.39.0 10 | Flask==3.1.0 11 | Flask-JWT-Extended==4.7.1 12 | Flask-Migrate==4.1.0 13 | Flask-RESTful==0.3.10 14 | Flask-Script==2.0.6 15 | Flask-SQLAlchemy==3.1.1 16 | greenlet==3.1.1 17 | gunicorn==23.0.0 18 | iniconfig==2.0.0 19 | itsdangerous==2.2.0 20 | Jinja2==3.1.6 21 | Mako==1.3.9 22 | MarkupSafe==3.0.2 23 | marshmallow==3.26.1 24 | monotonic==1.6 25 | packaging==24.2 26 | passlib==1.7.4 27 | pluggy==1.5.0 28 | psycopg2-binary==2.9.10 29 | pycparser==2.22 30 | PyJWT==2.10.1 31 | pyparsing==3.2.1 32 | pytest==8.3.4 33 | python-dateutil==2.9.0.post0 34 | python-dotenv==1.0.1 35 | python-editor==1.0.4 36 | python-slugify==8.0.4 37 | pytz==2025.1 38 | six==1.17.0 39 | SQLAlchemy==2.0.38 40 | text-unidecode==1.3 41 | toml==0.10.2 42 | typing_extensions==4.12.2 43 | urllib3==2.3.0 44 | webargs==8.6.0 45 | Werkzeug==3.1.3 46 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/__init__.py -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/api/resources/__init__.py -------------------------------------------------------------------------------- /src/api/resources/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/api/resources/admin/__init__.py -------------------------------------------------------------------------------- /src/api/resources/auth.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from hmac import compare_digest 3 | from src.models.user import UserModel 4 | from src.models.schemas.user import UserSchema, user_summary 5 | 6 | user_schema = UserSchema() 7 | 8 | 9 | class Register(Resource): 10 | @staticmethod 11 | @UserSchema.validate_fields(location=('json',)) 12 | def post(args): 13 | 14 | if not compare_digest(args['password'], args['password_confirmation']): 15 | return { 16 | 'success': False, 17 | 'errors': { 18 | 'password': ['Password and password confirmation do not match']} 19 | }, 409 20 | 21 | user = UserModel.find_by_email(args['email']) 22 | if user: 23 | return { 24 | 'success': False, 25 | 'error': 'Email has already been taken' 26 | }, 409 27 | 28 | is_admin = False 29 | if UserModel.count_all() < 1: 30 | is_admin = True 31 | 32 | phone = None 33 | 34 | if 'phone' in args: 35 | phone = args['phone'] 36 | 37 | hashed_password = UserModel.generate_hash(args['password']) 38 | 39 | user = UserModel(args['name'], hashed_password, args['email'], phone, is_admin) 40 | user.save_to_db() 41 | 42 | return { 43 | 'success': True, 44 | 'user': user_summary.dump(user).data 45 | }, 201 46 | -------------------------------------------------------------------------------- /src/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/middlewares/__init__.py -------------------------------------------------------------------------------- /src/middlewares/admin.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from flask_jwt_extended import current_user 3 | 4 | 5 | def admin_required(): 6 | def decorator(f): 7 | @wraps(f) 8 | def decorated_function(*args, **kwargs): 9 | if not current_user.is_admin: 10 | return {"error": "Unauthorized user"}, 403 11 | return f(*args, **kwargs) 12 | return decorated_function 13 | return decorator 14 | -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/models/__init__.py -------------------------------------------------------------------------------- /src/models/basemodel.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from slugify import slugify 3 | from datetime import datetime 4 | 5 | db = SQLAlchemy() 6 | 7 | 8 | class BaseModel(db.Model): 9 | __abstract__ = True 10 | 11 | id = db.Column(db.Integer, primary_key=True) 12 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 13 | updated_at = db.Column(db.DateTime, default=datetime.utcnow) 14 | 15 | """ Database operations """ 16 | 17 | @classmethod 18 | def find_all(cls): 19 | return cls.query.all() 20 | 21 | @classmethod 22 | def count_all(cls): 23 | return cls.query.count() 24 | 25 | @classmethod 26 | def find_by_id(cls, _id): 27 | return cls.query.get(_id) 28 | 29 | @classmethod 30 | def find_latest(cls): 31 | return cls.query.order_by(cls.id.desc()).first() 32 | 33 | @classmethod 34 | def find_enabled_item(cls, _id): 35 | return cls.query.filter_by(id=_id, status='enabled').first() 36 | 37 | @classmethod 38 | def find_by_first(cls, **kwargs): 39 | return cls.query.filter_by(**kwargs).first() 40 | 41 | @classmethod 42 | def find_by(cls, **kwargs): 43 | return cls.query.filter_by(**kwargs).all() 44 | 45 | @classmethod 46 | def find_and_order_by(cls, order_by='id', **kwargs): 47 | return cls.query.filter_by(**kwargs).order_by(order_by).all() 48 | 49 | @classmethod 50 | def find_by_slug(cls, slug): 51 | return cls.query.filter_by(slug=slug).first() 52 | 53 | @classmethod 54 | def find_all_omit_record_with_this_id(cls, _id): 55 | return cls.query.filter(cls.id != _id).all() 56 | 57 | @classmethod 58 | def find_all_omit_record_with_this_name(cls, name): 59 | return cls.query.filter(cls.name != name).all() 60 | 61 | @classmethod 62 | def find_all_omit_record_with_this_slug(cls, slug): 63 | return cls.query.filter(cls.slug != slug).all() 64 | 65 | @classmethod 66 | def find_first_omit_record_with_this_name(cls, _id, name): 67 | return cls.query.filter_by(name=name).filter(cls.id != _id).first() 68 | 69 | def save_to_db(self): 70 | if self.id is not None: 71 | self.updated_at = datetime.utcnow() 72 | 73 | db.session.add(self) 74 | db.session.commit() 75 | 76 | def delete_from_db(self): 77 | db.session.delete(self) 78 | db.session.commit() 79 | 80 | """ Utility functions """ 81 | 82 | @classmethod 83 | def make_slug(cls, slug): 84 | return slugify(slug) 85 | 86 | @classmethod 87 | def date_to_string(cls, raw_date): 88 | return "{}".format(raw_date) 89 | -------------------------------------------------------------------------------- /src/models/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/models/schemas/__init__.py -------------------------------------------------------------------------------- /src/models/schemas/base.py: -------------------------------------------------------------------------------- 1 | from webargs.flaskparser import use_args, use_kwargs 2 | from marshmallow import Schema, fields 3 | 4 | 5 | class BaseSchema(Schema): 6 | __abstract__ = True 7 | 8 | id = fields.Int(dump_only=True) 9 | created_at = fields.DateTime(dump_only=True) 10 | updated_at = fields.DateTime(dump_only=True) 11 | 12 | @classmethod 13 | def validate_fields(cls, args_type=None, schema_kwargs=None, **kwargs): 14 | schema_kwargs = schema_kwargs or {} 15 | 16 | def factory(request): 17 | # Filter based on 'fields' query parameter 18 | only = request.args.get('fields', None) 19 | # Respect partial updates for PATCH requests 20 | partial = request.method == 'PUT' 21 | # Add current request to the schema's context 22 | # and ensure we're always using strict mode 23 | return cls( 24 | only=only, partial=partial, strict=True, 25 | context={'request': request}, **schema_kwargs 26 | ) 27 | 28 | if args_type == 'use_kwargs': 29 | return use_kwargs(factory, **kwargs) 30 | return use_args(factory, **kwargs) 31 | 32 | 33 | class SluggableSchema(BaseSchema): 34 | slug = fields.String(dump_only=True) 35 | -------------------------------------------------------------------------------- /src/models/schemas/user.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, validate, post_load 2 | from src.utils.validators import name_field, validate_len 3 | from .base import BaseSchema 4 | 5 | 6 | class UserSchema(BaseSchema): 7 | name = name_field 8 | email = fields.Email(required=True) 9 | phone = fields.Int( 10 | validate=[validate.Length(min=11, max=15), lambda p: validate_len(p, 11)] 11 | ) 12 | password = fields.Str(load_only=True, required=True, validate=lambda p: validate_len(p, 8)) 13 | password_confirmation = fields.Str(required=True, load_only=True) 14 | 15 | @post_load() 16 | def user_details_strip(self, data): 17 | data['email'] = data['email'].lower().strip() 18 | 19 | 20 | user_summary = UserSchema(exclude=('updated_at',)) 21 | -------------------------------------------------------------------------------- /src/models/user.py: -------------------------------------------------------------------------------- 1 | from passlib.hash import bcrypt_sha256 as sha256 2 | from .basemodel import BaseModel, db 3 | 4 | 5 | class UserModel(BaseModel): 6 | __tablename__ = "users" 7 | 8 | name = db.Column(db.String(80), nullable=False) 9 | phone = db.Column(db.String(15)) 10 | email = db.Column(db.String(120), unique=True, nullable=False) 11 | password = db.Column(db.String(255), nullable=False) 12 | status = db.Column(db.String(8), nullable=False) 13 | is_admin = db.Column(db.Boolean(), nullable=False) 14 | 15 | def __init__(self, name, password, email, phone=None, is_admin=False): 16 | self.password = password 17 | self.name = name 18 | self.email = email 19 | self.is_admin = is_admin 20 | self.phone = phone 21 | self.status = 'active' 22 | 23 | @classmethod 24 | def find_by_email(cls, email): 25 | return cls.query.filter_by(email=email).first() 26 | 27 | @classmethod 28 | def find_all_omit_record_with_this_email(cls, email): 29 | return cls.query.filter_by(cls.email != email).all() 30 | 31 | @staticmethod 32 | def generate_hash(password): 33 | return sha256.hash(password) 34 | 35 | @staticmethod 36 | def verify_hash(password, hashed_password): 37 | return sha256.verify(password, hashed_password) 38 | -------------------------------------------------------------------------------- /src/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/routes/__init__.py -------------------------------------------------------------------------------- /src/routes/api.py: -------------------------------------------------------------------------------- 1 | from src.api.resources.auth import Register 2 | 3 | 4 | def routes(api): 5 | api.add_resource(Register, '/signup') 6 | 7 | 8 | def admin_routes(admin_api): 9 | pass 10 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/helpers.py: -------------------------------------------------------------------------------- 1 | def date_to_string(raw_date): 2 | return "{}".format(raw_date) -------------------------------------------------------------------------------- /src/utils/security.py: -------------------------------------------------------------------------------- 1 | from src.models.user import UserModel 2 | from main import jwt 3 | 4 | 5 | def authenticate(email, password): 6 | user = UserModel.find_by_email(email) 7 | if user and UserModel.verify_hash(password, user.password): 8 | return user 9 | 10 | 11 | @jwt.user_identity_loader 12 | def user_identity_lookup(user): 13 | return user.id 14 | 15 | 16 | @jwt.user_lookup_loader 17 | def identity(_jwt_header, jwt_data): 18 | user_id = jwt_data['sub'] 19 | return UserModel.find_by_id(user_id) 20 | -------------------------------------------------------------------------------- /src/utils/validators.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, validate, ValidationError 2 | 3 | 4 | def validate_len(input_field, length): 5 | if len(input_field.strip()) < length: 6 | raise ValidationError( 7 | 'Field must not be less than {} characters long'.format(length) 8 | ) 9 | 10 | 11 | name_field = fields.String( 12 | required=True, 13 | validate=lambda p: validate_len(p, 3), 14 | location='json', 15 | error_messages={'required': 'This field is required'} 16 | ) 17 | 18 | id_field = fields.Int( 19 | required=True, 20 | validate=lambda x: x > 0, 21 | location='query', 22 | error='Field must be greater than 0' 23 | ) 24 | 25 | status_field = fields.String( 26 | required=True, 27 | validate=validate.OneOf(['enabled', 'disabled']), 28 | location='json' 29 | ) 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnsumei/flask-rest-api-setup/632aafa409a2c1d211b0300b0b1683d65bcb5bcb/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | from main import create_app 6 | 7 | 8 | @pytest.fixture 9 | def client(): 10 | test_app = create_app() 11 | db_fd, test_app.config['DATABASE'] = tempfile.mkstemp() 12 | 13 | test_app.config['TESTING'] = True 14 | 15 | client = test_app.test_client() 16 | 17 | yield client 18 | 19 | os.close(db_fd) 20 | os.unlink(test_app.config['DATABASE']) 21 | --------------------------------------------------------------------------------