Web CNLearn - FastAPI 1 - Creating a Project Structure

Let’s build a FastAPI application quickly… So am I actually making this (made this) and not just claim so on my CV? Crazy right?? Like it says in the title, we are going to be using FastAPI for the backend. This is a fun project so I’m not necessarily using a “mature” framework and I will also be using some SQLAlchemy features that might break (their new async implementation) so if you follow this series do keep that in mind. If you’re up for some fun, please follow along :)

The repository for this project is available here. I will only post each commit with the posts so if you want to see the whole project at once be patient :) I am constantly making changes and when I’m somewhat satisfied, I push a commit.

Why FastAPI?

So why FastAPI? Well, it’s a new (ish) framework. I am using it for another project where it’s working great, quite light and fast. I should mention it’s based on Starlette, a great ASGI framework. In fact we will be going through the documentation of it quite a bit. I am also slightly basing the project structure on the template mentioned here and available here. But not too much, it’s my project after all :)

Project Plan

So what’s the plan?

  1. Create a new FastAPI project and give it some structure
  2. Connect a PostgreSQL database to it. We will use Alembic to create our migrations
  3. Copy the CRUD and some other functionality from the GUI CNLearn posts I’ve been writing
  4. Add users (that part will be taken from the FastAPI project template)
  5. Add FEATURES! (which ones? To be confirmed)…

Installing it

What do we need? FastAPI, uvicorn, SQLAlchemy, alembic, email-validator and python-dotenv for now.

pip install fastapi uvicorn SQLAlchemy alembic email-validator python-dotenv

I also created a requirements folder where I froze the pip list to a base.txt file. We will also have dev.txt, prod.txt and stage.txt which will be empty for now.

Project Structure

In today’s post we will simply create a project structure and add a sample API endpoint. I have the following folder structure:

Project Structure

The requirements/base.txt file is just the frozen list of dependencies. Then, let’s look at the app/api/v1/routes/sample.py file first. For now, it’s just a simple endpoint that returns some stuff:


from typing import List, Dict, Union

from fastapi import APIRouter

router = APIRouter()


@router.get("/")
async def get_words() -> List[Dict[str, Union[int, str]]]:
    """
    Returns a list of words.
    """
    words = [
        {"id": 1, "simplified": "好", "pinyin": "hǎo", "definitions": "good"},
        {"id": 2, "simplifieid": "不", "pinyin": "bù", "definitions": "no"}
    ]
    return words

I am first creating an instance of APIRouter() and using that to declare my path operations. In this case, there’s only a path at “/” (relative to the APIRouter() path) that returns a list of word dictionaries. So that’s the sample endpoint. It’s worth noting you can also add a prefix and tags to the router, but I will do that in the app/api/v1/api.py file. Let’s have a look:

from fastapi import APIRouter

from app.api.v1.routes import sample

api_router = APIRouter()
api_router.include_router(sample.router, prefix="/sample", tags=["sample"]

Once again, I am importing APIRouter and creating an instance of it. I am also importing the sample module from our v1.routes. I then use the include_router() method of an APIRouter to add another router as a sub-router. Note that here I am also using a prefix of “/sample”, meaning my get_words() function endpoint will be accessible at DOMAIN:PORT/sample/. I also added a tags list to it. Tags are a way of grouping endpoints together.

Let’s come out of the api folder and look at the main server.py file:

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from app.api.v1.api import api_router


def create_application():
    """
    Creates and returns a FastAPI instance.
    """
    app = FastAPI(
        title="Web CNLearn Backend",  # TODO: retrieve this from settings
        version="0.0.5"  # TODO: retrieve this from settings
    )
    # change CORS settings
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],  # TODO: retrieve this from settings
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    app.include_router(api_router)
    return app


app = create_application()

Why am I using a function to create our FastAPI instance? That way, we will (later) be able to pass some settings in (and will also help when overwriting some methods during testing). I create a FastAPI instance with a title and a version and change some CORS settings. Note that I am also importing the the api_router from app.api.v1.api and including that router in the FastAPI instance. then I return that app. That’s it, we have a working FastAPI application. Let’s run it. From the root directory of the project, run:

uvicorn app.server:app –reload

By default, it will be available at http://127.0.0.1:8000. Let’s check the sample endpoint at http://127.0.0.1:8000/sample/. When you open it, you should get some JSON data back. And there you go!

I know it seems like I created a lot of unnecessary folders but they will help us in the end. In the next post (already writing it) we will use Pydantic for the app settings, connect to PostgreSQL database (asynchronously and synchronously), use Alembic, and deal with authentication. Yes we’ll do authentication before we create the words/characters table. Why? So that I create an endpoint where by being superuser I can add information to the database.

You can find the commit for this post here.