# A simple Python FastAPI template with API key authentication

I’ve been dusting off my Python recently and building some APIs with [FastAPI](https://fastapi.tiangolo.com/). I had a pretty simple use case - a backend API that uses API keys for authentication. However, I couldn’t find a straightfoward tutorial. The FastAPI docs are very good but a little too in-depth; for example they bundle the creation of OAuth bearer tokens into the same code as the backend itself. So I wanted to create a template for myself that simplifies the authentication, abstracts the database stuff, and also follows a reasonable best-practices approach to project layout, modules, packages and so on. So here it is! But be warned; like I said, I’m still rusty. If you spot anything obviously terrible in the code below, please let me know (nicely).

## Project layout

We’ll start with a simple Python project layout for a FastAPI app.

```plaintext
.
├── app
│   ├── __init__.py
│   ├── auth.py
│   ├── db.py
│   ├── main.py
│   └── routers
│       ├── __init__.py
│       ├── public.py
│       ├── secure.py
├── venv
```

Hopefully this makes sense if you’re fairly comfortable with Python. `app` is our Python package in this case, `main` is our main module, while `auth` and `db` are utility modules in the package. `routers` is a subpackage containing the `public` and `secure` submodules. There’s empty `__init__.py` files aplenty to tell Python this is a proper package, and in my case I also have a `venv` directory for my virtual environment, although that isn’t required. Now let’s go into what each of these files actually does.

### main.py

Here’s our main module:

```python
from fastapi import FastAPI, Depends
from routers import secure, public
from auth import get_user

app = FastAPI()

app.include_router(
    public.router,
    prefix="/api/v1/public"
)
app.include_router(
    secure.router,
    prefix="/api/v1/secure",
    dependencies=[Depends(get_user)]
)
```

We start by importing the modules we’ll need from FastAPI, namely itself and the `Depends` module. From our own `auth` module we import a function called `get_user` which we’ll explain in a moment. From our `routers` subpackage we import `secure` and `public`. And we create our app with `app = FastAPI()`.

So what are these routers? Well, in a simple FastAPI app we could just start defining paths and responses in our main program. Here’s a super simple example:

```python
@app.get("/")
async def root():
    return {"message": "Hello World"}
```

In 3 lines we can specify that we want to return “Hello World” for every `GET` request to `/` (asynchronously to boot!). But this doesn’t scale very well.

A router is a separate set of paths and functions grouped together. They’re useful for grouping features of a much larger API together into logical pieces. In this tutorial, we’ll create two groups of paths: `secure` for paths that require an API key, and `public` for those that don’t.

Let’s look at this piece again:

```python
app.include_router(
    public.router,
    prefix="/api/v1/public"
)
```

Here we call the `include_router` function on our `app`, and pass in 2 paramters. The first, `public.router` is the `router` object from the `public` module. The second `prefix`, defines where these paths sit in the context of our overall API app. As you can see, routers make it easy to change URL prefixes and even introduce API versioning.

The next one is a bit more complex:

```python
app.include_router(
    secure.router,
    prefix="/api/v1/secure",
    dependencies=[Depends(get_user)]
)
```

Now we’re using the `router` object from the `secure` module, and we’ve added a dependency using FastAPI’s `Depends` function. The dependency is on the `get_user` function, that we’ve defined in the `auth` module. As you can probably guess, that means that all paths in this router *depend* on that function. So let’s take a look at it.

### auth.py

```python
from fastapi import Security, HTTPException, status
from fastapi.security import APIKeyHeader
from db import check_api_key, get_user_from_api_key

api_key_header = APIKeyHeader(name="X-API-Key")
 
def get_user(api_key_header: str = Security(api_key_header)):
    if check_api_key(api_key_header):
        user = get_user_from_api_key(api_key_header)
        return user
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Missing or invalid API key"
    )
```

First we import some more useful stuff from FastAPI, including methods for dealing with headers and HTTP exceptions. They’ll make more sense when we get to the part that actually uses them in a moment. And yes, I do appreciate that this is a bit circular, because next we’re importing functions from `db` which we haven’t seen yet. But before we get into that file, just know that `check_api_key` returns `True` if an API key is valid, and `get_user_from_api_key` returns a user object from a valid API key.

The main purpose of the auth module is to provide us with the `get_user` function, which as we saw in the previous file, our secure routes depend on. This function accepts one parameter - the value of `X-API-Key` that has been sent in the request. Using the `APIKeyHeader` and `Security` functions in FastAPI allows us to define the header name for our API key (and therefore populate this automatically in our OpenAPI documentation) and extract it from the header.

With the API key stored in `api_key_header`, we next call our `check_api_key` function (again, we’ll see how that works in a moment). If the response is `True`, we proceed to get some user data by calling `get_user_from_api_key` and then return it.

If `check_api_key` returned `False`, then the API key we checked wasn’t valid. So we use FastAPI’s `HTTPException` to return a suitable response, in this case a 401 error.

Now we know what has to happen for every path that *depends* on `get_user`. Before we look at those paths and routes, let’s quickly explore `db.py`, which is where we defined `check_api_key` and `get_user_from_api_key`.

### db.py

This file is basically a placeholder for whatever backend database integration you want to use. Right now, it implements the methods we needs, and contains a hard-coded set of API keys and users.

```python
api_keys = {
    "e54d4431-5dab-474e-b71a-0db1fcb9e659": "7oDYjo3d9r58EJKYi5x4E8",
    "5f0c7127-3be9-4488-b801-c7b6415b45e9": "mUP7PpTHmFAkxcQLWKMY8t"
}

users = {
    "7oDYjo3d9r58EJKYi5x4E8": {
        "name": "Bob"
    },
    "mUP7PpTHmFAkxcQLWKMY8t": {
        "name": "Alice"
    },
}

def check_api_key(api_key: str):
    return api_key in api_keys

def get_user_from_api_key(api_key: str):
    return users[api_keys[api_key]]
```

I’m sure I don’t need to tell you - **don’t do this in production!** Instead, you should incorporate whatever database connections and query methods you need to to acquire API keys and user details from your database (how you actually *create* those keys is another discussion). The actual data structures may be very different as well.

For now, we have a simple `dict` of key:value pairs for `api_keys` where the key is the actual API key, and the value is the user ID. I’m using long UUIDs for API keys, and short UUIDs for user IDs, but again this could be something very different. For the users we have another `dict` where the key is the user ID, and the value is another `dict` to hold all the important user data. Right now we’re just storing a name.

`check_api_key` simply checks that the API key exists in the list of keys it knows about. Again, you’d probably need to change this for some sort of DB lookup in production.

`get_user_from_api_key` then uses the API key to get a corresponding user ID, and returns the nested user `dict`.

So far we have all of our basic functionality in terms of API authentication. Now we just need some routes!

### routers/public.py

```python
from fastapi import APIRouter
    
router = APIRouter()
    
@router.get("/")
async def get_testroute():
    return "OK"
```

This is about as simple as it gets. In this module, all we do is define a `router` object, with a single path. When a request is made to `/`. it will return `"OK"`. Don’t forget, the *actual* path is relative, and if you refer back to `main.py` you’ll see we set up this router to run under `/api/v1/public`.

### routers/secure.py

```python
from fastapi import APIRouter, Depends
from auth import get_user
    
router = APIRouter()
    
@router.get("/")
async def get_testroute(user: dict = Depends(get_user)):
    return user
```

The secure routes are only slightly more complex. Just like in `main.py` we’re using FastAPI’s `Depends` to specify that we are dependent on a function, in this case: `get_user`. So we know this route is guaranteed secure, and can only be accessed by someone with a valid API key, because without one you won’t get through the `get_user` function without triggering the HTTP Not Authorized error we set up earlier. If you have a valid key, for now we just return the user object.

## Setup

This may vary based on your own development preferences, but the quickest way I find to set up a FastAPI app is to create a `virtualenv`, install the FastAPI package, and then use [Uvicorn](https://www.uvicorn.org/) to run the app. You may have noticed the `venv` directory earlier - I created this with:

```bash
python -mvenv venv
```

I then activated the virtual environment and installed FastAPI:

```bash
source venv/bin/active
pip install "fastapi[standard]"
```

Finally, to run my FastAPI app, I can now run this command from the `app` directory:

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

The `--reload` option will reload my code automatically if I make any changes. If everything has worked for you, you should be able to access your app at [http://127.0.0.1:8000/](http://127.0.0.1:8000/)

## Testing

First we sent a GET request with no API key to the public endpoint `/api/v1/public/`:

![A GET request to /api/v1/public/ with no API key header returns an 'OK' response.](https://cdn.hashnode.com/res/hashnode/image/upload/v1723551250791/33a3bc42-1181-4845-9324-e67d6ef025f4.png align="center")

Now let’s try the secure endpoint at `/api/v1/secure/`:

![A GET request to /api/v1/secure/ with no API key header returns an HTTP 403 error.](https://cdn.hashnode.com/res/hashnode/image/upload/v1723551252592/7f7f4521-7431-41a2-aacb-e91a9fda3601.png align="center")

The API returns a 403 error. Note, this isn’t the 401 error we coded for when an API key isn’t found in the database. FastAPI can’t even find an API key, because we haven’t specified one, so immediately returns the 403 Forbidden error.

Let’s try a random API key that is *not* in the database:

![A GET request to /api/v1/secure/ with an invalid API key header returns an HTTP 401 error.](https://cdn.hashnode.com/res/hashnode/image/upload/v1723551254352/6e450214-e395-4316-ba89-b9f3bc2c3a0f.png align="center")

This time, we’ve supplied an `X-API-Key` header, but the key we’re supplying isn’t in the database. So we return a 401 Unauthorized error (technically, we’re *unauthenticated* but these error codes were written a long time ago!)

Finally, let’s try a working API key, for the user Bob:

![A GET request to /api/v1/secure/ with an valid API key header successfully returns a username.](https://cdn.hashnode.com/res/hashnode/image/upload/v1723551256128/c0e392cf-efdd-4493-8033-c33d31d995b1.png align="center")

Success! The API key is found, checked and proven valid, and the user details are retrieved and used in the response.

## What’s next?

This is hopefully some helpful project boilerplate to get you started on your own FastAPI app. Don’t forget, we’ve hardcoded a dummy database in `db.py`, so connecting the authentication up to a real database should be the next thing we do. I’ll try to tackle this in the next post!

From there, we could continue to implement different routes and paths, which brings us onto the topics that come after authentication - authorisation and/or admission control.

Let me know what you think in the comments!
