BUILDING BEAUTIFUL REST APIs
with Flask, Swagger UI and Flask-RESTPlus
Michał Karzyński • EuroPython 2016
ABOUT ME
• My name is Michał Karzyński (that’s Polish for Mike)
• I blog at http://michal.karzynski.pl
Short URL: karzyn.com
• I wrote a book for Linux admins, I write code in Python and JavaScript
• I’m the tech lead of a Web UI team at
WHAT IS A WEB API?
Web (JavaScript)
API
(JSON)
Server
(Python)
Phone (Swift, Java)
WHAT IS A REST API?
REPRESENTATIONAL STATE TRANSFER
A clever way to use HTTP to build APIs.
ANATOMY OF HTTP
Method Path Query Status Code
Headers Headers
Body Body
Request Response
GET ?search=Moby Dick
POST
PUT /api/books Cookies… 200 OK
DELETE
404 Not Found
Method Path Query Status Code
Headers Headers
Body Body
Request JSON Response
Method Path Query
Headers
REST CONVENTIONS Body
GET PUT POST DELETE
Collection
List books New book
/books
Item
Display book Update book Delete book
/books/123
Controller Borrow book
/books/123/borrow
FLASK
flask.pocoo.org
FLASK-RESTPlus
• define and document endpoints
• validate input
• format output (as JSON)
• turn Python exceptions into HTTP responses
• minimise boilerplate code
flask-restplus.rtfd.io
• generate interactive documentation (Swagger UI)
Demo
OPEN API FORMAT
OPEN API SPECIFICATION
• Standard language to describe REST APIs
• Open source (Linux Foundation)
• Tools:
• Swagger UI
• Swagger Editor
• Code generators
• Initiative with many powerful members
openapis.org swagger.io
OPEN API SPECIFICATION
• Standard language to describe REST APIs
• Open source (Linux Foundation)
• Tools:
• Swagger UI
• Swagger Editor
• Code generators
• Initiative with many powerful members
openapis.org swagger.io
Method Path Query
Headers
Body
Request
Method Path Query
REQUEST METHOD Headers
POST /api/books/123/borrow?when=today Body
from flask_restplus import Resource
@api.route('/<int:id>/borrow')
class BorrowBookController(Resource):
def post(self, id):
""" Borrow book from library.
Allows the current user to borrow
the book out of the library.
"""
...
return {'message': 'OK'}
Method Path Query
REQUEST METHOD Headers
POST /api/books/123/borrow?when=today Body
from flask_restplus import Resource
@api.route('/<int:id>/borrow')
class BorrowBookController(Resource):
class Resource:
def post(self, id):
""" Borrow book from library.
def get(self)...
def post(self)...
Allows the current user to borrow def put(self)...
the book out of the library.
def delete(self)...
"""
def patch(self)...
... def options(self)...
return {'message': 'OK'} def head(self)...
Method Path Query
REQUEST METHOD Headers
POST /api/books/123/borrow?when=today Body
Method Path Query
REQUEST METHOD Headers
POST /api/books/123/borrow?when=today Body
Method Path Query
REQUEST PATH Headers
POST /api/books/123/borrow?when=today Body
@api.route(‘/books/<int:id>/borrow’)
@api.route('/articles/<title>')
@api.route('/wiki/<path:wikipage>')
@api.route('/values/<float:value>')
@api.route('/object/<uuid:identifier>')
Method Path Query
REQUEST PATH Headers
GET /api/book/123/borrow?when=today Body
Method Path Query
QUERY ARGUMENTS Headers
GET /api/books?page=1&per_page=10 Body
from flask_restplus import reqparse
pagination = reqparse.RequestParser()
pagination.add_argument('page', type=int, required=False,
default=1, help='Page number')
pagination.add_argument('per_page', type=int, required=False,
choices=[10, 20, 30, 40, 50])
Method Path Query
QUERY ARGUMENTS Headers
GET /api/books?page=1&per_page=10 Body
from flask import request
from flask_restplus import Resource
@api.route('/')
class PostsCollection(Resource):
@api.expect(parsers.pagination)
def get(self):
args = pagination_arguments.parse_args(request)
page = args.get('page', 1)
per_page = args.get('per_page', 10)
...
Method Path Query
QUERY ARGUMENTS Headers
GET /api/books?page=1&per_page=10 Body
Method Path Query
REQUEST BODY (JSON)
Headers
API MODELS Body
blog_post = api.model('Blog post', {
'title': fields.String(description='Article title'),
'body': fields.String(description='Article content'),
'pub_date': fields.DateTime,
'category_id': fields.Integer(min=1),
})
@api.expect(blog_post)
def post(self):
...
Method Path Query
REQUEST BODY (JSON)
Headers
API MODELS Body
Method Path Query
API MODELS
Headers
INHERITANCE AND NESTING Body
category = api.model('Blog category', {
'id': fields.Integer(description='The unique id of category'),
'name': fields.String(description='Category name'),
})
category_with_posts = api.inherit('Blog category with posts', category,
{
'posts': fields.List(fields.Nested(blog_post))
})
Status Code
Headers
Body
Response
Status Code
Headers
RESPONSE STATUS CODE Body
@api.route('/<int:id>')
@api.response(404, 'Post not found.')
class PostItem(Resource):
@api.response(204, 'Post successfully deleted.')
def delete(self, id):
"""
Deletes blog post.
"""
delete_post(id)
return None, 204
Status Code
Headers
RESPONSE STATUS CODE Body
Status Code
Headers
RESPONSE BODY (JSON) Body
blog_post = api.model('Blog post', {
...
'category_id': fields.Integer(attribute='category.id'),
'name': fields.String(attribute=lambda x: x._private_name),
})
@api.marshal_with(blog_post)
@api.marshal_list_with(blog_post)
def get(self):
def get(self):
... ...
EXCEPTION HANDLING
from sqlalchemy.orm.exc import NoResultFound
@api.errorhandler(NoResultFound)
def database_not_found_error_handler(e):
log.warning(traceback.format_exc())
return {'message': 'A database result was not found.'}, 404
INERACTIVE
DEBUGGER
from flask import Flask
from flask_restplus import Resource, Api
app = Flask(__name__)
api = Api(app)
@api.route('/hello')
class HelloWorld(Resource):
def get(self):
return {'hello': 'world'}
if __name__ == '__main__':
app.run(debug=True)
Demo code and article available on my blog:
karzyn.com
THANK YOU