FastAPI Tutorial: Build, Deploy, and Secure an API for Free
This article is written by Marcelo Trylesinski, a FastAPI expert and maintainer of Starlette and Uvicorn. You can check out more of his work here. All opinions expressed are his own.
Some weeks ago, Zuplo sent me an email asking to write a blog post for them. I was a bit reluctant, because I don't want to promote companies that I don't know. But I talked to them, and they were completely fine with me writing a post about their service without promoting it. So, here it is. 🚀
For this post we will:
- Build a simple CRUD API with FastAPI
- Deploy to Render
- Explore Zuplo's features:
- Rate limiting ⏳
- Authentication 🔒
- Generate a developer documentation portal 👀
Pre-requisites#
- Python 3.12+
- Render account
- Zuplo account
Build a CRUD API with FastAPI#
For the purposes of this post, we'll build just a simple CRUD API.
Let's start creating directory, the virtual environment, and installing the dependencies we'll need.
pip install fastapi uvicorn
Then, we'll create a main.py
file with the following content:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
We created the read_root
endpoint function, that is ran when we receive an
HTTP GET request to the path /
.
Now that we have our first endpoint, we can run the server with:
uvicorn main:app --reload
The --reload
flag is used to reload your server when you make file changes.
From now, you'll not need to run it anymore after the changes we'll make.
Create the CRUD endpoints#
Okay, we'll just create the CRUD endpoints for a simple Item
model.
from typing import Annotated
from fastapi import FastAPI, HTTPException
from pydantic import Field
from typing_extensions import TypedDict
app = FastAPI(prefix="/items", responses={404: {"description": "Item not found."}})
class Item(TypedDict):
name: Annotated[str, Field(description="The name.")]
description: Annotated[str | None, Field(None, description="The description.")]
class ItemOutput(Item):
id: Annotated[int, Field(description="The Item ID.")]
items: dict[int, Item] = {}
id_counter = 0
@app.get("/")
async def read_items() -> list[ItemOutput]:
"""Read all items."""
return [{"id": id, **item} for id, item in items.items()]
@app.get("/{item_id}")
def read_item(item_id: int) -> ItemOutput:
"""Read a single item."""
try:
return {"id": item_id, **items[item_id]}
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
@app.post("/")
def create_item(item: Item) -> ItemOutput:
"""Create a new item."""
global id_counter
items[id_counter] = item
id_counter += 1
return {"id": id_counter - 1, **item}
@app.put("/{item_id}")
def update_item(item_id: int, item: Item) -> ItemOutput:
"""Update an item."""
try:
items[item_id] = item
return {"id": item_id, **item}
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
@app.delete("/{item_id}")
async def delete_item(item_id: int) -> ItemOutput:
"""Delete an item."""
try:
item = items.pop(item_id)
return {"id": item_id, **item}
except KeyError:
raise HTTPException(status_code=404, detail="Item not found")
This is the simplest CRUD API you can build with FastAPI.
We used a dict
to store the items, and a global counter to generate the IDs,
to simulate a database.
Deploy to Render#
For those who don't know, Render is a cloud platform that allows you to deploy your applications with ease. For small projects, and hobby projects, it's free. 🎉
!!! note I'm not affiliated with Render, and they do not sponsor me in any way.
After you create an account, you'll see this Overview page:
Click on the Deploy a Web Service, and you'll see this page:
If there's no repository on the list, you can click on the Credentials tab, and connect to your GitHub, GitLab, or Bitbucket account.
I usually only give permissions to the repositories I want to deploy. I follow the security principle of least privilege, whenever possible. 🤓
Cool! Now we can select the repository, and configure the deployment settings.
You shouldn't need to change anything here, maybe you can select a region closer to you. I live in The Netherlands, so I selected Frankfurt.
If you go down, you'll see the Build command, change it to:
pip install fastapi uvicorn
You can also change it to pip install -r requirements.txt
, and create a
requirements.txt
file with the dependencies. For now, we'll keep it simple.
On the Start command, change it to:
uvicorn main:app --host 0.0.0.0
Just remove the --reload
flag from the command we ran before, because that
flag is only for development.
On the Instance Type, select the Free plan. Since we're just playing around, we don't need to pay for it. 🎉
Let's click on Deploy Web Service, and that's it! 🚀
Wait a few minutes, and you'll be able to go to
https://<your_project_name>.onrender.com/docs
and see the Swagger UI
documentation generated by FastAPI.
Explore Zuplo's features#
I haven't used Zuplo before, so I'm going to explore it with you. 🚀
Let's go to Zuplo's Portal, and create an account. After you sign up, you'll see this page:
Let's create an empty project, and proceed. At first, you'll see a page with a lot of information:
Don't worry, I said we'll explore together! 🤓
Reading the first pages of the documentation, it looks like the first thing we need to do is to setup a Basic Gateway.
Setup a Basic Gateway#
Let's click on the Start Building on the screen above, and we'll see this page:
Given the docs, we need to add our OpenAPI JSON as a first step. We can get it
accessing the /openapi.json
endpoint from the FastAPI application we deployed
on Render.
Then you copy paste to the routes.oas.json
file:
Save it, and voilà! 🎉
Editor's Note: You can also import your API from the OpenAPI tab or via CLI if developing locally
Now you'll see the following screen with all your endpoints:
Let's click on "Test", and then "Test" again, and you'll see that everything works as expected.
We need to make this a bit more useful. The problem here is that Zuplo doesn't know what's our base URL, so we need to set it up.
Go to the Settings tab, and then Environment Variables. Add a new
variable with the key BASE_URL
, and the value
https://<your_project_name>.onrender.com
.
Cool! Now get back to where you were, and change the Forward to to
${env.BASE_URL}
, press save, and try to test it again. If everything goes
smoothly, you'll see an empty array as a response.
This means that Zuplo is working as expected. 🚀
Rate limiting#
Now, we can try to add a rate limiting to our API.
On your endpoint, you'll see the "+ Add Policy" button. Click on it, and search for "Rate Limiting".
When you click on it, you'll see a JSON with some options. It's nice that they describe the parameters next to the input fields. In this case, the default values mean:
- We'll rate limit by IP.
- We'll allow 2 requests for that IP.
- The window for those 2 requests is 1 minute.
Let's save, and try it out. Go to test again, and click on "Test" a three times. You'll see that after the second request, you'll get a 429 error.
If you wait for a minute, and try again, you'll see you have a 200 response once again.
Ok! We have a rate limiting working. 🎉
Authentication#
Now, let's try to add an authentication to our API.
We'll do the same thing as before, and click on "+ Add Policy", and search for "API Key Authentication".
The default configuration should be enough for now.
Let's follow the steps from the documentation to add the API key to our requests.
Let's go to "Services", and then configure the "API Key Service". Create a
customer with a test-consumer
name, and your email. You can leave the metadata
as is, and save it.
Copy the generated API key, and let's go back to the "Code" page.
Let's run the test again, first without any changes:
The 401 response is expected, since we didn't send the API key. Let's add it to the headers, and try again.
Oh! It worked! 🎉
Developer Documentation Portal#
On the footer of the page, you'll see many shortcuts. Click on "Gateway deployed", and let's go to the "Developer Portal".
Zuplo automatically generates a UI for your API documentation based on the OpenAPI JSON you provided. It also adds the rate limiting and authentication information to the documentation.
Conclusion#
Thanks for reading this post! 🚀
I hope you enjoyed it, and learned something new. I know I did! 😄
Thanks to Zuplo for reaching out to me, and allowing me to explore their product.