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:

Render Deployment

Click on the Deploy a Web Service, and you'll see this page:

Deploy a web service

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.

Render 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:

Zuplo new project

Let's create an empty project, and proceed. At first, you'll see a page with a lot of information:

Zuplo getting started

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:

Zuplo code 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:

Importing FastAPI OpenAPI to Zuplo

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:

Zuplo endponts

Let's click on "Test", and then "Test" again, and you'll see that everything works as expected.

Zuplo proxy

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.

Environment variable

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.

Zuplo base URL

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

Zuplo rate limit policy

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:

  1. We'll rate limit by IP.
  2. We'll allow 2 requests for that IP.
  3. 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.

Zuplo rate limiting

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.

ZUplo API key policy

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.

API key consumer

Copy the generated API key, and let's go back to the "Code" page.

Copy the key

Let's run the test again, first without any changes:

Zuplo unauthorized response

The 401 response is expected, since we didn't send the API key. Let's add it to the headers, and try again.

Add to headers

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

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

Zuplo developer portal

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.

Questions? Let's chatOPEN DISCORD
0members online

Designed for Developers, Made for the Edge