FastAPI Project with detailed explanation for beginners

SaloniSaloni
11 min read

FastAPI is a Python web framework for building APIs quickly and easily, and it’s especially great if we want to create apps that need to respond fast to many requests at once.

It’s called fast because it helps us code quickly and run apps with high performance.

Now, let’s get to the project.

This project is a basic FastAPI application that sets up a simple REST API demonstrating useful capabilities like filtering, sorting, and pagination to retrieve server details from a mock database. It includes key components such as modular routing, dynamic query-based filtering, environment configuration, schema validation with Pydantic, and dependency management via Poetry. This API organizes and manages server data straightforwardly, making it suitable for building more structured applications with customizable data access.

Following are the steps you need to follow:

  1. Setting Up the Project with Poetry

Poetry is a tool in Python for managing dependencies and virtual environments. It makes it easy to set up, manage, and share Python projects without needing to juggle multiple tools or manually manage dependencies.

Step 1: Initialize the Project

We used poetry init to create a pyproject.toml file, which is a configuration file in Python projects. It defines the project's settings, dependencies, and other details in a standardized way. We can think of it as a "home base" for managing our project's setup. Instead of using multiple configuration files, pyproject.toml provides a single file to handle.

  • [tool.poetry] → contains information about the project like its name, version, and description.

  • [tool.poetry.dependencies] → lists the packages that the project depends on.

  • [build-system] → define the build settings telling tools how to package or build the project.

Step 2: Adding Dependencies

We used poetry add fastapi uvicorn python-dotenv pydantic to add all the mentioned dependencies.

  • fastapi → A high-performance Python framework for building APIs quickly and efficiently, with easy integration of async programming.

  • uvicorn → A server used to run FastAPI applications, providing high-performance and compatibility with asynchronous code.

  • python-dotenv → A tool for loading environment variables from a .env file making configuration management easier and more secure.

  • pydantic → A data validation library used by FastAPI to ensure data types and structure are correct, based on Python type hints.

This is how our pyproject.toml file will look like:

