> This article is written by Alvaro, a member of the Zuplo community and
> longtime API builder. You can check out more of his work
> [here](https://medium.com/@atejada). All opinions expressed are his own.

A while back, I wrote a blog post on building an API using Ruby and Sinatra,
which you can check out
[here](/learning-center/how-to-build-an-api-with-ruby-and-sinatra). This time,
I’m not just switching frameworks (or microframeworks) but also languages—so
let’s talk about Python and Flask. 🤓

In the Ruby post, I had some fun weaving in quotes from Dune, but for this one,
I’m keeping things simple and focusing more on the API management side of
things.

We’ll build a straightforward Blog Post CRUD API, handling everything from
creating and reading posts to updating and deleting them. Let’s get started!

## What are we going to do today?

1. [Creating the project](#creating-the-project)
2. [Adding the required packages](#adding-the-required-packages)
3. [Creating the API](#creating-the-api)
4. [Testing our API](#testing-our-api)
5. [Hosting our API for the world to see](#hosting-our-api-for-the-world-to-see)
6. [Creating a project on Zuplo](#creating-a-project-on-zuplo)
7. [Adding a Rate Limit](#adding-a-rate-limit)
8. [Setting API Key Authentication](#setting-api-key-authentication)
9. [Developer Documentation Portal](#developer-documentation-portal)
10. [Wrapping Up](#wrapping-up)

## Creating the project

First, create a folder called **SimpleBlogAPI** and add a file named **app.py**
inside it. Since this is a simple project, we’ll keep all the source code in a
single file to keep things lightweight and easy to follow.

```bash
$ mkdir SimpleBlogAPI && cd SimpleBlogAPI
$ nano app.py
```

## Adding the required packages

For this project, we’ll use
[Flask](https://flask.palletsprojects.com/en/stable/) and
[Flask-RESTX](https://flask-restx.readthedocs.io/en/latest/).

Create a file named **requirements.txt** and type the following:

```txt
Flask==3.1.0
flask_restx==1.3.0
Gunicorn==23.0.0
uvicorn==0.34.0
```

To install all the dependencies, run the following command:

```bash
pip3 install -r requirements.txt
```

## Creating the API

Since we're using `Flask-RESTX`, the structure will be a bit different and
slightly more complex. But trust me, this extra structure will pay off—it'll
help us generate the OpenAPI Spec, which we'll need later 😌:

```python
from flask import Flask
from flask_restx import Api, Resource, fields, reqparse
from dataclasses import dataclass

app = Flask(__name__)

@dataclass
class Post:
    id: int
    title: str
    description: str

post_array = []
next_id = 1

@app.route("/")
def root_home():
    return "Welcome to the Simple Blog API!"

api = Api(
    app,
    version="1.0",
    title="Simple Blog API",
    description="An API to control a micro blog",
    doc="/swagger",
)

ns = api.namespace("posts", description="Blog posts")

post_parser = reqparse.RequestParser()
post_parser.add_argument("title", type=str, required=True, help="Post title")
post_parser.add_argument("description", type=str, required=True, help="Post content")

model = api.model("BlogModel", {
    "id": fields.Integer(readOnly=True, description="Post identifier"),
    "title": fields.String(required=True, description="Post title"),
    "description": fields.String(required=True, description="Post content"),
})

@ns.route("/")
class BlogResource(Resource):
    @ns.doc("get_posts")
    @ns.marshal_list_with(model)
    def get(self):
        return post_array

    @ns.doc("add_post")
    @ns.expect(post_parser)
    @ns.marshal_with(model, code=201)
    def post(self):
        global next_id
        args = post_parser.parse_args()
        post = Post(id=next_id, title=args["title"], description=args["description"])
        post_array.append(post)
        next_id += 1
        return post, 201

@ns.route("/<int:id>")
@ns.response(404, "Post not found")
@ns.param("id", "The post identifier")
class PostResource(Resource):
    @ns.doc("get_post")
    @ns.marshal_with(model)
    def get(self, id):
        post = next((post for post in post_array if post.id == id), None)
        if post is None:
            api.abort(404, "Post not found")
        return post

    @ns.doc("update_post")
    @ns.expect(post_parser)
    @ns.marshal_with(model)
    def put(self, id):
        post = next((post for post in post_array if post.id == id), None)
        if post is None:
            api.abort(404, "Post not found")
        args = post_parser.parse_args()
        post.title = args["title"]
        post.description = args["description"]
        return post

    @ns.doc("delete_post")
    @ns.marshal_with(model)
    def delete(self, id):
        global post_array
        post = next((post for post in post_array if post.id == id), None)
        if post is None:
            api.abort(404, "Post not found")
        post_array = [post for post in post_array if post.id != id]
        return "", 204

if __name__ == "__main__":
    app.run(debug=True)
```

## Testing our API

Now that we have data to work with, let’s start our server and test the API.

```bash
python3 app.py
```

Open your favorite web browser and navigate to **http://127.0.0.1:5000/**:

![hello world](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_01.png)

Success! It’s working as expected, although we have only tested the presentation
endpoint 😅 Let's add some entries:

```bash
curl -X POST "http://127.0.0.1:5000/posts/" \
-H "Content-Type: application/json" \
-d '{"title": "Hello World", "description": "The first post of the blog"}'
```

```bash
curl -X POST "http://127.0.0.1:5000/posts/" \
-H "Content-Type: application/json" \
-d '{"title": "Coding on Python", "description": "Doing some Python coding"}'
```

```bash
curl -X POST "http://127.0.0.1:5000/posts/" \
-H "Content-Type: application/json" \
-d '{"title": "Python, Flask and Zuplo", "description": "Making APIs better"}'
```

We should have three new entries, let's check them out.

Navigate to **http://127.0.0.1:5000/posts**:

![API response](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_02.png)

Great news! It works as expected, but only on our local machine. Wouldn’t it be
amazing to share it with the world?

First, we need to upload our project to GitHub.

## Hosting our API for the world to see

When it comes to hosting an API, there are plenty of options available. For this
particular API, we’ll use [Render](https://render.com/).

We need to create a new web service and select the repository we want to use—in
this case, **SimpleBlogAPI**.

![Render hosting](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_03.png)

![Connect git repo](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_04.png)

Don’t forget to update the start command to correctly call our **app.py** file:

![Change render command](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_05.png)

Once Render finishes deploying our API, we’ll be able to access the main entry
point using:

```
https://simpleblogapi.onrender.com/
```

As we just uploaded our API, it's going to be empty and without records, so we
need to create some.

**Keep in mind that we don't have any kind of authorization in place yet.**

As we have created our API with Swagger UI, we can take advantage of it 😎

In our browser, we need to navigate to:

```
https://simpleblogapi.onrender.com/swagger
```

![swagger docs](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_06.png)

It might not look like much right now, but if you click on the arrow on the
right-hand side, you can expand the endpoints and see everything in action.

![full swagger docs](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_07.png)

Now, if you expand the POST endpoint and click on the **Try it out** button,
you'll be able to test the API directly from the documentation:

![opening an endpoint](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_08.png)

Once you hit the **Execute** button, you'll be able to add some entries (I've
already added the first one). Finally, just press Execute, and the request will
be sent! 🚀

![testing from docs](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_09.png)

When we finished, we can check that the entries had been entered correctly:

```
https://simpleblogapi.onrender.com/posts
```

![Hosted API response](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_10.png)

Are we done yet? Yes and no. If we’re happy with it as is—simple, unsecured,
and, well, a bit amateurish—then we’re good. But if we want to make it cooler
without too much effort, we can start using Zuplo and level up the experience.

## Creating a project on Zuplo

After creating a
[Zuplo account](https://portal.zuplo.com/signup?utm_source=blog), we’ll need to
set up a new project. We’ll name it **simple-blog-api**, select an empty
project, and then create it. Wondering why Zuplo? Imagine having to build rate
limits, authentication, monetization, and other features entirely on your own.
That’s a lot of work and hassle. Zuplo simplifies all of that, making it a
breeze.

![Create a zuplo project](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_12.png)

Once the project is created, we’ll be greeted with this screen. From here,
simply press **Start Building**:

![Start building](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_13.png)

To start enhancing our API, click on **routes.oas.json**:

![routes.oas.json](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_14.png)

Next, we need to add a route, which Zuplo will manage:

![Import an OpenAPI](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_15.png)

In the Ruby post, we went with **Add Route**, but this time, we’ll select
**Import OpenAPI** instead. Now, you might be wondering—how exactly do we do
that? No worries, it’s easier than you think. 🤓

On the browser we just need to call

```
https://simpleblogapi.onrender.com/swagger.json
```

![Getting the raw OpenAPI](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_16.png)

And download it as swagger.json or any other name that we prefer.

Now, we can go back to Zuplo and upload the file.

![Choosing the OpenAPI file](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_17.png)

All of our endpoints will be imported automagically 🤩

![Finishing import](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_18.png)

Here, we just need to add a Summary (a brief description of our endpoint) and
replace the default Forward to value with our URL from Render. Simple as that!

![Adding summaries](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_19.png)

After configuring the routes, we need to save them. The save button is located
at the bottom, or we can press **[Command + S]** on a Mac.

![Saving changes](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_20.png)

The build process is blazing fast. Clicking the **Test** button allows us to, no
pun intended, test our API integration.

![Testing API](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_21.png)

Success! 🥳🥳🥳 Our Zuplo integration is working perfectly!

## Adding a Rate Limit

Most likely, we don’t want people abusing our API or attempting to take down our
server with DDoS attacks. While coding a rate limit policy isn’t difficult,
there are many factors to consider—so why bother? Let’s have Zuplo manage that
for us.

![Adding policy](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_22.png)

We need to add a new policy on the request side. Since we’ll be dealing with
many policies, simply type **rate** and select **Rate Limiting**:

![Choose rate limiting](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_23.png)

All we need to do here is press **Ok**. Easy, right?

![Configure policy](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_24.png)

Our rate limit has been added. Now, we just need to save and run the test three
times. On the third attempt, we’ll hit the rate limit. Of course, we can adjust
this in the policy, as shown in the image above, where **requestsAllowed** is
set to 2.

![Policy added](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_25.png)

We can do that for all the other endpoints if we want to.

Exceeding the request limit will temporarily block further data requests. We’ll
need to wait a minute before trying again.

![Rate limit exceeded](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_26.png)

So far, so good—but what if we want to prevent unauthorized access to our API?
We’ll need to implement some sort of authentication. While it’s not overly
difficult to build, it involves multiple steps, configurations, and testing.
Wouldn’t it be great to let Zuplo handle those details for us? That way, we can
focus solely on our API.

## Setting API Key Authentication

We need to navigate back to the **Policies** section and add a new policy—this
time, **API Key Authentication**:

![Add API key auth](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_27.png)

There’s not much to do here—just press **OK**.

![Configure API key auth](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_28.png)

It’s important to move the **api-key-inbound** policy to the top:

![API key auth applied](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_29.png)

If we save it and try testing the API, access will be denied:

![Unauthorized response](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_30.png)

At the top of the screen, click **Services**, then select **Configure** under
**API Key Service**:

![Go to services tab](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_31.png)

We need to create a consumer, which will function as a token:

![Create API consumer](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_32.png)

We need to set a name for the subject and specify the email or emails associated
with the key:

![Configure consumer](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_33.png)

Once we click **Save consumer**, an API key will be generated. We can either
view it or copy it:

![Consumer created](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_34.png)

Now, we can go back to **Code** → **routes.oas.json** to test our API. Here, we
need to add the authorization header and pass the **Bearer** token along with
the API key:

![200 Response](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_35.png)

Success once again! It’s working as expected! 🥳🥳🥳

If you think that doing this manually is not for you, read this:

![Automatic API keys](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_36.png)

And that’s how you enhance your API with Zuplo 😎 But wait, there’s more! As
always, we have a cherry on top.

## Developer Documentation Portal

If we click on **Gateway** at the bottom of the screen, we’ll see a link to the
Developer Portal:

![Developer portal](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_37.png)

Copy it and add /docs to the very end:

```
https://simple-blog-api-main-7f50b25.d2.zuplo.dev/docs
```

Here, we can see that every route we create will be added to the Stripe-style
Developer Portal:

![New developer portal](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_38.png)

If we log in, we can see our API keys:

![Login](/media/posts/2025-03-29-flask-api-tutorial/PythonZuplo_39.png)

## Wrapping Up

If you’d like to check out the source code for the API, you can see it on my
Github repo [SimpleBlogAPI](https://github.com/atejada/SimpleBlogAPI).

If you want to learn more about the tools I used - here are some quick
documentation links:

- [Flask User's Guide](https://flask.palletsprojects.com/en/stable/#user-s-guide)
- [Flask RestX](https://flask-restx.readthedocs.io/en/latest/)
- [Zuplo](https://zuplo.com/docs/articles/what-is-zuplo)

Now, what kind of API will you build with Flask? 😎