Create a Serverless Python API with AWS Amplify and Flask
Jun 08, 2023 • 9 Minute Read
Flask is one of my favorite tools for creating a quick API. Python is still my favorite programming language despite using JavaScript primarily the past few years -- the syntax feels straightforward and elegant to me and the power of Python libraries like Pandas is incredible.
Deploying Python applications is not always the most fun activity, so we're also going to use AWS Lambda to make our app serverless. In addition, we'll use AWS Amplify's command line interface to create and manage our resources.
We'll also need a database to store our data, in this case we'll build an API for Britney Spears' songs and use DynamoDB store our data.
This tutorial will be most helpful for someone who already has basic knowledge of AWS Amplify, how APIs work, and Python.
Let's dive into the code!
Project Setup
First, we'll initialize an Amplify project. You'll need to have the Amplify CLI installed and configured and Node installed. I'll start with a React app, but we'll focus on the backend for the tutorial so if you aren't a React dev you should be fine!
Run the following commands in your terminal to create a React boilerplate, change into it, and then initialize Amplify.
$ npx create-react-app britney-spears-api
$ cd britney-spears-api
$ amplify init
Initializing Amplify will prompt you with some questions. For all of these, you can choose the default by just pressing enter!
? Enter a name for the project britneyspearsapi
? Enter a name for the environment dev
? choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path build
? Build Command: npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? yes
? Please choose the profile you want to use `your-profile`
Add a Database
Next, we'll need to add a DynamoDB database to our project. Run amplify add storage
in your CLI. This will ask you some questions about your data.
? Please select from one of the below mentioned services: NoSQL Database
? Please provide a friendly name for your resource that will be used to label this category in the project: britneySongStorage
? Please provide table name: britneySongs
? What would you like to name this column: id
? Please choose the data type: string
? Would you like to add another column: Yes
? What would you like to name this column: name
? Please choose the data type: string
? Would you like to add another column: Yes
? What would you like to name this column: year
? Please choose the data type: number
? Would you like to add another column: Yes
? What would you like to name this column: link
? Please choose the data type: string
? Would you like to add another column: No
? Please choose partition key for the table: id
? Do you want to add a sort key to your table? N
? Do you want to add global secondary indexes to your table? No
? Do you want to add a Lambda Trigger for your Table? No
Now, run amplify push -y
to deploy your database on AWS!
Create the API
Now, let's add an API! We'll create routes for all the CRUD operations and allow database interaction.
We'll add an API using the Amplify CLI: amplify add api
.
You'll be prompted to answer some questions. First, name your API, then provide a base path for your routes.
? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this category in the project: britneySongApi
? Provide a path (e.g., /book/{isbn}): /song
Then, we'll be prompted to create a new AWS Lambda function and name it. We'll use a Python one.
? Choose a Lambda source Create a new Lambda function
? Provide an AWS Lambda function name: britneyspearsapi
? Choose the runtime that you want to use: Python
We'll also need to configure some advanced settings in order to provide access to our database. Use your space bar to select storage and all CRUD actions.
? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the categories you want this function to have access to. storage
? Select the operations you want to permit on britneySongStorage create, read, update, delete
You can then answer "No" to the next few questions, though I'd select yes for editing your local Lambda function. It'll just open your text editor up to the right file for you!
? Do you want to invoke this function on a recurring schedule? No
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? Yes
? Restrict API access No
? Do you want to add another path? No
Creating an API will enable Amazon API Gateway which will handle the routing of HTTP requests to our Lambda function.
Finally, run amplify push to deploy your services to the cloud. The -y
flag will just skip the confirm step.
$ amplify push -y
Note: you'll need to re-run this push command whenever you want to deploy changes to your API.
Python Function
First, change into your function's directory:
cd amplify/backend/function/britneyspearsapi
You'll need to have pipenv installed for Python packaging. Then, we'll install the necessary packages.
aws-wsgi
will allow us to use Flask routingboto3
enables interaction with AWS services in Python, we'll use it for DynamoDBflask
is our web frameworkflask-cors
will handle CORS for our flask app
$ pipenv install aws-wsgi boto3 flask flask-cors
Then, open up your Lambda function's code -- it was opened for you during the amplify add api
step, but if you closed it, open up src/index.py
within your function's folder.
There will be a generated "Hello World" Lambda function in your file, you can delete it.
First, we need to initialize a Flask application. We'll also use flask-cors
to handle CORS.
from flask_cors import CORS
from flask import Flask, jsonify, request
app = Flask(__name__)
CORS(app)
Below this code, add a Lambda handler -- this function will process our request event. API Gateway is looking specifically for a function called handler
so make sure to call it that!
We'll also use awsgi
to use WSGI middleware with API Gateway.
Import awsgi
:
+ import awsgi
from flask_cors import CORS
from flask import Flask, jsonify, request
Then add the handler
function:
def handler(event, context):
return awsgi.response(app, event, context)
Above the handler
, let's add our first route! We already created a base path for our API during the amplify add api
step above. I'm going to save that route to a variable so that I can prefix all my urls with it. Then, I'll add a route to my Flask app that handles that /songs/
url and takes GET requests. Right now, I'll just send a "hello world" message back.
# Constant variable with path prefix
BASE_ROUTE = "/song"
@app.route(BASE_ROUTE, methods=['GET'])
def list_songs():
return jsonify(message="hello world")
Run amplify push -y
to deploy your changes.
To use this route on the frontend, first I'll install aws-amplify
. Do this in your frontend's root directory instead of your Python function's.
$ npm i aws-amplify
Then, configure Amplify. If you're in a create-react-app
generated project, put this in the index.js
file.
import { Amplify } from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)
Then, add your API call. We'll use aws-amplify
's API.get
to make a GET request to our API. The first argument is the name of the API we created, the second is the route we want to request.
import { API } from 'aws-amplify'
const getData = async () => {
const data = await API.get('britneySongApi', '/song')
console.log(data)
}
Add Create Route
Now, let's make a route that will create a new song in our database. I'll import boto3
, uuid
, and os
and then create a new connection to the database. We'll also get the name of our DynamoDB table from our environmental variables that Amplify created for us. These are listed in the output of the amplify add api
step.
import awsgi
+ import boto3
+ import os
from flask_cors import CORS
from flask import Flask, jsonify, request
+ from uuid import uuid4
+ client = boto3.client("dynamodb")
+ TABLE = os.environ.get("STORAGE_BRITNEYSONGSTORAGE_NAME")
BASE_ROUTE = "/song"
Now, we'll create our route. This one will still be at the /song
url, but this time it will handle a POST request. We'll get the request object, convert it to json, and then create the item. We'll provide the name of the table and the data for the item. We'll randomly generate the id
using Python's uuid4
function. Then, we'll return a message that the item was created.
@app.route(BASE_ROUTE, methods=['POST'])
def create_song():
request_json = request.get_json()
client.put_item(TableName=TABLE, Item={
'id': { 'S': str(uuid4()) },
'name': {'S': request_json.get("name")},
'year': {'S': request_json.get("year")},
'link': {'S': request_json.get("link")},
})
return jsonify(message="item created")
Now, on the frontend, we'll make a POST request to create the new song!
const data = await API.post('britneySongApi', '/song', {
body: {
name: 'Toxic',
year: '2003',
link: 'https://www.youtube.com/watch?v=LOZuxwVk7TU'
}
})
console.log(data)
Add List Route
Let's update our list route to return a list of all the songs in the database. Change the return on the list_items
function to be a table scan.
@app.route(BASE_ROUTE, methods=['GET'])
def list_songs():
+ return jsonify(data=client.scan(TableName=TABLE))
Then on the frontend, you can make this request to see all the songs!
const data = await API.get('britneySongApi', '/song')
console.log(data)
Add Get Route
Now, we'll add a function that gets one song based on its id
. We'll create a route that handles GET requests to /song/<song_id>
. Then, we'll query the database for the song with that id!
@app.route(BASE_ROUTE + '/<song_id>', methods=['GET'])
def get_song(song_id):
item = client.get_item(TableName=TABLE, Key={
'id': {
'S': song_id
}
})
return jsonify(data=item)
Add Edit Route
Now, we need to add a route to edit an item. This will handle PUT
requests to /song/<song_id>
. The Key
will be the id
of the song we want to update. The UpdateExpression
will allow us to modify all the attributes of the song. Then the ExpressionAttributeNames
provides the key names and the ExpressionAttributeValues
provides the values.
@app.route(BASE_ROUTE + '/<song_id>', methods=['PUT'])
def update_song(song_id):
client.update_item(
TableName=TABLE,
Key={'id': {'S': song_id}},
UpdateExpression='SET #name = :name, #year = :year, #link = :link',
ExpressionAttributeNames={
'#name': 'name',
'#year': 'year',
'#link': 'link'
},
ExpressionAttributeValues={
':name': {'S': request.json['name']},
':year': {'S': request.json['year']},
':link': {'S': request.json['link']},
}
)
return jsonify(message="item updated")
Add Delete Route
Finally, we'll add a route to delete a song.
@app.route(BASE_ROUTE + '/<song_id>', methods=['DELETE'])
def delete_song(song_id):
client.delete_item(
TableName=TABLE,
Key={'id': {'S': song_id}}
)
return jsonify(message="song deleted")
Run amplify push -y
to deploy your changes!
Debugging
To debug issues with your API, you can update run amplify mock
with a event.json
file to match your request's event object. For example, to test the /song
path, you could use an event like this:
event.json
{ "path": "/song", "httpMethod": "GET", "queryStringParameters": ""}
Then run:
$ amplify mock function britneyspearsapi
to test your function locally.
Next Steps
This tutorial demonstrates how to use the AWS SDK with an AWS Amplify-generated Lambda function to build a full CRUD API. You could continue to use other Amplify resources like storage to store the song files, or authentication to restrict the API usage. You could also build out a full frontend to pull the data.