├── endpoints ├── __init__.py ├── todos │ ├── __init__.py │ ├── model.py │ └── resource.py └── users │ ├── __init__.py │ ├── model.py │ └── resource.py ├── .gitignore ├── settings.py ├── manage.py ├── requirements.txt ├── LICENSE ├── app.py └── README.md /endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /endpoints/todos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ipynb_checkpoints 3 | __pycache__ 4 | .idea 5 | *.pyc 6 | /dist/ 7 | /*.egg-info -------------------------------------------------------------------------------- /endpoints/users/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | users_blueprint = Blueprint('users', __name__) -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | SQLALCHEMY_DATABASE_URI = 'sqlite:///flask.db' 2 | SQLALCHEMY_TRACK_MODIFICATIONS = False 3 | BUNDLE_ERRORS = True 4 | SECRET_KEY = "3j4k5h43kj5hj234b5jh34bk25b5k234j5bk2j3b532" -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager 2 | from flask_migrate import Migrate, MigrateCommand 3 | from app import app, db 4 | 5 | migrate = Migrate(app, db) 6 | 7 | manager = Manager(app) 8 | manager.add_command('db', MigrateCommand) 9 | 10 | if __name__ == '__main__': 11 | manager.run() -------------------------------------------------------------------------------- /endpoints/users/model.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | 3 | 4 | class User(db.Model): 5 | __tablename__ = 'user' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(20)) 9 | 10 | todos = db.relationship('Todo', backref='user', lazy='select') 11 | 12 | def __repr__(self): 13 | return 'Id: {}, name: {}'.format(self.id, self.name) 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.7 2 | aniso8601==2.0.0 3 | click==6.7 4 | Flask==0.12.2 5 | Flask-Migrate==2.1.1 6 | Flask-RESTful==0.3.6 7 | Flask-Script==2.0.6 8 | Flask-SQLAlchemy==2.3.2 9 | itsdangerous==0.24 10 | Jinja2==2.10 11 | jsonschema==2.6.0 12 | Mako==1.0.7 13 | MarkupSafe==1.0 14 | PyJWT==1.5.3 15 | python-dateutil==2.6.1 16 | python-editor==1.0.3 17 | pytz==2018.3 18 | six==1.11.0 19 | SQLAlchemy==1.2.2 20 | Werkzeug==0.14.1 21 | -------------------------------------------------------------------------------- /endpoints/todos/model.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | 3 | 4 | class Todo(db.Model): 5 | __tablename__ = 'todo' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | name = db.Column(db.String(20)) 9 | description = db.Column(db.String(100)) 10 | 11 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), 12 | nullable=False) 13 | 14 | def __repr__(self): 15 | return 'Id: {}, name: {}'.format(self.id, self.name) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tomas Rasymas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | from flask_restful import Api 3 | from flask_sqlalchemy import SQLAlchemy 4 | from werkzeug.exceptions import HTTPException 5 | from werkzeug.exceptions import default_exceptions 6 | import settings 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.errorhandler(Exception) 12 | def handle_error(e): 13 | code = 500 14 | if isinstance(e, HTTPException): 15 | code = e.code 16 | return jsonify(error=str(e)), code 17 | 18 | for ex in default_exceptions: 19 | app.register_error_handler(ex, handle_error) 20 | 21 | 22 | app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI 23 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = settings.SQLALCHEMY_TRACK_MODIFICATIONS 24 | app.config['BUNDLE_ERRORS'] = settings.BUNDLE_ERRORS 25 | 26 | db = SQLAlchemy(app) 27 | api = Api(app) 28 | api.prefix = '/api' 29 | 30 | from endpoints.users.resource import UsersResource 31 | from endpoints.todos.resource import TodosResource 32 | 33 | api.add_resource(UsersResource, '/users', '/users/') 34 | api.add_resource(TodosResource, '/todos', '/todos/') 35 | 36 | if __name__ == '__main__': 37 | app.run() 38 | -------------------------------------------------------------------------------- /endpoints/users/resource.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse, request 2 | from flask_restful import fields, marshal_with, marshal 3 | from .model import User 4 | from app import db 5 | 6 | user_fields = { 7 | 'id': fields.Integer, 8 | 'name': fields.String, 9 | 'todos': fields.List(fields.Nested({'id': fields.Integer, 10 | 'name': fields.String, 11 | 'description': fields.String})), 12 | } 13 | 14 | user_list_fields = { 15 | 'count': fields.Integer, 16 | 'users': fields.List(fields.Nested(user_fields)), 17 | } 18 | 19 | user_post_parser = reqparse.RequestParser() 20 | user_post_parser.add_argument('name', type=str, required=True, location=['json'], 21 | help='name parameter is required') 22 | 23 | 24 | class UsersResource(Resource): 25 | def get(self, user_id=None): 26 | if user_id: 27 | user = User.query.filter_by(id=user_id).first() 28 | return marshal(user, user_fields) 29 | else: 30 | args = request.args.to_dict() 31 | limit = args.get('limit', 0) 32 | offset = args.get('offset', 0) 33 | 34 | args.pop('limit', None) 35 | args.pop('offset', None) 36 | 37 | user = User.query.filter_by(**args).order_by(User.id) 38 | if limit: 39 | user = user.limit(limit) 40 | 41 | if offset: 42 | user = user.offset(offset) 43 | 44 | user = user.all() 45 | 46 | return marshal({ 47 | 'count': len(user), 48 | 'users': [marshal(u, user_fields) for u in user] 49 | }, user_list_fields) 50 | 51 | @marshal_with(user_fields) 52 | def post(self): 53 | args = user_post_parser.parse_args() 54 | 55 | user = User(**args) 56 | db.session.add(user) 57 | db.session.commit() 58 | 59 | return user 60 | 61 | @marshal_with(user_fields) 62 | def put(self, user_id=None): 63 | user = User.query.get(user_id) 64 | 65 | if 'name' in request.json: 66 | user.name = request.json['name'] 67 | 68 | db.session.commit() 69 | return user 70 | 71 | @marshal_with(user_fields) 72 | def delete(self, user_id=None): 73 | user = User.query.get(user_id) 74 | 75 | db.session.delete(user) 76 | db.session.commit() 77 | 78 | return user 79 | -------------------------------------------------------------------------------- /endpoints/todos/resource.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource, reqparse, request 2 | from flask_restful import fields, marshal_with, marshal 3 | from .model import Todo 4 | from app import db 5 | 6 | todo_fields = { 7 | 'id': fields.Integer, 8 | 'name': fields.String, 9 | 'description': fields.String, 10 | 'user_id': fields.Integer 11 | } 12 | 13 | todo_list_fields = { 14 | 'count': fields.Integer, 15 | 'todos': fields.List(fields.Nested(todo_fields)), 16 | } 17 | 18 | todo_post_parser = reqparse.RequestParser() 19 | todo_post_parser.add_argument('name', type=str, required=True, location=['json'], 20 | help='name parameter is required') 21 | todo_post_parser.add_argument('description', type=str, required=True, location=['json'], 22 | help='description parameter is required') 23 | todo_post_parser.add_argument('user_id', type=int, required=True, location=['json'], 24 | help='user_id parameter is required') 25 | 26 | 27 | class TodosResource(Resource): 28 | def get(self, todo_id=None): 29 | if todo_id: 30 | todo = Todo.query.filter_by(id=todo_id).first() 31 | return marshal(todo, todo_fields) 32 | else: 33 | args = request.args.to_dict() 34 | limit = args.get('limit', 0) 35 | offset = args.get('offset', 0) 36 | 37 | args.pop('limit', None) 38 | args.pop('offset', None) 39 | 40 | todo = Todo.query.filter_by(**args).order_by(Todo.id) 41 | if limit: 42 | todo = todo.limit(limit) 43 | 44 | if offset: 45 | todo = todo.offset(offset) 46 | 47 | todo = todo.all() 48 | 49 | return marshal({ 50 | 'count': len(todo), 51 | 'todos': [marshal(t, todo_fields) for t in todo] 52 | }, todo_list_fields) 53 | 54 | @marshal_with(todo_fields) 55 | def post(self): 56 | args = todo_post_parser.parse_args() 57 | 58 | todo = Todo(**args) 59 | db.session.add(todo) 60 | db.session.commit() 61 | 62 | return todo 63 | 64 | @marshal_with(todo_fields) 65 | def put(self, todo_id=None): 66 | todo = Todo.query.get(todo_id) 67 | 68 | if 'name' in request.json: 69 | todo.name = request.json['name'] 70 | 71 | if 'description' in request.json: 72 | todo.description = request.json['description'] 73 | 74 | db.session.commit() 75 | return todo 76 | 77 | @marshal_with(todo_fields) 78 | def delete(self, todo_id=None): 79 | todo = Todo.query.get(todo_id) 80 | 81 | db.session.delete(todo) 82 | db.session.commit() 83 | 84 | return todo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-RESTful API project template 2 | 3 | This project shows one of the possible ways to implement RESTful API server. 4 | 5 | There are implemented two models: User and Todo, one user has many todos. 6 | 7 | Main libraries used: 8 | 1. Flask-Migrate - for handling all database migrations. 9 | 2. Flask-RESTful - restful API library. 10 | 3. Flask-Script - provides support for writing external scripts. 11 | 4. Flask-SQLAlchemy - adds support for SQLAlchemy ORM. 12 | 13 | Project structure: 14 | ``` 15 | . 16 | ├── README.md 17 | ├── app.py 18 | ├── endpoints 19 | │   ├── __init__.py 20 | │   ├── todos 21 | │   │   ├── __init__.py 22 | │   │   ├── model.py 23 | │   │   └── resource.py 24 | │   └── users 25 | │   ├── __init__.py 26 | │   ├── model.py 27 | │   └── resource.py 28 | ├── manage.py 29 | ├── requirements.txt 30 | └── settings.py 31 | ``` 32 | 33 | * endpoints - holds all endpoints. 34 | * app.py - flask application initialization. 35 | * settings.py - all global app settings. 36 | * manage.py - script for managing application (migrations, server execution, etc.) 37 | 38 | ## Running 39 | 40 | 1. Clone repository. 41 | 2. pip install requirements.txt 42 | 3. Run following commands: 43 | 1. python manage.py db init 44 | 2. python manage.py db migrate 45 | 3. python manage.py db upgrade 46 | 4. Start server by running python manage.py runserver 47 | 48 | ## Usage 49 | ### Users endpoint 50 | POST http://127.0.0.1:5000/api/users 51 | 52 | REQUEST 53 | ```json 54 | { 55 | "name": "John John" 56 | } 57 | ``` 58 | RESPONSE 59 | ```json 60 | { 61 | "id": 1, 62 | "name": "John John", 63 | "todos": [] 64 | } 65 | ``` 66 | PUT http://127.0.0.1:5000/api/users/1 67 | 68 | REQUEST 69 | ```json 70 | { 71 | "name": "Smith Smith" 72 | } 73 | ``` 74 | RESPONSE 75 | ```json 76 | { 77 | "id": 1, 78 | "name": "Smith Smith", 79 | "todos": [] 80 | } 81 | ``` 82 | DELETE http://127.0.0.1:5000/api/users/1 83 | 84 | RESPONSE 85 | ```json 86 | { 87 | "id": 3, 88 | "name": "Tom Tom", 89 | "todos": [] 90 | } 91 | ``` 92 | GET http://127.0.0.1:5000/api/users 93 | 94 | RESPONSE 95 | ```json 96 | { 97 | "count": 2, 98 | "users": [ 99 | { 100 | "id": 1, 101 | "name": "John John", 102 | "todos": [ 103 | { 104 | "id": 1, 105 | "name": "First task", 106 | "description": "First task description" 107 | }, 108 | { 109 | "id": 2, 110 | "name": "Second task", 111 | "description": "Second task description" 112 | } 113 | ] 114 | }, 115 | { 116 | "id": 2, 117 | "name": "Smith Smith", 118 | "todos": [] 119 | } 120 | ] 121 | } 122 | ``` 123 | GET http://127.0.0.1:5000/api/users/2 124 | ```json 125 | { 126 | "id": 2, 127 | "name": "Smith Smith", 128 | "todos": [] 129 | } 130 | ``` 131 | GET http://127.0.0.1:5000/api/users?name=John John 132 | ```json 133 | { 134 | "count": 1, 135 | "users": [ 136 | { 137 | "id": 1, 138 | "name": "John John", 139 | "todos": [ 140 | { 141 | "id": 1, 142 | "name": "First task", 143 | "description": "First task description" 144 | }, 145 | { 146 | "id": 2, 147 | "name": "Second task", 148 | "description": "Second task description" 149 | } 150 | ] 151 | } 152 | ] 153 | } 154 | ``` 155 | GET http://127.0.0.1:5000/api/users?limit=1&offset=1 156 | ```json 157 | { 158 | "count": 1, 159 | "users": [ 160 | { 161 | "id": 2, 162 | "name": "Smith Smith", 163 | "todos": [] 164 | } 165 | ] 166 | } 167 | ``` 168 | 169 | Todo endpoint is similar to Users endpoint. --------------------------------------------------------------------------------