Series: Building Quorentra CRM from Zero — A Modular, ChatGPT-Native AI CRM
Installing FastAPI, creating the first Quorentra backend application, introducing API versioning, building the health endpoint, running Uvicorn, and adding the first automated API test.

1. Introduction
In Part 3, we created the canonical Quorentra repository.
We established:
quorentra/├── backend/├── frontend/├── mcp/├── scripts/├── docs/├── docker/├── .env.example├── .gitignore└── README.md
We also created the Python virtual environment and established an important convention:
Backend commands are executed from the
quorentra/backenddirectory.
Now we are going to create the first running Quorentra component.
By the end of this article, the following request will work:
GET /api/v1/health
and return:
{ "status": "healthy", "service": "quorentra-api", "version": "0.0.2"}
That may appear modest compared with the eventual CRM architecture.
But it represents an important milestone.
For the first time:
Quorentra will be running software.
2. Starting Checkpoint
This article starts from the repository created in Part 3.
The relevant structure should be:
quorentra/│├── backend/│ ├── .venv/│ ├── app/│ │ └── __init__.py│ ├── tests/│ └── README.md│├── frontend/├── mcp/├── scripts/├── docs/├── docker/│├── .env├── .env.example├── .gitignore└── README.md
Before continuing, open PowerShell and navigate to the backend:
cd C:\Users\<your-user>\Documents\GitHub\quorentra\backend
Activate the virtual environment:
.\.venv\Scripts\Activate.ps1
Your prompt should resemble:
(.venv) PS C:\Users\<your-user>\Documents\GitHub\quorentra\backend>
Everything in this article assumes commands are executed from this directory unless stated otherwise.
3. Verify the Python Environment
Before installing anything, verify which Python interpreter is active.
Run:
python --version
Then:
where.exe python
The first path should point to something similar to:
C:\Users\<your-user>\Documents\GitHub\quorentra\backend\.venv\Scripts\python.exe
Also verify pip:
python -m pip --version
This prevents one of the most common Python development problems:
Installing packages into one Python environment while running the application with another.
4. Install the First Backend Dependencies
For this checkpoint, we only need a small dependency set.
Install FastAPI and Uvicorn:
python -m pip install fastapi "uvicorn[standard]"
We will also need testing support:
python -m pip install pytest httpx
Why httpx?
FastAPI’s testing infrastructure uses HTTPX for application-level HTTP testing.
At this point we have four important packages:
fastapiuvicornpytesthttpx
We deliberately do not install SQLAlchemy, Alembic, PostgreSQL drivers, OpenAI libraries, MCP packages, or AI dependencies yet.
Those will be introduced when we actually need them.
5. Verify FastAPI Installation
Run:
python -m pip show fastapi
You should see package information.
Also verify Uvicorn:
python -m pip show uvicorn
And pytest:
python -m pytest --version
If these commands work, the environment is ready.
6. Create the First FastAPI Application
Our current application package contains:
backend/└── app/ └── __init__.py
Create:
backend/app/main.py
From PowerShell:
New-Item app\main.py -ItemType File
The structure becomes:
backend/└── app/ ├── __init__.py └── main.py
Open main.py.
Add:
from fastapi import FastAPIapp = FastAPI( title="Quorentra API", description="Backend API for the Quorentra AI CRM.", version="0.0.2",)app.get("/")async def root() -> dict[str, str]: return { "message": "Quorentra API", "status": "running", }
We now have the smallest useful FastAPI application.
7. Understanding the Application Object
The line:
app = FastAPI(...)
creates the ASGI application.
This object will become the central FastAPI application instance.
When we later run:
uvicorn app.main:app
the expression means:
app.main:app│ │ ││ │ └── FastAPI object│ ││ └── main.py│└── app Python package
In other words:
app/main.py
contains:
app = FastAPI(...)
Understanding this notation is important because incorrect module paths frequently cause:
Error loading ASGI app.Could not import module "main".
8. Why We Run Uvicorn from backend
Our directory structure is:
quorentra/└── backend/ └── app/ └── main.py
The Python package we want to import is:
app
Therefore our working directory should normally be:
quorentra/backend
From there Python can resolve:
import app
and Uvicorn can resolve:
app.main:app
This is why our canonical development command will be executed from backend.
9. Start Quorentra for the First Time
From:
quorentra/backend
with the virtual environment active, run:
python -m uvicorn app.main:app --reload
Using:
python -m uvicorn
rather than simply:
uvicorn
has one useful advantage during development.
It ensures Uvicorn runs through the currently selected Python interpreter.
You should see output similar to:
INFO: Uvicorn running on http://127.0.0.1:8000INFO: Started reloader processINFO: Started server processINFO: Application startup complete.
Quorentra is now running.
10. Open Quorentra in the Browser
Open:
http://127.0.0.1:8000
You should receive:
{ "message": "Quorentra API", "status": "running"}
This is the first live response from the Quorentra backend.
We have reached:
Browser │ ▼FastAPI
The application does not yet have PostgreSQL, React, authentication, CRM modules, MCP, or AI.
That is fine.
The first component works.
11. FastAPI Interactive Documentation
FastAPI automatically generates OpenAPI documentation.
Open:
http://127.0.0.1:8000/docs
You should see the Swagger UI.
You can also open:
http://127.0.0.1:8000/redoc
for the alternative API documentation interface.
This automatic API documentation will become increasingly useful as Quorentra grows.
12. Why We Need API Versioning Early
Our root endpoint is useful for initial verification, but business APIs should live under a versioned namespace.
We will use:
/api/v1
Why version the API before we even have customers?
Because changing:
/api/companies
later into:
/api/v1/companies
creates unnecessary churn.
The cost of establishing the convention now is almost zero.
So our first real application endpoint will be:
GET /api/v1/health
13. Do Not Put Every Route in main.py
We could write:
app.get("/api/v1/health")
directly inside main.py.
That would work.
But if we continue doing this, main.py will eventually contain hundreds of routes.
Instead, we establish router separation immediately.
Create:
backend/app/api/
Then:
backend/app/api/v1/
From PowerShell:
mkdir app\apimkdir app\api\v1
Create package files:
New-Item app\api\__init__.py -ItemType FileNew-Item app\api\v1\__init__.py -ItemType File
Now create:
New-Item app\api\v1\health.py -ItemType File
The structure becomes:
backend/└── app/ ├── api/ │ ├── __init__.py │ └── v1/ │ ├── __init__.py │ └── health.py │ ├── __init__.py └── main.py
14. Build the Health Router
Open:
app/api/v1/health.py
Add:
from fastapi import APIRouterrouter = APIRouter()router.get("/health")async def health_check() -> dict[str, str]: return { "status": "healthy", "service": "quorentra-api", "version": "0.0.2", }
This router defines the health capability.
Notice that it does not yet contain:
/api/v1
That prefix will be applied centrally.
15. Create the API v1 Router
We do not want main.py to import every endpoint individually.
Create:
app/api/v1/router.py
From PowerShell:
New-Item app\api\v1\router.py -ItemType File
Add:
from fastapi import APIRouterfrom app.api.v1.health import router as health_routerapi_router = APIRouter()api_router.include_router( health_router, tags=["health"],)
This router will eventually aggregate all v1 API modules.
Later it might look like:
api_router.include_router(auth_router, prefix="/auth")api_router.include_router(companies_router, prefix="/companies")api_router.include_router(contacts_router, prefix="/contacts")api_router.include_router(opportunities_router, prefix="/opportunities")
But we only add routes when the associated modules exist.
16. Connect the Router to FastAPI
Now update:
app/main.py
to:
from fastapi import FastAPIfrom app.api.v1.router import api_routerapp = FastAPI( title="Quorentra API", description="Backend API for the Quorentra AI CRM.", version="0.0.2",)app.include_router( api_router, prefix="/api/v1",)app.get("/")async def root() -> dict[str, str]: return { "message": "Quorentra API", "status": "running", }
Because Uvicorn is running with:
--reload
it should automatically detect the file changes and restart.
17. Test the Health Endpoint
Open:
http://127.0.0.1:8000/api/v1/health
Expected response:
{ "status": "healthy", "service": "quorentra-api", "version": "0.0.2"}
We now have:
Browser │ ▼FastAPI │ ▼API v1 Router │ ▼Health Endpoint
This is our first versioned Quorentra API.
18. Test Through Swagger UI
Open:
http://127.0.0.1:8000/docs
You should see the health section.
Expand:
GET /api/v1/health
Click:
Try it out
Then:
Execute
You should receive HTTP:
200
and the expected JSON response.
19. Improve the Health Response with a Schema
Returning:
dict[str, str]
works, but FastAPI is strongest when API contracts are explicit.
We will therefore introduce our first Pydantic response model.
Create:
app/api/v1/schemas.py
From PowerShell:
New-Item app\api\v1\schemas.py -ItemType File
Add:
from pydantic import BaseModelclass HealthResponse(BaseModel): status: str service: str version: str
Now update:
app/api/v1/health.py
to:
from fastapi import APIRouterfrom app.api.v1.schemas import HealthResponserouter = APIRouter()router.get( "/health", response_model=HealthResponse,)async def health_check() -> HealthResponse: return HealthResponse( status="healthy", service="quorentra-api", version="0.0.2", )
This gives FastAPI an explicit API contract.
20. Why Explicit Response Models Matter
Later, Quorentra will return much more complex data.
For example:
CompanyResponseContactResponseOpportunityResponseTaskResponsePipelineSummaryResponse
Explicit schemas give us:
- validation;
- predictable serialization;
- generated OpenAPI documentation;
- clearer API contracts;
- better frontend integration;
- safer MCP adaptation later.
This small health schema establishes a pattern we will reuse throughout the project.
21. Create the First Automated Test
Manual browser testing is useful, but it is not sufficient.
We want Quorentra’s development process to become testable from the beginning.
Our current test directory is:
backend/tests/
Create:
backend/tests/api/
From PowerShell:
mkdir tests\api
Create:
New-Item tests\__init__.py -ItemType FileNew-Item tests\api\__init__.py -ItemType FileNew-Item tests\api\test_health.py -ItemType File
The backend structure becomes:
backend/├── app/│ ├── api/│ │ └── v1/│ └── main.py│└── tests/ ├── __init__.py └── api/ ├── __init__.py └── test_health.py
22. Write the Health Test
Open:
tests/api/test_health.py
Add:
from fastapi.testclient import TestClientfrom app.main import appclient = TestClient(app)def test_health_check() -> None: response = client.get("/api/v1/health") assert response.status_code == 200 assert response.json() == { "status": "healthy", "service": "quorentra-api", "version": "0.0.2", }
This test starts the FastAPI application in-process and calls the endpoint.
It does not require Uvicorn to be running separately.
23. Run the First Quorentra Test
If Uvicorn is occupying the terminal, stop it with:
Ctrl + C
Make sure you remain in:
quorentra/backend
Run:
python -m pytest
Expected result should resemble:
collected 1 itemtests/api/test_health.py . [100%]1 passed
Quorentra now has its first automated test.
24. Why python -m pytest?
You could run:
pytest
But throughout this series, we will often prefer:
python -m pytest
because it makes the interpreter relationship explicit.
The command means:
Run pytest using the currently active Python interpreter.
That reduces confusion when multiple Python installations exist on Windows.
25. Verify Python Imports
From:
quorentra/backend
run:
python -c "from app.main import app; print(app.title)"
Expected result:
Quorentra API
This small test confirms that the canonical package import works.
If this fails with:
ModuleNotFoundError: No module named 'app'
first check the current directory:
Get-Location
It should end with:
\quorentra\backend
This is precisely why we established the working-directory convention in Part 3.
26. Avoid This Uvicorn Command
From the repository root:
quorentra/
do not casually run:
uvicorn main:app --reload
There is no root-level:
main.py
Likewise, from backend, do not run:
uvicorn main:app --reload
because our application lives at:
app/main.py
The canonical command is:
python -m uvicorn app.main:app --reload
executed from:
quorentra/backend
Keeping this consistent avoids a large class of ASGI import errors.
27. Add a Development Server Script
We can now create our first utility script.
Return temporarily to the repository root:
cd ..
Create:
scripts/run_backend.ps1
using:
New-Item scripts\run_backend.ps1 -ItemType File
Add:
Set-Location "$PSScriptRoot\..\backend"& ".\.venv\Scripts\python.exe" -m uvicorn app.main:app --reload
Now, from the repository root, you can run:
.\scripts\run_backend.ps1
This script deliberately sets the correct backend working directory before starting Uvicorn.
That gives developers two valid approaches.
From backend:
python -m uvicorn app.main:app --reload
Or from the repository root:
.\scripts\run_backend.ps1
28. Add a Test Script
Create:
scripts/test_backend.ps1
Add:
Set-Location "$PSScriptRoot\..\backend"& ".\.venv\Scripts\python.exe" -m pytest
From the repository root:
.\scripts\test_backend.ps1
Expected result:
1 passed
These scripts will become useful as the project grows.
29. Create a Requirements File
We now have actual Python dependencies, so we should record them.
From the backend directory:
cd backend
Create:
requirements.txt
A simple first version can contain:
fastapiuvicorn[standard]pytesthttpx
For this tutorial series, we will initially keep the dependency file readable rather than dumping every transitive dependency into it.
Later, we can adopt more formal dependency locking if required.
30. Separate Runtime and Development Dependencies
As the project grows, it is useful to distinguish application dependencies from development/testing dependencies.
We can prepare for that now.
Create:
requirements.txtrequirements-dev.txt
Use:
requirements.txt
for runtime dependencies:
fastapiuvicorn[standard]
Use:
requirements-dev.txt
for:
-r requirements.txtpytesthttpx
This means a development environment can install everything with:
python -m pip install -r requirements-dev.txt
while a production environment can later install only:
python -m pip install -r requirements.txt
This is cleaner than mixing test tools into the production dependency set.
31. Test Dependency Reproducibility
We do not need to destroy the current virtual environment just to test this.
But the intended setup command for a new developer is now:
cd backendpython -m venv .venv.\.venv\Scripts\Activate.ps1python -m pip install --upgrade pippython -m pip install -r requirements-dev.txt
Then:
python -m pytest
This gives us the beginning of a reproducible backend setup.
32. Introduce Application Metadata
Instead of scattering values such as:
Quorentra API0.0.2
through multiple files, we can centralize simple application metadata.
Create:
app/core/
From backend:
mkdir app\coreNew-Item app\core\__init__.py -ItemType FileNew-Item app\core\constants.py -ItemType File
Add to:
app/core/constants.py
the following:
APP_NAME = "Quorentra API"APP_VERSION = "0.0.2"APP_DESCRIPTION = "Backend API for the Quorentra AI CRM."
Now update main.py:
from fastapi import FastAPIfrom app.api.v1.router import api_routerfrom app.core.constants import ( APP_DESCRIPTION, APP_NAME, APP_VERSION,)app = FastAPI( title=APP_NAME, description=APP_DESCRIPTION, version=APP_VERSION,)app.include_router( api_router, prefix="/api/v1",)app.get("/")async def root() -> dict[str, str]: return { "message": APP_NAME, "status": "running", }
Update health.py:
from fastapi import APIRouterfrom app.api.v1.schemas import HealthResponsefrom app.core.constants import APP_VERSIONrouter = APIRouter()router.get( "/health", response_model=HealthResponse,)async def health_check() -> HealthResponse: return HealthResponse( status="healthy", service="quorentra-api", version=APP_VERSION, )
Now the version has one authoritative location.
33. Why Not Build the Full Configuration System Yet?
Eventually Quorentra will have configuration such as:
DATABASE_URLSECRET_KEYACCESS_TOKEN_EXPIRE_MINUTESCORS_ORIGINSOPENAI_API_KEY
We could introduce a full Pydantic Settings architecture now.
But there is no configuration-dependent behavior yet.
We will introduce that architecture when PostgreSQL and environment-driven settings arrive.
Again:
Complexity should arrive with the requirement that justifies it.
34. Run the Test Suite Again
Run:
python -m pytest
Expected:
1 passed
Then start the server again:
python -m uvicorn app.main:app --reload
Verify:
http://127.0.0.1:8000/
and:
http://127.0.0.1:8000/api/v1/health
Both should work.
35. Current Backend Structure
At the end of this article, the backend should resemble:
backend/│├── .venv/│├── app/│ ├── __init__.py│ ││ ├── api/│ │ ├── __init__.py│ │ └── v1/│ │ ├── __init__.py│ │ ├── health.py│ │ ├── router.py│ │ └── schemas.py│ ││ ├── core/│ │ ├── __init__.py│ │ └── constants.py│ ││ └── main.py│├── tests/│ ├── __init__.py│ └── api/│ ├── __init__.py│ └── test_health.py│├── requirements.txt├── requirements-dev.txt└── README.md
And the root project contains:
scripts/├── run_backend.ps1├── test_backend.ps1└── README.md
36. Our First Request Path
We can now describe the first real Quorentra request path.
Browser │ ▼Uvicorn │ ▼FastAPI │ ▼/api/v1 │ ▼Health Router │ ▼HealthResponse │ ▼JSON
This is small.
But it establishes several patterns that will survive throughout the entire project:
- ASGI application structure;
- router separation;
- API versioning;
- Pydantic response contracts;
- test organization;
- dependency tracking;
- canonical execution commands.
37. Run All Verification Checks
Stop Uvicorn if necessary:
Ctrl + C
From:
quorentra/backend
run:
python -c "from app.main import app; print(app.title)"
Expected:
Quorentra API
Run:
python -m pytest
Expected:
1 passed
Start:
python -m uvicorn app.main:app --reload
Open:
http://127.0.0.1:8000/api/v1/health
Expected:
{ "status": "healthy", "service": "quorentra-api", "version": "0.0.2"}
If all four checks succeed, the implementation checkpoint is valid.
38. Troubleshooting
ModuleNotFoundError: No module named 'app'
First check:
Get-Location
You should be in:
quorentra\backend
Then verify:
python -c "import app; print(app)"
If this works, the package is visible.
Import "fastapi" could not be resolved
Check the selected VS Code Python interpreter.
It should be:
backend\.venv\Scripts\python.exe
Then verify from the VS Code terminal:
python -m pip show fastapi
If FastAPI is installed in the virtual environment but VS Code still reports the import as unresolved, reselect the interpreter or restart the Python language server.
Error loading ASGI app
Make sure the command is:
python -m uvicorn app.main:app --reload
and that it is executed from:
quorentra\backend
Verify:
app/main.py
exists and contains:
app = FastAPI(...)
Port 8000 Is Already in Use
Run on another port:
python -m uvicorn app.main:app --reload --port 8001
Then open:
http://127.0.0.1:8001
Pytest Cannot Import app
Again, verify that pytest is executed from:
quorentra/backend
using:
python -m pytest
Do not start by adding sys.path hacks to the tests.
Fix the environment or working directory instead.
39. Git Checkpoint
Stop Uvicorn.
Return to the repository root:
cd ..
Check:
git status
Review the files.
Make sure the virtual environment is not included.
Then:
git add .
Commit:
git commit -m "feat: add FastAPI application foundation"
Check:
git status
Expected:
nothing to commit, working tree clean
This is our second meaningful implementation checkpoint.
40. Quorentra Version
We can now consider the project:
Quorentra 0.0.2
The state is:
Repository ✓Python env ✓FastAPI ✓API routing ✓Health endpoint ✓API schema ✓Automated test ✓PostgreSQL -React -Authentication -CRM -MCP -ChatGPT -AI -
For the first time, Quorentra itself is running.
41. Acceptance Criteria
Part 4 is complete when:
✓ FastAPI is installed✓ Uvicorn is installed✓ app/main.py exists✓ FastAPI application starts✓ GET / works✓ API v1 routing exists✓ GET /api/v1/health works✓ Health response uses a Pydantic model✓ Swagger documentation works✓ pytest is configured✓ Health endpoint test passes✓ app imports correctly✓ Runtime dependencies are recorded✓ Development dependencies are recorded✓ Backend run script works✓ Backend test script works✓ Git checkpoint exists
Most importantly:
The application must actually run before moving to Part 5.
42. What We Deliberately Did Not Add
We still have no:
- PostgreSQL connection;
- SQLAlchemy;
- Alembic;
- database models;
- React frontend;
- authentication;
- CRM entities;
- MCP server;
- OpenAI integration.
That is intentional.
The current architecture is:
Developer │ ▼Uvicorn │ ▼FastAPI │ ▼Quorentra API
The next step is to give Quorentra persistence.
43. Next Article
In Part 5, we will build the PostgreSQL persistence foundation.
We will introduce:
FastAPI │ ▼SQLAlchemy │ ▼PostgreSQL
We will add:
- environment-driven application configuration;
- database URL configuration;
- SQLAlchemy;
- PostgreSQL driver;
- engine creation;
- session management;
- database dependencies;
- connection testing;
- Alembic;
- the first migration;
- database-aware health checking.
The health architecture will evolve from:
FastAPI │ ▼healthy
to:
FastAPI │ ▼Database Health Check │ ▼SQLAlchemy │ ▼PostgreSQL
Our next acceptance criterion will therefore be stronger.
Instead of merely returning:
{ "status": "healthy"}
Quorentra will have to prove that its database is actually reachable.
That will give us:
Quorentra 0.0.3 — API + Persistent Database Foundation
and move us one major step closer to the first Minimum Viable Operation:
React ↓FastAPI ↓PostgreSQL
The FastAPI foundation is now complete.
Next, Quorentra gets its database.