(This blog post is a work in progress.)
In the last installment, we discussed some ways in which front-end developers can protect their API-keys. We came to the conclusion that to truly protect your API-keys, you will need a designated back-end — a proxy server — to handle your API calls.
In this example we will be using Flask to create such an application, though the general concept can be easily applied by developers of any stack. The idea is that our front-end application will send a request to our Flask app. Then our app, based on the request from the client, will send a request to the API. In doing so, we no longer need an API-key to be present in the client request since it will be handled by our back-end.
Table of Contents
Prerequisites
- an API you're interested in and any necessary keys. Here is a big list of APIs
- Basic command-line skills
- Git/Github
My goal is to make this tutorial accessible to front-end developers with limited back-end and/or Python experience. Luckily, Python syntax is very semantic and intuitive, so hopefully developers from other stacks will have no problem following along with the examples used in this post.
If you're a back-end developer, this is not for you. You know what to do already. This is for our beginner front-end developers who want to hide their keys, nothing more.
Git/Github
At some point in this project, you will need to create a git repo. At what point in the application you integrate version-control is up to you, but having basic git skills is needed for when we host the finished-product on Heroku.
What We're Making
Here is Github repo for what we're building.
We're are building a very minimal Flask app. We don't need a database or authentication, we just need a server to make requests to our API and pass JSON to our React/Redux app. There will be vulnerabilities in this app, but your API-key will be safe and others will take note of your effort to keep it secret (hopefully).
If you're looking for an in-depth introduction to Flask, Miguel Ginberg's Flask mega tutorial is excellent.
Environments
For any Python project, regardless of it's size or complexity, it's a good idea to create a virtual environment. This ensures that as libraries update and backwards-compatability becomes an issue, your app will still work.
Create an Environment
When it comes to Python, I'm an Anaconda person. Anconda is basically Python but it comes bundled with a lot of data-science libraries and other features like Jupyter Notebooks. I suggest you give it a try.
Find the download instructions for your OS and install Anaconda. If you don't use Anaconda, here are the instructions for creating a virtualenv with pip. We will only being using conda
, the Anaconda CLI, for creating our environment and downloading pip
. After that, we will use pip
for everything else.
Now that that's out of the way, go ahead and create an environment for your Flask app using conda
:
C:\> conda create --name flaskenv
This will create a virtual environment named flaskenv
.
To see a list of your virtual environments, run the following:
C:\> conda info --env
This will print a list of your environments to the console. Learn more about managing environments with conda.
Activating an Environment
I will demonstrate how to activate a Python environment on a Windows OS, simply because Windows users have it hard enough as it is.
I love PowerShell, but when it comes to Python virtual-environments you'll want to use the command prompt. We have a lot to cover in this post, so I won't go in-depth as to why we are using the latter versus the former, though I encourage you to play around with both options to discover the answer organically.
C:\> activate flaskenv
(flaskenv) C:\>
pip
Once your environment is activated, you can start installing your packages. The first package you will need is pip
, as this is how we will be downloading our packages. I know this may appear a bit strange to download a package-manager inside a package-manager, but it demonstrates the flexibility of using conda
.
(flaskenv) C:\> conda install pip
Now we can use pip
to install our packages, but always make sure your environment is active before doing so. If your environment is not active, not only will you be downloading all of those packages globally on your machine, but you will not be able to save the packages you use to the requirements.txt file. The requirements.txt is Flask's version of package.json, i.e., a list of your applications dependencies.
Once you install pip
, you can install flask
using pip
.
(flaskenv) C:\> pip install flask
requirements.txt
(flaskenv) C:\api-app\> pip freeze > requirements.txt
Now you can open requirements.txt and view your app's dependencies.
Be sure to run pip freeze > requirements.txt
whenever you add a new package.
App It Up
Create a new directory api-app with the following structure:
api-app/
app/
__init__.py
routes.py
run.py
__init__.py
from flask import Flask
app = Flask(__name__)
If you'd like to learn more about Python class and OOP, this article by Jeff Knupp is a good start.
Here we are assigning an instance of the Flask
class to the variable app
. As your app becomes more complex, you will pass this class instance to other libraries. For example:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
Note: This example is not part of this project.
This is how inheritance works in Python. The SQLAlchemy class (the child class) instance inherits app
(the parent class) — this provides the child class with the attributes and methods of the parent class.
Passing __name__
to Flask
tells Python the proper way to execute the file. This has to do with how Python executes programs. For more information, you may reference this stackoverflow post.
routes.py
from app import app
@app.route('/')
def index():
return 'Hello, World!'
For JavaScript developers this syntax may look a bit strange. First, def index()
defines a funtion named index. Second, @app.route('/')
is what's known as a Python decorator,
which is sort of like a callback function. To clarify things, let's translate this to Express, a popular NodeJS web framework:
app.get('/', function(req, res){
res.send('hello world');
});
Both of these apps are doing the same thing. In Python, @
before a function name makes that function a decorator. Decorators, a higer-order function, are followed by a function, which is passed as an argument to the decorator.
Now that we have a route created, we can import our routes into our __init__.py file.
# app/__init__.py
from flask import Flask
app = Flask(__name__)
from app import routes
We place the routes import at the bottom to avoid what's known as a circular dependency — you can learn more about this issue here.
run.py
from app import app
That's all the file requires. To clarify, app is the Flask instance we created in __init__.py, and it is a member of the app package. The app package refers to the /app folder in our directory, thus why in our __init__.py file we are able to import our routes from app.
FLASK_APP
(flaskenv) C:\api-app\>set FLASK_APP=run.py
Flask will look for an environment FLASK_APP
set to, in this case, run.py. This will instruct Flask on the proper way to import our application.
Run it!
(flaskenv) C:\api-app\>flask run
If everything goes smoothly, you can visit your app at http://127:0.0.0.1:5000/
and hopefully see "Hello, World" in the browser.
Configuration
We are going to add a config.py file to our project. This is where the developer can define some variables that will be used throughout the application. It's a good place to create variables that the developer wants to keep secret, such as API-keys.
Once you add the config.py file, your project structure should look something like this:
api-app/
app/
__init__.py
routes.py
run.py
config.py
Once we create our config.py, we need to tell our app to use it. Here config is referencing our file config.py and not an installed library.
# app/__init__.py
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
config.py
# app/config.py
import os
class Config(object):
API_KEY = os.environ.get('API_KEY') or 'nice-try'
Recall in the first post when we used process.env.API_KEY
to keep the API out of version-control? Well, os.environ.get('API_KEY')
is doing the same thing. The configuration object will check to see if the environment variable API_KEY
exists, if it doesn't we provide a sarcastic fallback.
As a reminder, you set environment variables like this:
# Bash
$ export API_KEY=someKey
:: windows
(flaskenv) C:\api-app\> set API_KEY=someKey
As I mentioned this app is minimal, thus our config.py only contains one key. Yes, a key. For JavaScript developers, you can think of our configuration object as a JS-object — a collection of key, value pairings.
For example, accessing the API_KEY
defined in our config.py file will look something like this:
from app import app
...
API_KEY = app.config['API_KEY']
I hope it's becoming clearer what is taking place in our __init__.py file. If not, there is no issue with treating this application as a bit of a black-box while you continue learning. I made it made it clear the purpose of this application is hiding your API-key from wrong-doers. For front-end developers with no interest in learning Python, not having a deep-understanding of Python modules is okay. In other words, this blog post is getting quite lengthy and I don't have time to elaborate on the nuances of Python here.
Requests
Now that our basic Flask app is in place, we can begin creating the routes our front-end will access. For this tutorial we will only create one route, though the logic is easily replicable for additional routes.
To access the API, we will use the Python's requests
library.
Run the following in your terminal:
(flaskenv) C:\api-app\> pip install requests
Lets make a request
Close your app if it's running, and enter python
in your terminal. This will activate the Python command-line, which is where we'll explore the requests
library.
(flaskenv) C:\api-app\>python
Python 3.6.0 |Anaconda custom (64-bit)| (default, Dec 23 2016, 11:57:41)
[MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
To exit the Python terminal, enter quit()
, because ctrl-c
won't do it.
For this example, let's use an API that doesn't require a key. We will use the IEX API, a free stock API.
Copy this URL: https://api.iextrading.com/1.0/stock/market/batch?symbols=fb,f&types=quote,news
Here fb
and f
are ticker symbols for Facebook and Ford, respectively. Even if you pass invalid symbol parameters (i.e., ticker symbols that don't exist), the API response will still return data for valid queries, which is really nice.
Let's make our first request. Enter the following in your Python terminal:
>>>import requests
>>>res = requests.get('https://api.iextrading.com/1.0/stock/market/batch?symbols=fb,asdasdasd,f&types=quote,news')
requests.get()
will make a GET request to the passed URL and return a <Response>
object. This object will have attributes expected in an HTTP response.
For example, let's make sure the request was succcessful:
>>> res.status_code
200
If everything goes well, we get the glorious status code of 200
.
Since we know our request was successful, let's take a look at the data:
>>> resJson = res.json()
>>> resJson
{'FB': {'quote': {'symbol': 'FB','companyName': 'Facebook Inc.''primaryExchange': 'Nasdaq Global Select' 'sector':
...
Sorry for the console dump, but you get a sense of the data. By calling res.json()
we can now index our data, so let's get some data that's a bit more managable:
>>> resJson['FB']['quote']['open']
145.83
I encourage you to play around a bit with the requests
library. Otherwise, let's get back to the app.
Routes
Make the following modifications to app/routes.py:
from app import app
from flask import jsonify
import requests
@app.route('/')
def index():
res = requests.get(
'https://api.your-api.com/something.json?api-key={0}'
.format(app.config['API_KEY']))
if res.status_code != 200:
errData = {'status': res.status_code, 'error': 'There was an error'}
return jsonify(errData), res.status_code
apiData = jsonify(res.json())
return apiData
As you can see, we pass our res.json()
to jsonify()
, which is imported from flask
. This is because res.json()
does not provide the encoding we need. By passing our data to jsonify
, we now get a flask Response object that will properly serialize our data as application/json mimetype.
Visit http://localhost:5000
and you should see a whole bunch of data. If you use Chrome, download JSON view. JSON view will format your JSON data so it doesn't appear as one big mess in your browser.
Once you download JSON view, you should see something like this when you access the route:
Now what you see in the browser when you visit localhost:5000
should look exactly the same as when you place the actual API URL in the browser. The primary difference, of course, is now your API-key will not appear in the response:
Development
When developing, the only thing you need to do is make sure your front-end app and back-end app are listening on different ports. For example, your front-end app will run on port 8000
and your back-end app will run on port 5000
.
Hosting
We will use Heroku to host our application since it is the most user-friendly option, however I suggest you check out Digitalocean, go through some tutorials, and challenge yourself a bit. Otherwise, Heroku is still an excellent and popular hosting service.
Download Heroku
The first thing you need to do is download the Heroku CLI and go through the tutorial — this will take you through the proces of creating your first heroku app.
We will be using Heroku's free plan, because it's free. The catch, however, is that your application will turn off after 30 minutes. If you decide to upgrade your plan to hobby
, it's only about $7 a month depending on your site's traffic.
Gunicorn
(flaskenv) C:\api-app\> pip install gunicorn
Gunicorn is a "Python WSGI HTTP Server for UNIX." Basically it will provide the production server for our application.
Procfile
We need to tell Heroku how to run our application — this is where the Procfile comes in. In the Procfile we will tell our dyno
(the server instance our application is running on) what it should do once the server starts up.
Create a new file in the root-directory named Procfile — no extensions, simply Procfile
api-app/
app/
__init__.py
routes.py
run.py
config.py
requirements.txt
Procfile
Inside your Procfile, add the following code:
web: gunicorn: run:app
First, web
defines our process — this tells our dyno
that we want a web-server. Second, we pass run:app
to gunicorn
to start our server, where run
refers to run.py and app
refers to the app
we've created, i.e., the one imported in run.py.
Now that our serve is in place, let's configure our environment variables.
heroku config
You can configure environment variables in two ways with Heroku:
- Through the Heroku dashboard on their site
- Through the
Heroku CLI
Since this is a programming tutorial, we'll be using the CLI to configure the two environment variables used in this application. Also, Heroku has a really cool CLI.
Run the following in the terminal:
(flaskenv) C:\api-app\>heroku config:set FLASK__APP=run.py
...
(flaskenv) C:\api-app\>heroku config:set API_KEY=<YOUR_API_KEY>
git push heroku master
Now comes the moment of truth — pushing your master
branch to Heroku. Two item checklist before pushing to Heroku:
-
Make sure all necessary packages are in your requirements.txt file:
(flaskenv) C:\api-app\> pip freeze > requirements.txt
- Make sure your
master
branch is up to date.
And that's about it. If you've done those two things, then you're ready to push to Heroku:
(flaskenv) C:\api-app\>git push heroku master
You should see some command-line output, followed by Verifying deploy... done.
Now open your app and see your data:
(flaskenv) C:\api-app\>heroku open
When the site opens in your browser, you should see your JSON.
And that's about it. When you're ready to show your app to people you can upgrade your plan and the server won't turn off after thirty-minutes.
CORS
Now you are able to access your data by making fetch requests to your Heroku app. Sadly, as our app stands, other's will also be able to make ajax to your API. To prevent this from happening, we will be implementing CORS.
Here is a definition from MDN's article on CORS:
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application makes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.
For our purposes, we will only be focusing one part of CORS — Access-Control-Allow-Origin
The Access-Control-Allow-Origin
is configured in our Flask app and instructs the app which "origins" to provide access to. Here the origin is the domain of the client making the request.
Flask-CORS
We'll use Flask-CORS to configure CORS for our Flask app.
(flaskenv) C:\api-app\> pip install flask-cors
...
(flaskenv) C:\api-app\> pip freeze > requirements.txt
Now make the following modifications to app/__init__.py, replacing the URLs in the origins
list with your own.
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=["https://yoururl.com", "https://www.yoururl.com"])
from app import routes
Now only the domain of your frontend application will be able to make ajax requests to your heroku app. origins
can be a string or a list, so configure it accordingly. Make sure you include https://
or http://
and include both your naked and www
domains, or else you will get a CORS error. There is a fair-amount to CORS, so I encourage you to do some more research on the topic.
Conclusion
By implementing an app such as this you demonstrate you're a front-end developer who is security conscious, which is important. This tutorial may have either scared you or excited you about backend workflows, but now your API is safe and that's important.
As for me, this blog took a very long time to write so I'm going to relax and work on a front-end blog.