[tool.poetry]
name = "fastapi-base"
version = "0.1.0"
description = ""
authors = ["Saloni <saloni17.in@gmail.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.13"
fastapi = "^0.115.4"
uvicorn = "^0.32.0"
python-dotenv = "^1.0.1"
pydantic = "^2.9.2"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
  1. Creating a Basic FastAPI Application

The main.py file initializes the FastAPI app and defines routes.

Okay… now what is routes?

In FastAPI or web applications in general, a route is simply a path or URL that connects to a specific function in our code. This function defines what should happen when someone visits that path. We can think of it like a map that says, “If someone goes to this URL, run this code.”

from fastapi imprt FastAPI
app = FastAPI()
@app.get("/")
async def root():
    return{"message": "Server is up and running fine."}
  • FastAPI() → sets up the FastAPI app.

  • @app.get(“/”) → creates a GET endpoint (route combined with an HTTP method) at the root URL /. When accessed, it responds with {"message": "Server is up and running fine."}.

  1. Running the Application

To start the server, we will use poetry run uvicorn main:app --reload --host 127.0.0.1 --port 8000

  • poetry run → uses poetry to run the command within the project’s environment, ensuring all dependencies are available.

  • uvicorn main:ap → tells uvicorn to start the FastAPI app.

    main is the filesystem (main.py), and app is the FastAPI instance within it.

  • -- reload” → enables auto-reloading, so the server restarts with each code change.

  • -- host 0.0.0.0” → sets the server to listen on localhost

  • -- port 8000” → runs the server on port 8000, making it accessible at http://localhost:8000.

  1. Configuring Environment-Specific Settings

Centralizing app settings makes it easy to manage different environments (dev & prod).

Step 1: Create a .env file with environment-specific settings, such as:

TITLE="Server API"  
DESCRIPTION="API for retrieving server details"
OPENAPI_PREFIX=""

Step 2: Load Settings in config.py:

We will keep it inside the “core” folder, and this is how our core/config.py file will look like:

import os
from dotenv import load_dotenv
from pydantic import BaseSettings

load_dotenv()

class GlobalConfig(BaseSettings):
    title: str = os.environ.get("TITLE")
    version: str = "1.0.0"  
    description: str = os.environ.get("DESCRIPTION") 
    openapi_prefix: str = os.environ.get("OPENAPI_PREFIX")  
    docs_url: str = "/docs"  
    redoc_url: str = "/redoc"  
    openapi_url: str = "/openapi.json"
    api_prefix: str = "/api"

settings = GlobalConfig()
  • dotenv → loads varibales from .env.

  • GlobalConfig → A Pydantic model that sets default value if .env is missing values.

(A Pydantic model is very useful for handling data in FastAPI. It is a way to define the shape and structure of the data in Python, especially for APIs, e.g., The title is a string, pydantic will automatically check and enforce these rules when data is received, helping to catch mistakes and keep data clean.)

Now let’s look at the project structure achieved so far,

Now if we go to the documentation of our app http://localhost:8000/docs we can see the attributes we have customized:

  1. Adding Routes for Server Details

For large projects, organizing routes in separate files improves readability.

i. Define server.py for server-related routes:

Before that, let’s provide our server details, /DB/data.py

  • /api/routes/server.py
from fastapi import APIRouter
from DB import fake_db
router = APIRouter()

@router.get("")
async def get_servers():
    return fake_db
  • APIRouter from FastAPI → helps organize and modularize routes.

  • router = APIRouter() → initializes a new router for defining routes, which will later be connected to the main FastAPI app, allowing us to keep the routes organized.

  • @router.get(““) → sets up a GET endpoint. Since there’s an empty string as a path, it will inherit the prefix defined in another file. (/server define in router.py)

  • async def get_servers() → defines asynchronous function called get_servers that will run whenever the endpoint is accessed. Making it async helps handle request more efficiently, especially when there are multiple requests to the server.

Now we will add this resource to our main router file:

ii. /api/router.py

from fastapi import APIRouter  
from api.routes.server import router as server_router  

router = APIRouter()  
router.include_router(server_router, prefix="/server")

The purpose of this file is to collect and organize all API routes from different modules to make it easy to manage routes in one place.

  • from api.routes.server import router as server_router → imports the router from server.py and rename it to server_router for clarity.

  • router.include_router(server_router, prefix=”/server”) → adds the routes defined in server_router and gives them the prefix /server. So @router.get(““) in server.py now becomes /server when accessed.

Finally, we will add our router to our application,

  1. Including the Router in the Main App (main.py)

Now, we will sets up the main FastAPI application, connects configuration settings, and includes all routes.

from fastapi import FastAPI
from core.config import settings
from api.router import router

app = FastAPI(
    title=settings.title,
    version=settings.version,
    description=settings.description,
    openapi_prefix=settings.openapi_prefix,
    docs_url=settings.docs_url,
    openapi_url=settings.openapi_url
)
app.include_router(router, prefix=settings.api_prefix)

@app.get("/")
async def root():
    return {"Message": "Server is up and running fine."}
  • app = FastAPI(…) → creates FastAPI app with settings specified in config.py

  • app.include_router(router, prefix=settings.api_prefix) → includes all routes from api/router.py and attaches the prefix settings.api_prefix (/api), making routes accessible as /api/server.

Now, if we review the documentation of our app ,we can see our new endpoint,

And, if we go to http://localhost:8000/api/server

  1. Schemas for Data Validation (schemas/server.py)

Pydantic schemas validate data to ensure consistency.

from pydantic import BaseModel

class ServerRead(BaseModel):
    hostname: str
    class_: str
  • ServerRead → ensures each server entry has hostname and class_ fields as strings.
  1. Using the Schema in the Endpoint

We will use the schema to validate data sent or returned by an endpoint in api/routes/server.py

from fastapi import APIRouter, status
from schemas.server import ServerRead
from DB.data import fake_db

router = APIRouter()
@router.get(
        "",
        response_model=list[ServerRead],
        status_code=status.HTTP_200_OK,
        name="get_servers"
)
async def get_servers() -> list[ServerRead]:
    return [ServerRead(**server)for server in fake_db]

i. Imports:

  • APIRouter and status from FastAPI:

    → status provides HTTP status codes to specify response status.

  • ServerRead from schemas.server:

    → this is a pydantic model (schema) that defines the structure of the server data to ensure it meets specific requirements.

  • fake_db from DB.data:

    → a mock databade of server information for testing purposes.

ii. Router Setup:

  • router = APIRouter()

iii. Defining the Endpoints:

  • router.get(…) → sets up a GET request endpoint to retrieve server details.

    response_model = list[ServerRead] → specifies that the response will be a list of ServerRead objects, ensuring the returned data matches the ServerRead schema structure.

    status_code = status.HTTP_200_0K → sets the reponse status to 200, indicating success.

    name = “get_servers” → names the route, which will be useful in documentation.

iv. Function to Get Servers:

  • async def get_servers() - > list[ServerRead] → asynchronously defines a function that returns a list of server information.

  • return [ServerRead(**server) for server in fake_db] → this line creates a list of ServerRead instances by iterating over fake_db. Each dictionary in fake_db is unpacked with **server to match the fields in ServerRead.

So, in the documentation of our app, we can see the attributes.

  1. Retrieve Server Information with Optional Filters

Before getting into filtering and stuff, let’s add some more server-related data in our fake_db, so it will be fun when we retrieve server information.

i. Update fake_db in DB/data.py

fake_db = [
    {    
        "hostname": "slc0001", 
        "server_class": "Linux Server",
        "ip_address": "192.168.1.10",
        "server_state": "Installed",
        "patching_group": "P1-Premium-2nd-Saturday",
        "business_owner": "saloni.saloni@wipro.com",
        "os_version": "RHEL 7.9",
        "kernel_version": "3.10.0-1160.118.1.el7.x86_64",
        "service_status": {
            "nfs": "active",
            "ldap": "inactive",
            "sshd": "active"
        },
        "installed_agent": {
            "splunk_agent": "yes",
            "mdatp_agent": "no",
            "illumio_agent": "yes",
            "commvault_agent": "yes"
        }
    },
    {
        "hostname": "slc0012", 
        "server_class": "Linux Server",
        "ip_address": "192.168.1.12",
        "server_state": "Retired",
        "patching_group": "P6-Premium-2nd-Saturday",
        "business_owner": "xyz@wipro.com",
        "os_version": "RHEL 9.4",
        "kernel_version": "5.14.0-427.42.1.el9_4.x86_64",
        "service_status": {
            "nfs": "active",
            "ldap": "active",
            "sshd": "active"
        },
        "installed_agent": {
            "splunk_agent": "yes",
            "mdatp_agent": "yes",
            "illumio_agent": "yes",
            "commvault_agent": "yes"
        }
    },
    {
        "hostname": "slc0023", 
        "server_class": "Linux Server",
        "ip_address": "192.168.1.13",
        "server_state": "Installed",
        "patching_group": "P12-Premium-2nd-Saturday",
        "business_owner": "abc@wipro.com",
        "os_version": "SUSE 15 SP5",
        "kernel_version": "5.3.18-150300.59.174.1-default",
        "service_status": {
            "nfs": "active",
            "ldap": "inactive",
            "sshd": "inactive"
        },
        "installed_agent": {
            "splunk_agent": "yes",
            "mdatp_agent": "no",
            "illumio_agent": "no",
            "commvault_agent": "yes"
        },
    },
    {
        "hostname": "defreon01", 
        "server_class": "Windows Server",
        "ip_address": "200.168.1.13",
        "server_state": "Installed",
        "patching_group": "P12-Premium-2nd-Saturday",
        "business_owner": "utkarsh@wipro.com",
        "os_version": "Windows",
        "kernel_version": "10.0",
        "service_status": {
            "nfs": "inactive",
            "ldap": "inactive",
            "sshd": "inactive"
        },
        "installed_agent": {
            "splunk_agent": "yes",
            "mdatp_agent": "no",
            "illumio_agent": "no",
            "commvault_agent": "no"
        }
    }
]

ii. Update the ServerRead Model in schemas/server.py

This schema needs to match the structure of fake_db an includes nested types for service_status and installed_agent.

from pydantic import BaseModel
from typing import Optional, Dict

class ServiceStatus(BaseModel):
    nfs: str
    ldap: str
    sshd: str

class InstalledStatus(BaseModel):
    splunk_agent: str
    mdatp_agent: str
    illumio_agent: str
    commvault_agent: str

class ServerRead(BaseModel):
    hostname: str
    server_class: str
    ip_address: str
    server_state: str
    patching_group: str
    business_owner: str
    os_version: str
    kernel_version: str
    service_status: ServiceStatus
    installed_agent: InstalledStatus

iii. Update the /api/routes/server.py Route

Now, we will define the API endpoint to retrieve server details from our predefined database (fake_db). By specifying optional query parameters (hostname and class), we can filter the results to view only the servers that match those criteria, making it a flexible tool for retrieving specific server information.

from fastapi import APIRouter, Query, status
from typing import Optional, List
from schemas.server import ServerRead
from DB.data import fake_db

router = APIRouter()
@router.get(
        "",
        response_model=List[ServerRead],
        status_code=status.HTTP_200_OK,
        name="get_servers"
)
async def get_servers(
        hostname: Optional[str] = Query(None),
        server_class: Optional[str] = Query(None)
        ) -> List[ServerRead]:
        return [
                server for server in fake_db
                if (hostname is None or server["hostname"] == hostname) and (server_class is None or server["server_class"] == server_class)
        ]

Imports:

  • Query → enables query parameter handling with extra configurations, such as default values.

  • Optional and List → these are type hints from the typing module.

    Optional means that a parameter can be None and List represents a list data type.

Router Definition:

  • hostname and server_class are optional query parameter (Optional[str]) that defaults to None.

List[ServerRead] → specifies that the function returns a list of ServerRead objects, matching the expected output format defined in schemas/server.py.

Filtering Logic:

We are using a list comprehension to filter fake_db:

  • each server entry is included if it matches the hostname and server_class values provided in the query parameters.

  • if either hostname or server_class is None, that criterion is ignored, so the filter applies only if the user has specified that parameter.

The filtered list is returned as the response.

Now, we will check this API, if we run the server again and access the app and documentation of our app, this is how it will work:

Firstly, we can see our updated server information in http://localhost:8000/api/server

Then, if we go to http://localhost:8000/docs

We are now getting two parameters hostname and server_class. We can filter out servers information with these parameters like shown below:

We can see the same information using http://localhost:8000/api/server?hostname=slc0001 in our browser.

and with http://localhost:8000/api/server?server_class=Windows%20Server

Thank you for following along with this project! It’s a simple yet powerful one, with a clean setup and easy testing.

Stay tuned for an upcoming post where I’ll dive into a more advanced FastAPI project with additional features.

Keep Learning :)

0
Subscribe to my newsletter

Read articles from Saloni directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Saloni
Saloni