← Home

The Concise Guide to Building Flask APIs

Manohar Vanga
Manohar Vanga

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:

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 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).


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.


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


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 (and DEBUG) through the FLASK_ENV variable (and FLASK_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__)

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

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



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.


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 the MethodView section later in this guide.

Below are common examples of the flask.Flask.route() decorator.

Default Route

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

def hello():
    return 'Hello World!', 200
def hello():
    return 'Hello World!', {'X-Token': 'abc123'}
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

def hello(name):
    return 'Hello {}!'.format(name)
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

def hello():
    return {'message': 'Hello World!'}
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

def hello():
    return 'Hello World'
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

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

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
            # 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=['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

def download_trusted_path():
    return send_file(open('file.txt', 'rb'),
from flask import send_from_directory

def download_untrusted_path(filename):
    return send_from_directory(
                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 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__)

def api_hello():
    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

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


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

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

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')

def app_cli_hello():
    print('Hello World!')

@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


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

Logging Exceptions

    logging.exception('Got exception')

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 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 %}


# 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'
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)

        data = RequestSchema().load(request.get_json(force=True))
    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):
            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()

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()

def set_secure_headers(response):
    return response


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()'