The Concise Guide to Building Flask APIs
This is a concise, no-nonsense guide to the parts of Flask that are relevant to building APIs. It is intended as an opinionated reference, and covers most of Flask’s surface area that I’ve had to use over the years across dozens of side projects. It is not intended as a beginner Flask tutorial or comprehensive documentation of Flask functionality, but rather occupies a middle ground between them.
Prerequisites: I’m assuming you are well-versed in Python and have some experience with Flask.
The below topics ARE covered in this guide:
- Setup
- App Structure
- Configuration
- Routes
- CRUD Routes
- File Downloads
- The Request Object
- Blueprints
- App-Specific CLI
- Logging
- Templates
- API Responses
- Data Validation
- Browser Security
- CORS Setup
- Deploying
This guide does NOT cover the following:
- Anything pertaining to non-API Flask apps, which includes templates, sessions and form support in Flask. This guide specifically assumes you’re going to be developing the frontend and backend separately (generally a good idea).
- Database setup and data modelling, which is a vast enough topic to deserve its own guide, probably in the near future.
- Common API functionality. This includes authentication, authorization, dealing with payments and subscriptions and so on. These features, while essential to many APIs today, sit one layer above the topics covered in this guide. These will likely be the subject of future guides.
Setup
Installation
Setup and initialize Flask with the following three commands:
$ python3 -m venv .venv
$ . .venv/bin/activate
$ pip install flask
Since Python 3.3 the venv
standard library module lets you create isolated virtual environments. You don’t need to install an external tool like virtualenv
(which may be familiar to users of older versions of Python).
I like to create the virtual environment in a hidden folder called .venv
so that it’s not part of directory listings. It also goes into my .gitignore
so it stays out of my source repository.
I use the .venv
pattern so often that I have the following alias in my .bash_profile
:
alias venv='python3 -m venv .venv'
Development Server
Flask comes with a development server built in. To start it, just type:
$ flask run
The Flask dev server looks to the FLASK_APP
variable to find your app, which you can optionally set (see table below).
Setting FLASK_APP
The FLASK_APP
environment variable is used by the Flask development server to locate your application instance. That is, to find an instance of the flask.Flask
class to serve. It can get a little confusing so below is a quick reference on how to set it.
To make things easier for you, Flask goes through a sequence of steps to try and automatically find your Flask app instance. That means, if you structure your app in a way that Flask expects, you don’t need to bother setting up the FLASK_APP
environment variable.
The format of FLASK_APP
is as follows:
export FLASK_APP=<path/><module><instance><(args)>
Field | Description |
---|---|
path | The directory to cd into before searching. If unspecified, it defaults to the current directory. Must end with a slash. |
module | The Python module to import. This can be a file module (e.g., myapp for myapp.py ). If unspecified, it searches, by default, for files named app.py and wsgi.py . |
instance | The Flask instance to use. This can be the name of a flask.Flask instance variable or a function that returns one. If unspecified, Flask searches in order for: variables named app , application and finally any other variables of type Flask . Following that, it searches for functions called create_app() and make_app() . |
args | If the specified instance is a function, you can pass it arguments here |
The application structures presented later in this document adhere to these conventions so do not require explicitly setting FLASK_APP
. For example, using a file called app.py
and initializing Flask within a create_app()
app factory function within will not require setting the FLASK_APP
variable.
Setting FLASK_ENV
The other useful variable to set is FLASK_ENV
, which typically takes the values 'development'
or 'production'
. You can use your own ones as well (e.g., 'staging'
).
The only effect this has on a normal Flask application is toggling debugging mode, which is enabled for 'development'
and disabled for 'production'
.
Debugging mode enables the interactive debugger and automatic file reloader.
App Structure
Structuring Flask apps is where things start to get opinionated. I’ve found that I use one of three basic app structures.
- Single file template: all code lives in a single
app.py
file. This includes initialization, configuration, routes/blueprints, models, app-specific CLI functions, and extensions. I find this structure very useful for small demos and apps with limited scope. - Single module template: all code lives inside a Python module. Initialization and configuration go into
__init__.py
, and blueprints, models, CLI functions, and extensions each live in their own files. I use this structure for MVPs and demos as it’s slightly more manageable than the single-file structure.
You can also have a large application template where the application is structured as a module, but is split by functionality into sub-modules. The initialization and configuration go into the main module’s __init__.py
, but each functionality (e.g., authentication, application) lives in its own sub-module. Each sub-module is structured like the single module template above, with its own blueprints, models and CLI functions, which get attached to the main instance during initialization.
If you’re wondering whether you need a large application structure, you don’t. You should use the single module template or even the single file template first. At the point you really do need to split things off into sub-modules, it should be fairly easy to convert either of the provided templates. Furthermore, at that point you should have enough experience with Flask to not need an opinionated template from me. As a result, I’ve omitted it from this guide.
For both the above templates, clone the repository, create a new virtual environment and run the following command to install dependencies:
$ pip install -r requirements.txt
Configuration
Every Flask instance can be configured via its config
property:
app = Flask(__name__)
app.config['CONFIG_NAME'] = 'Config Value'
Flask has a few special configuration variables that affect its behaviour. These can be accessed directly via the config
property as show above, or via special property names for each, shown below:
As Key | As property | Description |
---|---|---|
ENV |
app.env |
Specifies the current environment ('development' or 'production' ). Loaded from the FLASK_ENV environment variable. Specifies the current environment ('production' or 'development' ). |
DEBUG |
app.debug |
Specifies whether debugging output is enabled. Typically not set manually but automatically set by the environment (True for 'development' environment, False for 'production' ). Can be overridden using the FLASK_DEBUG environment variable |
SECRET_KEY |
app.secret_key |
Secret key used for anything that requires encryption. Loaded via the SECRET_KEY environment variable. This should never be hardcoded. This should never be exposed. |
TESTING |
app.testing |
Specifies whether testing mode is enabled. Activated automatically when running tests. |
Some notes on the above:
- Always set
ENV
(andDEBUG
) through theFLASK_ENV
variable (andFLASK_DEBUG
if overriding default behaviour). It’s a bad idea to try and set them in code as it won’t be visible to startup logic, which may not initialize correctly. - With vanilla Flask, the only functionality that uses
SECRET_KEY
is session support. However, 3rd party Flask extensions may use it as well, so it’s a good idea to always set it. Leaking this value basically prevents your application from differentiating between attackers and legitimate users. - Setting
TESTING
disables error catching during request handling, so that you get better error reports when performing test requests against the application.
Generating Secure Secret Keys
You can generate a strong secret on the command line using Python’s secrets
module:
python3 -c 'import secrets; print(secrets.token_hex(32))'
The above command generates a cryptographically strong random 32-byte string using the most secure source of randomness that your OS provides.
Configuring Flask Instances
There are a couple of ways to set configuration values in Flask. Three are outlined below. Approach 1 is the one I use most frequently and is also the simplest. Approach 2 is what I use for more complex projects. I’ve used approach 3 a few times as a transition from approach 1 to 3.
Approach 1: By direcly setting values in app.config
app = Flask(__name__)
app.config['CONFIG_NAME'] = 'Config Value'
This is the simplest approach, and one I use for the majority of projects. Just set configuration values directly during initialization by accessing app.config
.
Approach 2: Using objects (which can be further subclassed, one for each environment)
# in config.py
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite:///:memory:'
# In app.py
app = Flask(__name__)
app.config.from_object('config.Config')
This approach is the most flexible and allows for easily configuring flask instances for different environments (through sub-classing). However, I only reach for this when building moderately complex apps.
Approach 3: Via an environment variable pointing to config module
# Set some environment variable to the path of your config file
export ENV_VAR_NAME='/path/to/config.py'
app = Flask(__name__)
# Now tell flask to load the config from that file
app.config.from_envvar('ENV_VAR_NAME')
This approach is useful for splitting out configuration into a separate file. I’ve only used this one rarely.
One useful trick, in general, is updating the configuration values for a previously-configured Flask instance (by using configuration names as parameters):
# app is a previously configured Flask instance
app.config.update(
TESTING=True,
)
Routes
Decorators
Decorator | Description |
---|---|
@app.route() |
Registers the decorated function as a handler for a specified URL rule. |
@app.before_request() |
Registers the decorated function to run prior to handling any request. |
@app.after_request() |
Registers the decorated function to run after the route handler has generated a response. |
@app.errorhandler(http_error_code) |
Registers the decorated function to be called when a particular HTTP error occurs, as specified by the http_error_code . |
Routes
I typically use two ways to define rules for the routing system:
- The
flask.Flask.route()
decorator. This is what you’ll use 99.9% of the time. - The
flask.Flask.add_url_rule()
function. This is useful for registering class-based views, as we’ll see in theMethodView
section later in this guide.
Below are common examples of the flask.Flask.route()
decorator.
Default Route
@app.route('/hello')
def hello():
return 'Hello World!'
The default route only accepts GET
requests.
When returning a string as shown above, Flask will wrap it into a response object for you. You can send raw HTML this way.
Speciying Request Types
@app.route('/hello', methods=['GET', 'POST']))
def hello():
return 'Hello World!'
Above, the methods
parameter to the route()
decorator specifies the HTTP methods that the function can receive (GET
and POST
above).
Returning HTTP Status Codes and Custom Headers
@app.route('/hello')
def hello():
return 'Hello World!', 200
@app.route('/hello')
def hello():
return 'Hello World!', {'X-Token': 'abc123'}
@app.route('/hello')
def hello():
return 'Hello World!', 200, {'X-Token': 'abc123'}
The above three examples return tuples. Three scenarios are automatically handled by Flask:
- If the return value is of type
(str, int)
, the second part is assumed to be the HTTP status code to return. (In the previous examples, where no status code is specified, a 200 status code is returned.) - If the return value is of type
(str, dict)
, the second part is assumed to be a dictionary of custom headers to set in the response. - If the return value is of type
(str, int, dict)
, the third part is assumed to be a dictionary of custom headers to set in the response.
URL Variables
@app.route('/hello/<name>')
def hello(name):
return 'Hello {}!'.format(name)
@app.route('/hello/<int:age>')
def hello(age):
return "You’re {} years old!".format(age)
You can add URL variables with the <variable_name>
syntax. Your function then receives the <variable_name>
as a keyword argument. Optionally, you can specify the type of the argument using the syntax <converter:variable_name>
.
Converter Type | Description |
---|---|
string (default) | accepts any text without a slash |
int | accepts positive integers |
float | accepts positive floating point values |
path | like string but also accepts slashes |
uuid | accepts UUID strings |
Using a URL with a particular type, say /hello/<int:age>
, but providing a different type (e.g., a string as in /hello/Alice
) will not throw any errors, but rather just return a 404 (not found) error.
Return JSON
@app.route('/hello')
def hello():
return {'message': 'Hello World!'}
@app.route('/hello')
def hello():
return jsonify(['Hello', 'World'])
The two examples above are ways to return JSON. I personally prefer the first approach, where you just return a dict
. Flask handles the conversion to JSON and wrapping it into a request object.
The jsonify()
function is a helper that basically does the same thing: serialize to JSON, and then wrap it into a request object. This is useful if your JSON response is not a dict
(e.g., a list
).
HTTP Method-Specific Handlers
@app.get('/hello')
def hello():
return 'Hello World'
@app.post('/hello')
def hello():
return 'Hello World'
Starting with Flask 2.0.0, you can now use new decorators for common HTTP methods as shown above. The above syntax is logically equivalent to: @app.post('/hello', methods=['GET'])
and @app.post('/hello', methods=['POST'])
. Just cleaner.
Maintenance Mode
import os
@app.before_request
def check_under_maintenance():
if os.path.exists("maintenance"): # Check if a "maintenance" file exists (whatever it is empty or not)
abort(503) # No need to worry about the current URL, redirection, etc
@app.errorhandler(503)
def error_503(error):
return {'error': 'Currently in maintenance'}, 503
CRUD Routes
A common pattern I use for standard create-read-update-destroy (CRUD) operations on data is based on the MethodView
class. A complete boilerplate for CRUD operations is shown below:
from flask.views import MethodView
class UserAPI(MethodView):
def get(self, user_id):
if user_id is None:
# return a list of users
else:
# expose a single user
def post(self):
# create a new user
def delete(self, user_id):
# delete a single user
def put(self, user_id):
# update a single user
def patch(self, user_id):
# update a single user
user_view = UserAPI.as_view('user_api')
app.add_url_rule('/users', view_func=user_view,
methods=['GET'], defaults={'user_id': None})
app.add_url_rule('/users', view_func=user_view,
methods=['POST'])
app.add_url_rule('/users/<int:user_id>',view_func=user_view,
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
Above, we subclass Flask’s flask.views.MethodView
class, override the functions for each operation we care about, and finally use flask.Flask.add_url_rule
to add routing rules, which result in the following:
Route | Description |
---|---|
GET /users |
Get a list of all users |
POST /users |
Create a new user |
GET /users/<int:user_id> |
Get a specific user, by id |
PUT /users/<int:user_id> |
Replace a specific user, by id |
PATCH /users/<int:user_id> |
Update a specific user, by id |
DELETE /users/<int:user_id> |
Delete a specific user, by id |
Notice that the UserAPI.get()
function above serves as a handler for both /users
and /users/<int:user_id>
. This is done by differentiating between the two within the function by checking if the user_id
keyword is None
.
File Downloads
Two useful functions that I use often are for sending files using Flask.
from flask import send_file
@app.route('/download')
def download_trusted_path():
return send_file(open('file.txt', 'rb'),
mimetype="text/plain",
as_attachment=True,
download_name="report.txt")
from flask import send_from_directory
@app.route('/download/<path:filename>')
def download_untrusted_path(filename):
return send_from_directory(
app.config['UPLOAD_FOLDER'],
filename, as_attachment=True)
The first snippet above uses the send_file()
function to send a plaintext file (file.txt
) as an attachment with the name (report.txt
). The send_file()
function should not be used with untrusted user-provided paths.
For that, you can use the second snippet above that uses send_from_directory()
to look for an untrusted path within a prefixed folder that you specify.
The Request Object
The most useful fields of the request
object are shown below:
request.method # The request type (HTTP verb)
request.args.get('key', None) # ?key=value
request.form['name'] # Form data
request.cookies.get('cookie_name') # Cookies
request.files['file1'] # Form with enctype="multipart/form-data"
Blueprints
Blueprints are like modular apps that can be attached to the main Flask app at a specified endpoint. For example, an api_v1
blueprint that contains the routes for v1 of your API, and is mounted with a prefix of /api/v1/
.
Blueprints are how I organize all my routes. Even in the simplest examples, I prefer to keep my routes organized under a blueprint in case I want to modularize the functionality down the line.
Creating a Blueprint
from flask import Blueprint, current_app
api = Blueprint('api', __name__)
@api.route('/hello')
def api_hello():
print(current_app.config['TEST'])
return {'message': 'Hello World!'}
First, notice how the decorator used above is api.route()
rather than the typical app.route()
.
Second, we don’t directly access our Flask instance directly (e.g., to access a config variable above), but rather do it through the Flask-provided proxy called current_app
(which is only available during a request).
Registering a Blueprint
from flask import Flask
from yourapp.api import api
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api/v1')
The register_blueprint()
function takes a url_prefix
parameter that mounts it at the right point. Our example /hello
handler shown above is made available at /api/v1/hello
.
Blueprint-Specific Error Handling
@api.errorhandler(404)
def api_route_not_found(e):
return {'error': 'Invalid endpoint'}
Blueprints can handle their own errors by decorating a function with the flask.Blueprint.errorhandler()
decorator as shown above. It takes an HTTP error code as an argument.
Nested Blueprints
api.register_blueprint(another_blueprint)
Starting with Flask 2.0.0, you can now register blueprints in a nested manner using the register_blueprint()
function within a flask.Blueprint
instance.
Flask Shell
The flask shell
command drops you into an interactive Python session with your Flask app loaded.
One common piece of functionality I always add is to import some variables so I can use them in my shell session easily. This can be done as follows:
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
We use the flask.Flask.shell_context_processor()
decorator to register a function that is invoked when the shell is started. Its return value is a dict
that is loaded into the environment.
We can now access the db
variable or the User
and Post
models in our shell without having to import them:
$ flask shell
>>> db
<SQLAlchemy engine=sqlite:///:memory:>
The FLASK_APP
environment variable must be set to make shell context processors work correctly.
App-Specific CLI
I never implement admin UIs for my projects. For one, it takes me quite some effort to build frontends and these quickly become opportunities for procrastination. Second, you have to spend time securing it as it’s as accessible as your application.
Instead, what I prefer to do is build app-specific command-line interfaces for useful tasks. Need to run some admin tasks? That’s a command. Need to retrieve statistics from the database? That’s another one. Need to refund a particular order? That’s a command as well.
Registering Top-Level Commands
import click
@app.cli.command('create-user')
@click.argument('name')
def cli_create_user(name):
print('Create a user here')
Flask makes it exceptionally easy to build out custom commands using Click.
We use the flask.Flask.cli.command()
decorator above and specify the name of the command (create-user
). We use click
’s decorator to specify an argument for our command. We can now run it using the following command:
flask create-user <name>
Registering App-Specific Commands
from flask.cli import AppGroup
app_cli = AppGroup('myapp')
app.cli.add_command(app_cli)
@app_cli.command('hello')
def app_cli_hello():
print('Hello World!')
@app_cli.command('hello-name')
@click.option('--name', required=True)
def app_cli_hello_with_name(name):
print('Hello {}!'.format(name))
We can also create sub-commands under which all our app-specific commands live.
We simply create a flask.cli.AppGroup
object and register it using app.cli.add_command()
. We then use the app_cli.command()
decorator inside our AppGroup
to register our commands.
The example commands above can now be executed as follows:
flask myapp hello
flask myapp hello-name --name Alice
Logging
I used to use print()
to debug my applications for the longest time, but have moved away from it towards Python’s standard logging
module.
The logging
module unifies logging output for all Python libraries. The benefit of using this standard logging API is that your application log can include your own messages integrated with messages from third-party modules.
Flask instances come pre-configured with a logger for you to write logs to:
app.logger.debug('A debug message')
app.logger.info('Some info')
app.logger.warning('A dire warning')
app.logger.error('Something went wrong')
app.logger.critical('Something really went wrong')
The easiest way to get started, and one that still solves approximately 100% of my use-cases is to use the convenience functions debug()
, info()
, warning()
, error()
and critical()
(documentation can be found here).
You can set the log level at which logs are emitted to standard output via the setLevel()
function:
import logging
app.logger.setLevel(logging.ERROR)
Logging Exceptions
try:
try_to_do_stuff()
except:
logging.exception('Got exception')
raise
For logging exceptions, you can use logging.exception()
from within an except
block. This will log the current exception along with the trace information, and prepend it with a user-specified message.
For more complex use-cases, refer to the documentation.
Templates
Templates are typically not used in APIs for generating responses. However, I’ve found them very handy for formatting templated emails to send to users. This allows for using a more expressive template language for describing your emails, and using the render_template()
method to generate the output that gets sent.
from flask import render_template
render_template('template.html', key1=val1,...)
Below is a quick reference of the most useful parts of Jinja2, which is Flask’s templating engine.
Including Templates
# Can include one template inside another
{% include 'anotherfile.html' %}
Extending Base Templates with Blocks
# Can define named blocks (block with name of 'content' below)
{ block content }{ endblock }
# Extend a template with named blocks
{% extends 'base.html' %}
# Fill them out as shown below:
{ block content }
Cool content here
{ endblock }
For Loops
# For loops over lists passed to render_template()
{% for user in users %}
<li>{{ user }}</li>
{% endfor %}
# A for-else construct for cases where lists may be empty
{% for user in users %}
<li>{{ user.username|e }}</li>
{% else %}
<li><em>no users found</em></li>
{% endfor %}
Conditional Rendering
# If-else for conditional rendering
{% if user.admin %}
<li>Logged in as user</span>
{% else %}
<li>Not logged in</a>
{% endif %}
Filters
# Escape untrusted content with the 'e' filter
{{ content|e }}
# A block filter that applies to the entire block
{% filter upper %}
uppercase me
{% endfilter %}
# A custom filter function called 'make_caps'
@app.template_filter('make_caps')
def caps(text):
return text.uppercase()
# Using our custom filter function
{{ content|make_caps }}
API Responses
There are a bunch of standards out there for structuring API responses. For simple apps, I tend to use the JSend specification. It can be summarized by the table below:
Type | Description | Required Keys | Optional Keys |
---|---|---|---|
success | All went well, and (usually) some data was returned. | status, data | |
fail | There was a problem with the data submitted, or some pre-condition of the API call wasn’t satisfied | status, data | |
error | An error occurred in processing the request, i.e. an exception was thrown | status, message | code, data |
I recommend reading the entire (short) specification in the linked repository.
When designing simple APIs with Flask, I combine the JSend response format with the appropriate HTTP error code for the situation using the below helper functions.
def api_response(status, data=None, message=None, code=None, http_code=200):
"""Build and return a JSON API response in the JSend format"""
ret = {'status': status}
ret['data'] = data
if message is not None:
ret['message'] = message
if code is not None:
ret['code'] = code
return ret, code
def api_success(data=None, http_code=200):
"""Returns a 'success' JSend response."""
return api_response('success', data=data, http_code=http_code)
def api_fail(data, http_code=400):
"""Returns a 'fail' JSend response."""
return api_response('fail', data=data, http_code=http_code)
def api_error(message, data=None, code=None, http_code=500):
"""Returns an 'error' JSend response."""
return api_response('error', data, message, code, http_code)
Here’s an example function for checking if a given username exists or not:
@api_v1.route('/users/<string:username>', methods=['GET'])
def user_get(username):
"""Check if a given username exists or not."""
if User.find_by_username(username) is not None:
return api_success() # Return empty success response
return api_fail({'username': 'No such user'}) # Invalid username
Data Validation
Over the years, I’ve come to use Marshmallow for validating user-provided data and serializing response data.
Validating Request Data
To use Marshmallow, you first define a schema class. Then you can validate
input data by instantiating it and calling its load()
method. Here is an example
of validating user-provided JSON for a login route:
import marshmallow
@api.route('/api/v1/sessions', methods=['POST'])
def session_create():
class RequestSchema(marshmallow.Schema):
user_or_email = marshmallow.fields.Str(required=True)
password = marshmallow.fields.Str(required=True)
try:
data = RequestSchema().load(request.get_json(force=True))
...
api_success(token)
except ValidationError as e:
api_fail(e.messages, http_status_code=e.status_code)
For validating request data, I like to define schema classes within the function itself. It keeps the validation logic close to where it’s used and makes it easy to read months later.
Serializing Response Data
For output data, which is more standardized for the entire app, I use create
global schema classes that I import as needed. To output data, you call the
dump()
method of a schema instance.
import marshmallow
class UserPublicSchema(marshmallow.Schema):
username = marshmallow.fields.Str()
profile_image = marshmallow.fields.Str()
name = marshmallow.fields.Str()
bio = marshmallow.fields.Str()
@api.route('/api/v1/users/<string:username>/profile', methods=['GET'])
def user_profile(username):
user = User.find_by_username(username)
if user is None:
return api_fail()
return api_success(UserPublicSchema().dump(user))
For a more comprehensive overview of Marshmallow field types take a look at the documentation
Custom Fields (Methods)
Custom fields can be computed on the fly by using the marshmallow.fields.Method()
type.
You provide it with the name of the function that returns the computed value.
import marshmallow
import datetime
class UserSchema(Schema):
...
last_login = marshmallow.fields.DateTime()
since_logged_in = marshmallow.fields.Method("get_days_since_logged_in")
def get_days_since_logged_in(self, user):
return datetime.datetime.now().day - user.last_login.day
Custom Fields (Subclassing marshmallow.fields.Field
)
To implement a custom field type, just subclass marshmallow.fields.Field
and provide
implementations for the _serialize()
and _deserialize()
methods, and raise a
marshmallow.ValidationError
if something goes wrong.
class PinCode(fields.Field):
"""Field that serializes to a string of numbers and deserializes
to a list of numbers.
"""
def _serialize(self, value, attr, obj, **kwargs):
if value is None:
return ""
return "".join(str(d) for d in value)
def _deserialize(self, value, attr, data, **kwargs):
try:
return [int(c) for c in value]
except ValueError as error:
raise ValidationError("Pin codes must contain only digits.") from error
More information on custom fields can be found here.
CORS Setup
Cross-Origin Resource Sharing needs to be setup any time you have the backend and frontend on different domains, a common practice in modern web application development (e.g., Flask API running on https://api.example.com and the web frontend being served from https://www.example.com).
The insidious bit is that you never get hit with CORS errors until you’re in production, at which point you’re scrambling to fix things.
I highly recommend carefully reading and understanding the entire MDN article on CORS.
Essentially, you need to ensure your Flask server sets certain headers when responding to API requests from a set of trusted origins. This allows users’ (CORS-respecting) browsers to allow that data to go through to the web application. Otherwise, it is blocked by the browser.
Luckily, understanding CORS is the hard part. Implementing it is fairly trivial using the Flask-CORS
extension.
pip install flask_cors
from flask_cors import CORS
cors = CORS()
cors.init_app(app)
Browser Security
To secure your API, there are certain HTTP response headers that you can set to increase the security of your application. Once set, these HTTP response headers prevent vulnerabilities in most modern browsers. You can find more information at the OWASP Secure Headers Project.
From a more practical perspective, you can just install secure.py and enable it for Flask with the below code:
import secure
secure_headers = secure.Secure()
@app.after_request
def set_secure_headers(response):
secure_headers.framework.flask(response)
return response
Deploying
In production, you don’t want to use Flask’s built-in dev server. Use Gunicorn instead. Gunicorn is a WSGI HTTP Server for POSIX environments.
Running a Flask application with Gunicorn requires specifying the Flask instance, similar to how we setup FLASK_APP
.
The format for specifying the app instance is: $(MODULE_NAME):$(VARIABLE_NAME)
. This can be a variable or an app factory function like create_app()
:
gunicorn --workers=4 'myproject:app'
gunicorn --workers=4 'myproject:create_app()'
Procfile for Deploying to Heroku
I use Heroku for 99% of my projects as it makes ops a breeze. I highly recommend doing the same if you’re working solo or with a small team. Even more so if you’re prototyping a product, where spending time on ops may take you away from higher priority tasks.
For deploying a Flask application to Heroku, you just provide a Procfile
with a command for the web
type:
$ cat Procfile
web: gunicorn --workers=4 'wsgi:create_app()'