Large Project Structure Using Routers
Current folder structure
So far, our project structure is quite simple:
Currrent code structure
Additionally, our main.py
file looks like this:
from fastapi import FastAPI, Query
app = FastAPI()
books = [
{
"id": 1,
"title": "Think Python",
"author": "Allen B. Downey",
"publisher": "O'Reilly Media",
"published_date": "2021-01-01",
"page_count": 1234,
"language": "English",
},
# ... (other book entries)
]
class Book(BaseModel):
id: int
title: str
author: str
publisher: str
published_date: str
page_count: int
language: str
class BookUpdateModel(BaseModel):
title: str
author: str
publisher: str
page_count: int
language: str
@app.get("/books", response_model=List[Book])
async def get_all_books():
return books
@app.post("/books", status_code=status.HTTP_201_CREATED)
async def create_a_book(book_data: Book) -> dict:
new_book = book_data.model_dump()
books.append(new_book)
return new_book
@app.get("/book/{book_id}")
async def get_book(book_id: int) -> dict:
for book in books:
if book["id"] == book_id:
return book
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
@app.patch("/book/{book_id}")
async def update_book(book_id: int,book_update_data:BookUpdateModel) -> dict:
for book in books:
if book['id'] == book_id:
book['title'] = book_update_data.title
book['publisher'] = book_update_data.publisher
book['page_count'] = book_update_data.page_count
book['language'] = book_update_data.language
return book
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
@app.delete("/book/{book_id}",status_code=status.HTTP_204_NO_CONTENT)
async def delete_book(book_id: int):
for book in books:
if book["id"] == book_id:
books.remove(book)
return {}
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
Restructuring the project
The problem here is that if we add more code to this file, our code will become messy and hard to maintain beacuse all our code will be in one file main.py
. To address this, we need to create a more organized project structure. To start, let's create a new folder called src
, which will contain an __init__.py
file to make it a Python package:
Now, create a folder named books
inside the src
directory. Inside this folder, add an __init__.py
file, a routes.py
file, a schemas.py
file, and a book_data.py
file. The routes.py
file will contain all the book routes, similar to what we created in the previous chapter. The schemas.py
file will contain the schemas that are currently in our root directory.
├── env/
├── main.py
├── requirements.txt
└── src/
|-- └── __init__.py
`-- └── books/
|-- └── __init__.py
|-- └── routes.py
|-- └── schemas.py
`-- └── book_data.py
First, let's move our books
list from main.py
to book_data.py
inside the books
directory.
books = [
{
"id": 1,
"title": "Think Python",
"author": "Allen B. Downey",
"publisher": "O'Reilly Media",
"published_date": "2021-01-01",
"page_count": 1234,
"language": "English",
},
# ... (other book entries)
]
Next, let's also move our Pydantic validation models from main.py
to the schemas.py
module inside the books
directory.
from pydantic import BaseModel
class Book(BaseModel):
id: int
title: str
author: str
publisher: str
published_date: str
page_count: int
language: str
class BookUpdateModel(BaseModel):
title: str
author: str
publisher: str
page_count: int
language: str
Now, let's update routes.py
as follows:
from fastapi import APIRouter
from src.books.book_data import books
from src.books.schemas import BookSchema, BookUpdateSchema
book_router = APIRouter()
@book_router.get("/books", response_model=List[Book])
async def get_all_books():
return books
@book_router.post("/books", status_code=status.HTTP_201_CREATED)
async def create_a_book(book_data: Book) -> dict:
new_book = book_data.model_dump()
books.append(new_book)
return new_book
@book_router.get("/book/{book_id}")
async def get_book(book_id: int) -> dict:
for book in books:
if book["id"] == book_id:
return book
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
@book_router.patch("/book/{book_id}")
async def update_book(book_id: int,book_update_data:BookUpdateModel) -> dict:
for book in books:
if book['id'] == book_id:
book['title'] = book_update_data.title
book['publisher'] = book_update_data.publisher
book['page_count'] = book_update_data.page_count
book['language'] = book_update_data.language
return book
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
@book_router.delete("/book/{book_id}",status_code=status.HTTP_204_NO_CONTENT)
async def delete_book(book_id: int):
for book in books:
if book["id"] == book_id:
books.remove(book)
return {}
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Book not found")
Introduction to FastAPI routers
What has been accomplished is the division of our project into modules using routers. FastAPI routers allow easy modularization of our API by grouping related API routes together. Routers function similarly to FastAPI instances (similar to what we have in main.py
). As our project expands, we will introduce additional API routes, and all of them will be organized into modules grouping related functionalities.
Let's enhance our main.py
file to adopt this modular structure:
# Inside main.py title
from fastapi import FastAPI
from src.books.routes import book_router
version = 'v1'
app = FastAPI(
title='Bookly',
description='A RESTful API for a book review web service',
version=version,
)
app.include_router(book_router,prefix=f"/api/{version}/books", tags=['books'])
Firstly, a variable called version
has been introduced to hold the API version. Next, we import the book_router
created in the previous example. Using our FastAPI instance, we include all endpoints created with it by calling the include_router
method.
Arguments added to the FastAPI instance are:
title
: The title of the API.description
: The description of the API.version
: The version of the API.
While these arguments may not be particularly useful at present, they become valuable when we explore API documentation with OpenAPI.
Furthermore, we added the following arguments to the include_router method:
-
prefix
: The path through which all related endpoints can be accessed. In our case, it's named the /{version}/books prefix, resulting in /v1/books or /v2/books based on the application version. This implies that all book-related endpoints can be accessed using http://localhost:8000/api/v1/books. -
tags
: The list of tags associated with the endpoints that fall within a given router.
Let us now now move all the source code in our main.py
module to src/__init__.py
. (delete your main.py
)
from fastapi import FastAPI
from src.books.routes import book_router
version = 'v1'
app = FastAPI(
title="Bookly",
description="A REST API for a book review web service",
version= version,
lifespan=life_span
)
app.include_router(book_router, prefix=f"/api/{version}/books", tags=['books'])
├── requirements.txt
├── run.py
└── src
|-- ├── books
|-- │ ├── book_data.py
|-- │ ├── __init__.py
|-- │ ├── routes.py
|-- │ ├── schemas.py
|-- │ ├── book_data.py
`-- └── __init__.py
Once more, let's start our server using fastapi dev src/
. Pay attention to the fact that this time we're specifyingsrc/
. This is because we've designated it as a package by including __init__.py
. Additionally, our FastAPI instance named app
is created there. Consequently, FastAPI will utilize it to operate our application.
Runnning our application will the following terminal output.
INFO Using path src
INFO Resolved absolute path /home/jod35/Documents/fastapi-beyond-CRUD/src
INFO Searching for package file structure from directories with __init__.py files
INFO Importing from /home/jod35/Documents/fastapi-beyond-CRUD
╭─ Python package file structure ─╮
│ │
│ 📁 src │
│ └── 🐍 __init__.py │
│ │
╰─────────────────────────────────╯
INFO Importing module src
INFO Found importable FastAPI app
╭─ Importable FastAPI app ─╮
│ │
│ from src import app │
│ │
╰──────────────────────────╯
Note:
The current organization of our API endpoints is as follows:
Endpoint | Method | Description |
---|---|---|
/api/v1/books | GET | Read all books |
/api/v1/books | POST | Create a book |
/api/v1/books/{book_id} | GET | Get a book by ID |
/api/v1/books/{book_id} | PATCH | Update a book by ID |
/api/v1/books/{book_id} | DELETE | Delete a book by ID |
Conclusion
This chapter has focused on creating a folder structure that we can use even when our project gets bigger. In the next chapter, we shall focus on database and look at how we can persist our data and use Python to manage a relational database.