---
title: "FastAPI Tutorial: Build, Deploy, and Secure an API for Free"
description: "Learn how to build, host, and secure an API with Python and FastAPI for free using Render and Zuplo. Implement auth, rate limiting, and autogenerate documentation."
canonicalUrl: "https://zuplo.com/blog/2025/01/26/fastapi-tutorial"
pageType: "blog"
date: "2025-01-26"
authors: "marcelo"
tags: "Python, API Tooling, Tutorial"
image: "https://zuplo.com/og?text=FastAPI%20Tutorial%3A%20Build%2C%20Deploy%2C%20and%20Secure%20an%20API%20for%20Free"
---
_This article is written by Marcelo Trylesinski, a
[FastAPI expert](https://fastapi.tiangolo.com/fastapi-people/#experts) and
maintainer of [Starlette](https://www.starlette.io/) and
[Uvicorn](https://www.uvicorn.org/). You can check out more of his work
[here](https://fastapiexpert.com/). 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. 🚀

<WhatsIncluded
  title="What we'll cover"
  features={[
    `Build a simple CRUD API
with FastAPI`,
    `Deploy to Render for free`,
    `Add rate limiting with Zuplo`,
    `Implement API key authentication`,
    `Generate a developer documentation portal`,
  ]}
/>

<CalloutAudience
  variant="useIf"
  items={[
    `Building a REST API with Python and
FastAPI`,
    `Looking for free hosting and deployment options`,
    `Need to add
authentication and rate limiting without writing code`,
    `Want auto-generated API
documentation from OpenAPI`,
  ]}
/>

### 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 a directory, the virtual environment, and installing the
dependencies we'll need.

```bash
pip install fastapi uvicorn
```

Then, we'll create a `main.py` file with the following content:

```python title="main.py"
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}
```

We created the `read_root` endpoint function, that is run when we receive an
HTTP GET request to the path `/`.

Now that we have our first endpoint, we can run the server with:

```bash
uvicorn main:app --reload
```

The `--reload` flag is used to reload your server when you make file changes.
From now on, 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.

```python title="main.py"
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](https://render.com) 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](/media/posts/2025-01-26-fastapi-tutorial/image.png)

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

![Deploy a web service](/media/posts/2025-01-26-fastapi-tutorial/image-1.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-2.png)

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:

```bash
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:

```bash
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](https://portal.zuplo.com/?utm_source=blog), and
create an account. After you sign up, you'll see this page:

![Zuplo new project](/media/posts/2025-01-26-fastapi-tutorial/image-3.png)

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

![Zuplo getting started](/media/posts/2025-01-26-fastapi-tutorial/image-4.png)

Don't worry, I said we'll explore together! 🤓

Reading the
[first pages of the documentation](https://zuplo.com/docs/articles/step-1-setup-basic-gateway?utm_source=blog),
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](/media/posts/2025-01-26-fastapi-tutorial/image-5.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-6.png)

Save it, and voilà! 🎉

<CalloutTip>
  You can also import your API from the OpenAPI tab or via CLI if developing
  locally.
</CalloutTip>

<CalloutDoc
  title="Getting Started with Zuplo"
  description={`Learn how to set up your first API gateway with Zuplo, import your OpenAPI specification, and start applying policies.`}
  href="https://zuplo.com/docs/articles/step-1-setup-basic-gateway"
  features={[
    `OpenAPI-native`,
    `Edge deployment`,
    `Zero-config DDoS protection`,
  ]}
/>

Now you'll see the following screen with all your endpoints:

![Zuplo endpoints](/media/posts/2025-01-26-fastapi-tutorial/image-7.png)

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

![Zuplo proxy](/media/posts/2025-01-26-fastapi-tutorial/image-8.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-14.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-9.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-10.png)

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" three times.
You'll see that after the second request, you'll get a 429 error.

![Zuplo rate limiting](/media/posts/2025-01-26-fastapi-tutorial/image-11.png)

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

<CalloutDoc
  title="Rate Limit Policy"
  description={`Zuplo's Rate Limit Policy enables per-user, per-key, or global rate limiting with configurable time windows and request thresholds.`}
  href="https://zuplo.com/docs/policies/rate-limit-inbound"
  features={[
    `Per-IP or
per-key limiting`,
    `Configurable time windows`,
    `Custom 429 responses`,
  ]}
/>

### 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](/media/posts/2025-01-26-fastapi-tutorial/image-12.png)

Let's follow the steps
[from the documentation](https://zuplo.com/docs/articles/step-3-add-api-key-auth?utm_source=blog)
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](/media/posts/2025-01-26-fastapi-tutorial/image-15.png)

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

![Copy the key](/media/posts/2025-01-26-fastapi-tutorial/image-16.png)

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

![Zuplo unauthorized response](/media/posts/2025-01-26-fastapi-tutorial/image-13.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-17.png)

Oh! It worked! 🎉

<CalloutDoc
  title="API Key Authentication Policy"
  description={`Zuplo's API Key Authentication policy validates API keys against your configured key store, providing simple yet powerful authentication for your APIs.`}
  href="https://zuplo.com/docs/policies/api-key-auth-inbound"
  features={[
    `Self-serve API keys`,
    `Rate limiting per key`,
    `Secret scanning
integration`,
  ]}
/>

### 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](/media/posts/2025-01-26-fastapi-tutorial/image-18.png)

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](/media/posts/2025-01-26-fastapi-tutorial/image-19.png)

<CalloutDoc
  title="Developer Portal"
  description={`Zuplo automatically generates beautiful, interactive API documentation from your OpenAPI specification.`}
  href="https://zuplo.com/docs/dev-portal/introduction"
  features={[
    `Generated from
OpenAPI`,
    `Interactive playground`,
    `Custom branding`,
  ]}
/>

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