I recently had the urge to try out FastAPI, and reading the docs I saw how similar it looked to Flask. So I thought, since I have this small demo I’m working on that is basically an API with a nice HTML front-end, how hard would it be to migrate?

Note: this was for a demo, so while the below works, it may not be the best solution in all cases.

This post uses the diff format. If you’re unfamiliar: lines starting - were removed, lines starting with + were added.

The tips I’ve picked up:

Web Server

Because you’re going to be dealing with async by default, if you’re using gunicorn you’re going to have to replace that with uvicorn. Same format though:

-$ gunicorn app:app --reload

+$ uvicorn app:app --reload

Application object

This one is fairly simple: just replace Flask with FastAPI:

-app = Flask(__name__)
+app = FastAPI()

Route methods

FastAPI is pedantic. Well, that is to say it’s pydantic, making use of the pydantic package for data validation through type annotations.

If you haven’t use type annotations before, checkout the typing docs.

In a practical sense, if you’ve been relying on Flask’s request without declaring it in your method signatures, you’ll have to do that. You’ll also have to declare your methods as async.

@app.get("/")
- def index():
+ async def index(request: Request):
     if request: 

Json responses

You can drop the jsonify, if you want to return a dict to the user.

- return jsonify(response)
+ return response

Templates

You can use Jinja2 in FastAPI, you just need to be a bit more specific:

Loading templates: explicitly tell it where to pull files:

+ from fastapi.templating import Jinja2Templates
+ templates = Jinja2Templates(directory="templates")

Linking static in templates: you just need to replace filename with path:

- url_for('static', filename='theming.js')
+ url_for('static', path='theming.js')

Be wary here, though! While this function reads the same, this will generate absolute URLs, and may not correctly pick up your scheme (HTTP vs HTTPS). In my case, I got “mixed content” errors. I worked around this temporarily by hardcoding the URLs, which may not be the best solution long term.

Uploads

This one is going to be harder.

Because of the type annotation earlier, you need to be very explicit with what you want the user to provide. You will also be questioned on the values used in your HTML form before you proceed.

In my case, I wanted to allow either a file upload or a curl of a JSON file, so these are the changes I needed to make.

For the form itself, I originally had:

<input type="file" id="upload_file" name="upload">

So upload must be in the method signature.

I also needed to change the way I redirect. You can do redirect responses, but in my case I’m using a GET-only route as a failure case for a POST-only route, so I need to explicitly set the status code here to not pass-through the POST method and get a routing error.

@app.post("/upload")
-def upload():
-    if "upload" not in request.files:
-        return redirect(request.url)
-    upload = request.files["upload"]
-    data = json.loads(upload.read())
+async def preapprove_upload(request: Request, upload: bytes = File(...)):
+    if not upload:
+       return RedirectResponse('/', status_code=303)
+    data = json.loads(upload)

You’ll also need to include the multipart-upload package if you’re doing complex upload things.

Curl Endpoint

This also required some changes, but it also did some processing for me.

By specifying Body it pre-processed the data for me, so I didn’t have to worry about JSON processing:

 @app.post("/preapprove")
-def preapprove():
-    if request.data:
-        response = apply_business_logic(json.loads(request.data))
+async def preapprove(data: dict = Body(...)):
+    if data:
+        response = apply_business_logic(data)

Thoughts

This wasn’t as bad as it could be. Having frameworks follow similar patterns helps, but it’s those nuances that’ll get you every time.

Though now I have fancy API documentation for free, so it’s probably worth it :)