First Steps in AI Solutions Engineering

Design a C4 diagram first, then build a FastAPI AI service that matches it.

Introduction

30 Second Summary

Many aspiring AI engineers jump straight into prompts and API calls. The endpoint works, but the code can quickly lose the structure the system was designed to have.

In this project, you'll work like an AI solutions engineer by designing an AI service with a C4 diagram, placing Gemini behind clear Python boundaries, and using CI to catch when those boundaries drift from the design. In the optional Secret Mission, you'll ground Gemini in the architecture context so the service can answer questions about its own design.

What You'll Build

You will build a GitHub repository where the architecture, Python modules, automated checks, and live AI endpoints all tell the same system story.

By the end of this project, you'll have:

  • 🗺️ A C4 system design that renders in your README and maps every application container to a Python file.
  • 💬 A live /ask endpoint that routes questions through FastAPI, an orchestrator, and the Gemini model provider.
  • 🚦 An architecture contract in CI that rejects code when its module boundaries drift from the diagram.
  • 🛡️ Secret Mission: Build an Ask Your Architecture endpoint that grounds Gemini in your system's own C4 context.

Are there any prerequisites?

No prior experience with architecture diagrams or AI APIs is needed. This guide includes full setup for every required tool, including Python, Git, and Cursor.

Before We Start

Before writing any code or diagrams, let's lock in what you're about to build and why it matters.

Set Up Your Project

Before you draw a single box on a diagram or write a single line of code, you need to setup the Infrastructure's dependencies. This step gets your Python environment, dependencies, API access, and version control ready to go.

You will install Python, create a project folder with a virtual environment, grab a free Gemini API key, and push your first commit to GitHub.

In this step, get ready to:
  • Install Python and create a project folder with dependencies.
  • Get a Gemini API key and verify the SDK works.
  • Initialize a Git repository and push to GitHub.
Install Python and create your project

First, open a terminal. This is where you will run every command in the project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.

Python 3.10 or higher is the minimum version for this project. Choose your operating system to check the version installed on your computer.

macOS

  • Check your Python version by running this command:
python3 --version

You should see Python 3.10 or a higher version number.

Windows

  • Check your Python version in PowerShell by running this command:
py --version

You should see Python 3.10 or a higher version number.

Linux

  • Check your Python version by running this command:
python3 --version

You should see Python 3.10 or a higher version number.

Python 3.10 or higher is the minimum version for this project. The command differs between Windows and macOS or Linux.

✔️ I see version 3.10 or higher

You are good to go. Move on to creating your project folder below.

ⓧ I see an older version

You have Python installed, but you need version 3.10 or higher for this project.

  • Open python.org/downloads in your browser.
  • Download the latest stable Python 3 installer for your operating system.
  • Run the installer.
  • On Windows, select Add Python to PATH.
  • On Windows, click Install Now.
  • Complete the installation prompts for your operating system.
  • Restart your terminal after the installation finishes.
  • Return to your operating system tab above and run its version command again.

macOS default Python

macOS may ship with an older Python that shadows the version you install. If python3 --version still shows the old version after installing, try running the full path (e.g. /usr/local/bin/python3 --version) or use a version manager like pyenv.

ⓧ Command not found

Python is not installed yet. No problem.

  • Open python.org/downloads in your browser.
  • Download the latest stable Python 3 installer for your operating system.
  • Run the installer.
  • On Windows, select Add Python to PATH.
  • On Windows, click Install Now.
  • Complete the installation prompts for your operating system.
  • Restart your terminal after the installation finishes.
  • Return to your operating system tab above and run its version command again.
  • Move to your Desktop Director
  • Create your project folder and move into it by running these commands:
cd ~/Desktop
mkdir ai-architecture-nextwork
cd ai-architecture-nextwork

A virtual environment keeps your project's packages separate from the rest of your system. This prevents version conflicts between different projects.

  • Create and activate a virtual environment by running the commands for your operating system:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

py -m venv venv
.\venv\Scripts\Activate.ps1

Linux

python3 -m venv venv
source venv/bin/activate

Your terminal should return to the prompt with (venv) at the beginning. That confirms the virtual environment is active.

Don't see (venv) in your prompt?

  • Confirm that you used the activation command shown for your operating system.

Help me activate my Python virtual environment.

Now you need a requirements.txt file to declare the project's dependencies. This file tells pip exactly which packages to install.

  • Create requirements.txt and add the project dependencies by running the commands for your operating system:

macOS

cat > requirements.txt << 'EOF'
fastapi[standard]==0.141.1
google-genai==2.19.0
python-dotenv==1.2.3
EOF

Windows

@"
fastapi[standard]==0.141.1
google-genai==2.19.0
python-dotenv==1.2.3
"@ | Set-Content requirements.txt

Linux

cat > requirements.txt << 'EOF'
fastapi[standard]==0.141.1
google-genai==2.19.0
python-dotenv==1.2.3
EOF

You should see your terminal return to the prompt with no error. That confirms the file was created with all three dependencies.

What are these packages?

FastAPI is a modern Python web framework for building APIs. The [standard] extra bundles everything you need to run a development server, including the fastapi CLI.

google-genai is Google's current Python SDK for Gemini.

python-dotenv loads the API key from a local .env file when the app starts.

✔️ Awesome, I've got everything!

Great. Double-check that you saved the file before installing the dependencies.

ⓧ I'd like to double check the full code

fastapi[standard]==0.141.1
google-genai==2.19.0
python-dotenv==1.2.3
  • Install the dependencies by running:
pip install -r requirements.txt

You should see output scrolling as pip downloads and installs the packages. The final line will say Successfully installed followed by a list of package names.

Install failed or pip not found?

  • Make sure your virtual environment is active. You should see (venv) in your terminal prompt before running pip.
  • If you see pip: command not found, try pip3 install -r requirements.txt instead.
  • help me fix my pip install for this FastAPI and google-genai project
Get a Gemini API key and verify the SDK

A Gemini API key lets the app call Gemini from Python. Choose the tab that matches your situation.

✔️ I already have a Gemini API key

Use your existing Gemini API key. Keep it private and ready for Step 3, where you add it to a local .env file.

ⓧ I need a Gemini API key

The Gemini API is free to use, and no credit card is required.

You need a Google account and an API key from Google AI Studio.

  • Click Get API key in the left panel to open the API keys page.
  • Click Create API key.
  • Enter AI Architecture as the key name if Google AI Studio asks for one.
  • Copy the newly generated API key.

Why is this free?

New Google AI Studio accounts start on the Free Tier with no billing account linked. Input and output tokens are free of charge on this tier. You will not be charged unless you manually upgrade to the Paid Tier.

  • Store your API key in a password manager or another private location.
  • Keep the key ready for Step 3, where you add it to a local .env file.
  • Never paste the key into GitHub, screenshots, or support messages.

SDK installation check

  • Verify the SDK can load by running:
python -c "from google import genai; print('SDK ready')"

Seeing an ImportError?

  • Make sure you are inside the virtual environment ((venv) should appear in your prompt). If not, run the activate command again.
  • Double check that pip install -r requirements.txt completed without errors. If it did not, run it again.
  • If you see ModuleNotFoundError: No module named 'google', your terminal may be using a different Python than the one in your virtual environment. Run which python (macOS) or where python (Windows) to confirm it points inside the venv folder.
  • help me fix this google-genai import error
Initialize git and push to GitHub

Version control tracks every change you make. It also lets you host the project on GitHub where the Mermaid diagrams you build in the next step will render visually.

  • Check if Git is installed by running:
git --version

✔️ I see a version number

Git is ready. Move on to creating your first commit below.

ⓧ Command not found

Git is not installed yet.

macOS

  • Open Apple's Command Line Tools installer by running this command. A system dialog will open:
xcode-select --install

The download can take several minutes while macOS installs the tools. A quiet progress bar is normal.

  • Click Install in the system dialog.
  • Wait for the installation to finish.
  • Confirm Git is ready by running:
git --version
Git still unavailable?
  • If no dialog opens, macOS may already have the tools. Run git --version again to check.
  • Help me fix the macOS Git installation.

Windows

  • Install Git from PowerShell by running this command. Your first WinGet installation may ask you to accept source agreements:
winget install --id Git.Git -e --source winget
  • Enter Y if PowerShell asks you to accept a source agreement.
  • Allow the installer if Windows asks for permission.
  • Confirm Git is ready by running:
git --version
Command still not working?
  • If PowerShell cannot find winget, install Git from the official Git for Windows page.
  • If installation succeeds but git --version fails, close PowerShell and open it again.
  • Help me fix the Windows Git installation.

Linux

Linux distributions use different package managers. Choose the command that matches your distribution.

Ubuntu or Debian
  • Install Git on Ubuntu or Debian by running:
sudo apt-get install git
Fedora 22 or later
  • Install Git on Fedora by running:
sudo dnf install git
  • Enter your password if the terminal asks for it.
  • Enter Y if the package manager asks for confirmation.
  • Confirm Git is ready by running:
git --version
Git still unavailable?
  • If your distribution uses another package manager, find its command on the official Git for Linux page.
  • Help me fix the Linux Git installation.
  • Initialize a git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit"

You should see output confirming the initial commit, with requirements.txt listed as a new file.

Seeing a git config error?

  • If Git asks you to set your name and email, run these two commands first (replacing the placeholder values with your own):

Help me fix my Git identity settings.

Creating a repository requires a signed-in GitHub account with a verified email address. Choose the tab that matches your account status.

✔️ I already have a GitHub account

Create your repository
  • Open github.com/new in your browser.
  • Select your personal account from the Owner dropdown menu.
  • Enter ai-architecture-nextwork in the Repository name field.
  • Enter Design an AI solution architecture and turn it into a working FastAPI endpoint in the Description field.
  • Select Public under repository visibility.
  • Leave the README, .gitignore, and license initialization options unselected.
  • Click Create repository.

ⓧ I need a GitHub account

A personal GitHub account is free. GitHub requires a verified email address before you can create a repository.

  • Open github.com/signup in your browser.
  • Follow GitHub's prompts to create your personal account.
  • Open the verification email from GitHub.
  • Click the verification link to confirm your email address.
Create your repository
  • Open github.com/new in your browser.
  • Select your personal account from the Owner dropdown menu.
  • Enter ai-architecture-nextwork in the Repository name field.
  • Enter C4 architecture to working FastAPI endpoint in the Description field.
  • Select Public under repository visibility.
  • Leave the README, .gitignore, and license initialization options unselected.
  • Click Create repository.
  • Find the Quick setup panel on the new repository page.
  • Select HTTPS above the repository URL.
  • Click the copy button beside the URL.
  • Switch back to your terminal
  • Connect your local repo to GitHub and push by running these commands (replacing the URL with your own repository URL):
git remote add origin [[REPO_URL="https://github.com/your-username/ai-architecture-nextwork.git"]]
git branch -M main
git push -u origin main
  • Refresh your GitHub repository page in the browser.

You should see requirements.txt listed in the repository. Your first commit is live.

Push failed or permission denied?

  • Double check that the repository URL in your git remote add command matches the URL shown on the GitHub setup page exactly.
  • If a browser window opens asking you to authorize Git, sign in with your GitHub account and click Authorize.
  • If the terminal asks for a password, GitHub no longer accepts account passwords for HTTPS. Generate a personal access token at github.com/settings/tokens (select the repo scope), then paste it when prompted instead of your password.
  • help me push my code to GitHub

Your environment is ready. Next up, you will draw your C4 architecture diagram before writing a single line of application code.

Draw Your Architecture First

Your project folder is set up, your dependencies are installed, and your first commit is live on GitHub.

AI solutions engineers design the path around a model before they write the integration. This step turns that path into a visible contract that your code and automation can enforce later.

In this step, get ready to:
  • Protect local files and secrets with a .gitignore file.
  • Design a Level 2 C4 diagram whose aliases match the Python filenames.
  • Publish the diagram and ignore rules to GitHub.
Protect local files from Git

A .gitignore file tells Git which local files must stay out of the repository. You will create it in Cursor before adding the architecture diagram.

You will use Cursor to create .gitignore and every project file that follows. Confirm it is installed before opening ai-architecture-nextwork from the terminal.

  • Choose the tab that matches your Cursor setup.

✔️ I already have Cursor

Cursor is ready. Keep the app closed so the command below opens the correct project folder.

ⓧ I need to install Cursor

Cursor is not installed yet. Choose your operating system below and install it.

macOS

  • Open the Cursor download page.
  • Click Download for Mac.
  • Open the downloaded .dmg file.
  • Drag Cursor into the Applications folder.
  • Press Cmd+Space to open Spotlight.
  • Type Cursor and press Enter to open it.
  • Close Cursor after it opens.

Windows

  • Open the Cursor download page.
  • Click Download for Windows.
  • Open the downloaded .exe file.
  • Follow the installer prompts to add Cursor to your computer.
  • Press the Windows key to open search.
  • Type Cursor and press Enter to open it.
  • Close Cursor after it opens.

Linux

  • Open the Cursor download page.
  • Choose the package for your Linux distribution or download the AppImage.
  • Follow the installation steps shown for your chosen package.
  • Open your application launcher.
  • Search for Cursor.
  • Open Cursor.
  • Close Cursor after it opens.
  • Open the ai-architecture-nextwork folder in Cursor by running this command:
cursor . --classic

You should see ai-architecture-nextwork at the top of the Explorer sidebar.

  • Click New File... in Cursor's Explorer sidebar.
  • Enter .gitignore as the filename.
  • Press Enter to create and open the file.
  • Copy all the content from the code block below.
  • Paste it into the new .gitignore file.
venv/
.env
__pycache__/

What do these rules protect?

  • venv/ keeps the machine-specific virtual environment out of the repository.
  • .env prevents local secrets from being committed.
  • __pycache__/ excludes Python's generated bytecode.
  • Save .gitignore by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm that .gitignore is listed in the Explorer sidebar.

Your ignore rules are now in place. The virtual environment and future API key stay out of every commit.

Files still appearing in Git?

  • Check that the filename starts with a dot and contains no extra extension.
  • Check that each ignore rule appears on its own line.
  • Help me troubleshoot my .gitignore file.
Create the C4 Container diagram

The C4 model gives you four zoom levels. Level 2 is the useful view here because it exposes the Python modules around the model without dropping into classes and functions.

| Level | View | Main question |
| --- | --- | --- |
| 1 | System context | Who uses the system? |
| 2 | Containers | What are its major building blocks? |
| 3 | Components | What lives inside each building block? |
| 4 | Code | How is each component implemented? |

You will express Level 2 with Mermaid so the diagram stays beside the code in README.md. Each internal alias matches its filename: main, orchestrator, and model_provider.

Why Mermaid instead of a drawing tool?

Drawing tools store architecture as separate image files. Those files are easy to forget when the code changes.

Mermaid keeps the diagram beside the code in README.md. You can update both in the same Git commit.

  • Click New File... in Cursor's Explorer sidebar.
  • Enter README.md as the filename.
  • Press Enter to create and open the file.
  • Copy all the content from the code block below.
  • Paste it into the new README.md file.
# AI Question Answering Service

## Architecture

```mermaid
%%{init: {"c4": {"c4ShapeMargin": 110, "c4ShapeInRow": 3}}}%%
C4Container
    title C4 Level 2 - AI Question Answering Service

    Person(user, "User", "Sends questions via HTTP")
    System_Ext(genai, "Google Gemini API", "External LLM service")

    System_Boundary(system, "AI Question Answering Service") {
        Container(main, "API Container", "FastAPI", "Receives HTTP requests and returns responses")
        Container(orchestrator, "Prompt Orchestrator", "Python module", "Builds prompts from user and system context")
        Container(model_provider, "Model Provider", "Python module", "Calls the LLM and returns generated text")
    }

    Rel(user, main, "POST /ask", "HTTPS")
    Rel(main, orchestrator, "User question")
    Rel(orchestrator, model_provider, "LLM prompt")
    Rel(model_provider, genai, "generate_content()", "HTTPS")

    UpdateRelStyle(user, main, $offsetX="-45", $offsetY="-30")
    UpdateRelStyle(main, orchestrator, $offsetX="-40", $offsetY="-10")
    UpdateRelStyle(orchestrator, model_provider, $offsetX="-45", $offsetY="-10")
    UpdateRelStyle(model_provider, genai, $offsetX="10")
```

What does this diagram describe?

  • Person(user, ...) defines the person sending HTTP requests.
  • System_Boundary(system, ...) groups the code you own.
  • Container(main, ...), Container(orchestrator, ...), and Container(model_provider, ...) map directly to Python files.
  • System_Ext(genai, ...) marks Gemini as an external system.
  • Rel(...) traces a question from the user to Gemini through each boundary.

✔️ I've got everything

Great. Double-check that you saved README.md before you push it to GitHub.

ⓧ I'd like to see the full code

# AI Question Answering Service

## Architecture

```mermaid
%%{init: {"c4": {"c4ShapeMargin": 110, "c4ShapeInRow": 3}}}%%
C4Container
    title C4 Level 2 - AI Question Answering Service

    Person(user, "User", "Sends questions via HTTP")
    System_Ext(genai, "Google Gemini API", "External LLM service")

    System_Boundary(system, "AI Question Answering Service") {
        Container(main, "API Container", "FastAPI", "Receives HTTP requests and returns responses")
        Container(orchestrator, "Prompt Orchestrator", "Python module", "Builds prompts from user and system context")
        Container(model_provider, "Model Provider", "Python module", "Calls the LLM and returns generated text")
    }

    Rel(user, main, "POST /ask", "HTTPS")
    Rel(main, orchestrator, "User question")
    Rel(orchestrator, model_provider, "LLM prompt")
    Rel(model_provider, genai, "generate_content()", "HTTPS")

    UpdateRelStyle(user, main, $offsetX="-45", $offsetY="-30")
    UpdateRelStyle(main, orchestrator, $offsetX="-40", $offsetY="-10")
    UpdateRelStyle(orchestrator, model_provider, $offsetX="-45", $offsetY="-10")
    UpdateRelStyle(model_provider, genai, $offsetX="10")
```
  • Save README.md by pressing Cmd+S.
  • Click Terminal in the top menu.
  • Click New Terminal.
Push your diagram to GitHub

Your diagram and ignore rules exist locally, but nobody else can see them yet. Commit and push both files so the repository carries its design and safety rules together.

  • Stage, commit, and push README.md and .gitignore by running these commands:
git add README.md .gitignore
git commit -m "Add C4 diagram and gitignore"
git push

What do these commands do?

  • git add README.md .gitignore stages both project files for the commit.
  • git commit -m "Add C4 diagram and gitignore" saves the diagram and ignore rules in one snapshot.
  • git push sends the commit to your remote GitHub repository.

Before you open your repository on GitHub, what do you expect to see in the README? Raw code, or a rendered diagram?

  • Open github.com in your browser.
  • Sign in to the GitHub account you used in Step 1 if prompted.
  • Open the ai-architecture-nextwork repository you created in Step 1.
  • Confirm that .gitignore appears in the repository file list.
  • Scroll down to the README section on the repository page.

You should see a rendered C4 diagram with the user, three internal containers, and the Gemini API connected by arrows. That visible design is the contract your code must now follow.

Seeing raw code instead of a diagram?

  • Confirm that the opening fence is exactly ```mermaid.
  • Confirm that the opening and closing triple backticks each sit on their own line.
  • Refresh GitHub with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
  • Help me troubleshoot the Mermaid rendering.

Your architecture is drawn and visible to anyone who visits your repository. Next up, you will build a working endpoint and discover firsthand how quickly code drifts from the diagram.

Build the Endpoint (Take the Shortcut)

Your architecture diagram is already live on GitHub, showing three distinct containers inside the AI Question Answering Service. The next step is proving that architecture is actually buildable with real, working code.

A quick monolith proves the request path before you invest in boundaries. The shortcut also creates the exact design mismatch an AI solutions engineer must learn to detect and correct.

In this step, get ready to:
  • Build main.py in Cursor.
  • Store the Gemini API key in a local .env file.
  • Run the endpoint and compare the code to your diagram.
Build main.py in Cursor

Your diagram shows three containers, but building all three files at once means waiting longer for your first working endpoint. A single file gets you there faster.

Why work in Cursor?

You should now see ai-architecture-nextwork in the Explorer sidebar. This view keeps the project structure visible while you build.

Your code opens in the editor while the integrated terminal stays in the panel below. You can move between changing, running, and checking the endpoint without leaving the project workspace.

  • Click the New File... button in the Explorer sidebar.
  • Enter main.py as the file name.
  • Press Enter to create and open the file.
  • Add the imports and app initialization by pasting this code:
# Monolithic version before the refactor
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from google import genai

# Load the local key before creating the Gemini client
load_dotenv()

# Set up the app, Gemini client, and constants
app = FastAPI(title="AI Architecture Demo")
client = genai.Client()
MODEL_ID = "gemini-3.8-flash"
SYSTEM_CONTEXT = "You are a helpful assistant. Respond concisely."

What does this code do?

  • FastAPI handles incoming HTTP requests and sends responses.
  • load_dotenv() reads .env and adds its values to the app's environment.
  • genai.Client() reads GEMINI_API_KEY from that environment without putting the secret in your Python code.
  • MODEL_ID and SYSTEM_CONTEXT control which model to call and how it responds.
  • Below the constants, add the data models and health endpoint:
# Define the request and response shapes
class QuestionRequest(BaseModel):
    question: str

class AnswerResponse(BaseModel):
    answer: str

# Health check endpoint
@app.get("/health")
async def health():
    return {"status": "healthy"}

What does this code do?

  • QuestionRequest and AnswerResponse are Pydantic models that define the shape of incoming requests and outgoing responses. FastAPI uses them to validate data automatically.
  • The /health endpoint is a simple GET route that returns a JSON status. It lets you confirm the server is running before testing anything complex.
  • Save main.py by pressing Cmd+S
  • Click Terminal in the top menu.
  • Click New Terminal.
  • Choose the tab for your operating system to activate the virtual environment.

macOS

  • Activate the project’s virtual environment by running this command:
source venv/bin/activate

You should see (venv) at the start of the terminal prompt.

Windows

  • Activate the project’s virtual environment by running this command:
.\venv\Scripts\Activate.ps1

You should see (venv) at the start of the terminal prompt.

Linux

  • Activate the project’s virtual environment by running this command:
source venv/bin/activate
Store the Gemini key locally

A terminal export disappears when that terminal closes. A local .env file keeps the key available between sessions, while python-dotenv loads it when your app starts.

  • Click the New File... button in the Explorer sidebar.
  • Enter .env and press Enter.

You should now see .env beside .gitignore and main.py in the Explorer sidebar.

Keep the key local

Your .env file contains a live credential in plain text. Your existing .gitignore excludes it, but never commit it or include it in a screenshot.

Use a managed secrets service instead of .env when you deploy.

  • Add this line to .env. Replace your-api-key-here with your Gemini API key:
GEMINI_API_KEY=your-api-key-here
  • Save .env.
  • Verify Python can load the key without displaying it by running in your Integrated Terminal:
python -c "from dotenv import load_dotenv; import os; load_dotenv(); print('API key loaded' if os.getenv('GEMINI_API_KEY') else 'API key missing')"

You should see API key loaded. Your app can now authenticate from any new terminal opened in this folder.

Seeing API key missing?

  • Confirm .env sits beside main.py in the ai-architecture-nextwork folder.
  • Confirm the file is named exactly .env, with no .txt extension.
  • Check that the line starts with GEMINI_API_KEY= without sharing the value.

Help me fix my local dotenv setup.

  • Start the development server by running this command in Integrated Terminal:
fastapi dev main.py

What does this command do?

fastapi dev starts a local development server with auto-reload enabled. It serves your app at http://127.0.0.1:8000. Every time you save main.py, the server restarts automatically with your changes.

The development server keeps this integrated terminal busy while it runs. A second integrated terminal lets you keep the server logs visible beside your test commands.

💡 Why does the root URL return 404?

The app does not define a route for /, so opening http://127.0.0.1:8000/ returns 404 Not Found. Use /health to check the server and /docs to test the API.

  • Click New Terminal or Split Terminal to create a second integrated terminal.

Before you test, what do you expect to see when you hit the health endpoint?

  • Test the health endpoint by running:
curl http://127.0.0.1:8000/health

Not seeing a response?

  • Check the first integrated terminal for a log showing that the server started at http://127.0.0.1:8000.
  • Confirm that (venv) appears at the start of the prompt in the first integrated terminal.
  • If curl reports Connection refused, inspect the first integrated terminal for a crash message.

help me debug why my FastAPI health endpoint is not responding

Add the /ask endpoint and spot the drift

The health check proves the server is alive. Now add the endpoint that actually calls Gemini to answer questions.

  • In main.py, add the following code below the health function:
# Ask endpoint: builds a prompt, calls Gemini, returns the answer
@app.post("/ask", response_model=AnswerResponse)
async def ask(request: QuestionRequest):
    prompt = f"{SYSTEM_CONTEXT}\n\nUser question: {request.question}"
    response = await client.aio.models.generate_content(
        model=MODEL_ID,
        contents=prompt,
    )
    return AnswerResponse(answer=response.text)

✔️ Awesome, I've got everything!

Great. Make sure you have saved main.py and that both the health and ask endpoints are returning responses.

ⓧ I want to see the full code

Here is the full main.py as it should look right now:

# Monolithic version before the refactor
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from google import genai

# Load the local key before creating the Gemini client
load_dotenv()

# Set up the app, Gemini client, and constants
app = FastAPI(title="AI Architecture Demo")
client = genai.Client()
MODEL_ID = "gemini-3.8-flash"
SYSTEM_CONTEXT = "You are a helpful assistant. Respond concisely."

# Define the request and response shapes
class QuestionRequest(BaseModel):
    question: str

class AnswerResponse(BaseModel):
    answer: str

# Return a simple status without calling Gemini
@app.get("/health")
async def health():
    return {"status": "healthy"}

# Build a prompt, call Gemini, and return its answer
@app.post("/ask", response_model=AnswerResponse)
async def ask(request: QuestionRequest):
    prompt = f"{SYSTEM_CONTEXT}\n\nUser question: {request.question}"
    response = await client.aio.models.generate_content(
        model=MODEL_ID,
        contents=prompt,
    )
    return AnswerResponse(answer=response.text)

Updated monolithic /ask endpoint in main.py.

  • Save main.py. The dev server auto-reloads with your changes.

What does this code do?

  • The /ask route accepts a JSON body with a question field.
  • The function combines the system context and the user's question into one prompt.
  • client.aio.models.generate_content() sends that prompt to gemini-3.8-flash without blocking the FastAPI event loop.

Before you run the request, do you think the endpoint will return an answer or a server error?

  • Switch to your second integrated terminal.
  • Choose your operating system to send the first request to /ask.

macOS

  • Send a JSON question to /ask by running this command:
curl -X POST http://127.0.0.1:8000/ask -H "Content-Type: application/json" -d '{"question":"What is the C4 model?"}'

Windows

  • Send a JSON question to /ask by running this command:
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8000/ask" -ContentType "application/json" -Body '{"question":"What is the C4 model?"}'

Linux

  • Send a JSON question to /ask by running this command:
curl -X POST http://127.0.0.1:8000/ask -H "Content-Type: application/json" -d '{"question":"What is the C4 model?"}'

You should receive a JSON response with an answer field explaining the C4 model. That first answer proves your local endpoint can reach Gemini and return its output.

Seeing a 500 error?

  • Check the first integrated terminal for the error from the failed request.
  • If it says No API key was provided, confirm .env sits beside main.py.
  • Confirm the line in .env starts with GEMINI_API_KEY= without copying the secret into logs or messages.

Help me debug the 500 error safely.

The endpoint works. You can send a question and get an AI-generated answer back. Now take a step back.

How many Python files does your code live in? How many containers does your C4 diagram show?

  • Count the Python files from your second integrated terminal by running this command:
ls *.py

You see one file: main.py. Now look at your diagram on GitHub. It shows three containers:

  1. API Container,
  2. Prompt Orchestrator,
  3. Model Provider.

What just happened?

You have already drifted. The diagram says there are boundaries between three components. The code says everything lives in one place.

This is exactly how architecture drift happens in real projects. You took the fastest path to a working endpoint, and within minutes the code no longer matches the design.

Your endpoint works, but the code and the diagram tell two different stories. In the next step, you will fix that drift by splitting the code to match the diagram exactly.

Make the Code Match the Diagram

Your /ask endpoint now sends a question to Gemini and returns an answer. Your C4 diagram still shows three containers with clear boundaries.

A passing endpoint is not enough when the implementation contradicts the system design. In this step, you will turn the C4 aliases into an executable architecture contract, watch the monolith fail it, and refactor until the same check passes.

In this step, get ready to:
  • Build a checker that turns Mermaid aliases into expected Python modules.
  • Refactor the monolith into the Model Provider, Prompt Orchestrator, and API boundaries.
  • Run the same checker again and prove that the architecture now matches.
Detect drift and extract the Model Provider

Your Mermaid aliases already name the Python modules the design expects. A small checker can compare that contract with the files on disk before a reviewer discovers the mismatch.

  • Click the New File... button in the Explorer sidebar.
  • Enter check_architecture.py and press Enter.
  • Add the architecture checker by pasting this code:
from pathlib import Path
import re
import sys

# Read the container aliases from the architecture contract.
diagram = Path("README.md").read_text(encoding="utf-8")
aliases = re.findall(r"^\s*Container\(([^,]+),", diagram, re.MULTILINE)

# Map each internal alias to the Python module it names.
missing = [f"{alias}.py" for alias in aliases if not Path(f"{alias}.py").exists()]

if missing:
    print("Architecture drift detected.")
    print(f"Missing modules: {', '.join(missing)}")
    sys.exit(1)

print("Architecture matches the diagram.")

How does the checker enforce the diagram?

  • re.findall() reads every internal Container alias from the Mermaid diagram.
  • The filename check turns aliases such as orchestrator into expected files such as orchestrator.py.
  • sys.exit(1) gives local scripts and CI the same machine-readable failure signal.
  • Save check_architecture.py.

The Model Provider container in your diagram has one job: sending prompts to the LLM and returning generated text. Right now, that logic is buried inside main.py. It needs its own file.

  • Click the New File... button in the Explorer sidebar.
  • Enter model_provider.py and press Enter.
  • Add the Model Provider code to model_provider.py by pasting the following block:
from dotenv import load_dotenv
from fastapi import HTTPException
from google import genai
from google.genai import errors

# Load the local key before creating the Gemini client.
load_dotenv()
MODEL_ID = "gemini-3.8-flash"
client = genai.Client()

# Call Gemini without blocking the FastAPI event loop.
async def generate(prompt: str) -> str:
    try:
        response = await client.aio.models.generate_content(
            model=MODEL_ID,
            contents=prompt,
        )
    except errors.APIError as error:
        raise HTTPException(
            status_code=502,
            detail="Gemini request failed.",
        ) from error

    return response.text
  • Save model_provider.py by pressing Cmd+S

What does this code do?

  • load_dotenv() runs before genai.Client() so the client can read the local API key.
  • client.aio.models.generate_content() performs the Gemini request asynchronously.
  • errors.APIError catches failures reported by the Gemini SDK without hiding unrelated programming errors.
  • HTTPException turns a provider failure into a controlled 502 API response.
Create the Prompt Orchestrator module

The Prompt Orchestrator container in your diagram builds prompts from user input and system context. It sits between the API and the Model Provider, formatting raw questions into structured prompts before they reach Gemini.

  • Click the New File... button in the Explorer sidebar.
  • Enter orchestrator.py and press Enter.
  • Add the Prompt Orchestrator code to orchestrator.py by pasting the following block:
from model_provider import generate

# Add the same system context to every user question.
SYSTEM_CONTEXT = "You are a helpful assistant. Respond concisely."

# Build the prompt before delegating the async model call.
async def answer_question(user_question: str) -> str:
    prompt = f"{SYSTEM_CONTEXT}\n\nUser question: {user_question}"
    return await generate(prompt)

What does this code do?

  • from model_provider import generate mirrors the arrow from the orchestrator to the provider in your C4 diagram.
  • SYSTEM_CONTEXT keeps prompt behavior inside the orchestration boundary.
  • answer_question() builds the prompt and awaits the provider without exposing Gemini details to the API layer.
  • Save orchestrator.py.

Seeing an ImportError?

  • If the error mentions model_provider, check that model_provider.py is saved in the same folder as orchestrator.py.
  • If the error mentions generate, make sure the function name in model_provider.py is spelled exactly generate (lowercase, no typos).

Why can't orchestrator.py import from model_provider?

Refactor main.py to match the API Container

Your diagram labels the API Container's job as receiving HTTP requests and returning responses. Right now, main.py also initializes the Gemini client, stores the system prompt, and builds prompts. All of that belongs in the other two modules now.

  • Open main.py and replace its entire contents with the following code:
from fastapi import FastAPI
from pydantic import BaseModel
import orchestrator

# Create the HTTP API.
app = FastAPI(title="AI Architecture Demo")

# Define the request and response shapes.
class QuestionRequest(BaseModel):
    question: str

class AnswerResponse(BaseModel):
    answer: str

# Return server health without calling Gemini.
@app.get("/health")
async def health():
    return {"status": "healthy"}

# Delegate each question across the orchestrator boundary.
@app.post("/ask", response_model=AnswerResponse)
async def ask(request: QuestionRequest):
    answer = await orchestrator.answer_question(request.question)
    return AnswerResponse(answer=answer)

Refactored main.py importing only orchestrator from the project's modules.

What changed?

  • main.py imports orchestrator as its only project module.
  • await orchestrator.answer_question() keeps the asynchronous request chain intact.
  • Gemini configuration and SDK imports stay inside model_provider.py.

The API boundary now receives HTTP requests and returns responses. Every deeper responsibility stays behind the orchestrator.

  • Save main.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three files: model_provider.py, orchestrator.py, and main.py.

ⓧ I'd like to double check the full code

Here is each file as it should look now.

from dotenv import load_dotenv
from fastapi import HTTPException
from google import genai
from google.genai import errors

# Load the local key before creating the Gemini client.
load_dotenv()
MODEL_ID = "gemini-3.8-flash"
client = genai.Client()

# Call Gemini without blocking the FastAPI event loop.
async def generate(prompt: str) -> str:
    try:
        response = await client.aio.models.generate_content(
            model=MODEL_ID,
            contents=prompt,
        )
    except errors.APIError as error:
        raise HTTPException(
            status_code=502,
            detail="Gemini request failed.",
        ) from error

    return response.text
from model_provider import generate

# Add the same system context to every user question.
SYSTEM_CONTEXT = "You are a helpful assistant. Respond concisely."

# Build the prompt before delegating the async model call.
async def answer_question(user_question: str) -> str:
    prompt = f"{SYSTEM_CONTEXT}\n\nUser question: {user_question}"
    return await generate(prompt)
from fastapi import FastAPI
from pydantic import BaseModel
import orchestrator

# Create the HTTP API.
app = FastAPI(title="AI Architecture Demo")

# Define the request and response shapes.
class QuestionRequest(BaseModel):
    question: str

class AnswerResponse(BaseModel):
    answer: str

# Return server health without calling Gemini.
@app.get("/health")
async def health():
    return {"status": "healthy"}

# Delegate each question across the orchestrator boundary.
@app.post("/ask", response_model=AnswerResponse)
async def ask(request: QuestionRequest):
    answer = await orchestrator.answer_question(request.question)
    return AnswerResponse(answer=answer)
  • Inspect the Python files after the refactor by running:
ls *.py

Before you rerun the architecture contract, do you expect any diagram aliases to be missing now?

  • Prove that the refactor satisfies the diagram by running:
python check_architecture.py

You should see Architecture matches the diagram. The same contract that exposed the monolith now approves the three-boundary implementation.

Before you start the server, think about this: you split one file into three and rewired the imports. Do you expect the /ask endpoint to return the same results as before?

  • Switch to the terminal running the server from the previous step and press Ctrl+C to stop it.
  • Start the refactored server by running:
fastapi dev main.py

What should I see?

The server starts at http://127.0.0.1:8000 with auto-reload enabled. It takes over the terminal, so you will need a new terminal tab to run curl commands.

  • Switch back to second terminal (make sure your venv is activated).
  • Test the health endpoint by running:
curl http://127.0.0.1:8000/health
  • Choose your operating system to test the /ask endpoint.

macOS

curl -X POST http://127.0.0.1:8000/ask -H "Content-Type: application/json" -d '{"question":"What is the C4 model?"}'

Windows

Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:8000/ask" -ContentType "application/json" -Body '{"question":"What is the C4 model?"}'

Linux

curl -X POST http://127.0.0.1:8000/ask -H "Content-Type: application/json" -d '{"question":"What is the C4 model?"}'
  • Stop the server by pressing Ctrl+C in the terminal where it is running.

Server failing after the refactor?

  • If the port is busy, stop the earlier server with Ctrl+C before restarting it.
  • If the import error names answer_question, confirm that main.py imports orchestrator and awaits orchestrator.answer_question().
  • If the API returns 502, inspect the server log for the Gemini SDK error without exposing your key.
  • Help me debug the refactored request chain.

Your local checker now proves that the code and diagram agree. Next, you will move that contract into GitHub Actions so every pushed change has to tell the same architectural story.

Prove the Match Automatically

Your local architecture check now fails on drift and passes after the refactor. The next goal is making that proof run for every change pushed to GitHub Actions.

A shared CI check turns your diagram from personal documentation into a team-wide guardrail. Anyone can see whether the code still satisfies the architecture before reviewing the implementation.

In this step, get ready to:
  • Create a GitHub Actions workflow for the architecture checker.
  • Push the workflow and inspect a green run.
  • Add the workflow status badge to the README.
Automate the architecture check

GitHub starts a workflow from a file inside .github/workflows/. This path holds the same architecture check you already proved locally.

  • Create the workflow folders and file by running this command:
mkdir -p .github/workflows && touch .github/workflows/architecture.yml

What does this command do?

  • The mkdir -p command creates .github/workflows/, including any missing parent folder.
  • The touch command creates architecture.yml inside the workflow folder.
  • In the Explorer sidebar, expand .github.
  • Expand workflows.
  • Open architecture.yml.
  • Paste the workflow configuration below into architecture.yml.
name: Architecture Check

# Run the contract for pushed commits and pull requests.
on:
  push:
  pull_request:

jobs:
  architecture:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repository
        uses: actions/checkout@v7
      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.13"
      - name: Check architecture
        run: python check_architecture.py
  • Save .github/workflows/architecture.yml.

What does this workflow do?

  • push and pull_request trigger the contract when shared code changes.
  • actions/checkout@v7 gives the runner access to your repository files.
  • actions/setup-python@v7 selects Python 3.13 explicitly so the runner does not depend on a changing default.
  • python check_architecture.py reuses the local contract without duplicating its logic.
Inspect the automated proof

The workflow only exists on your computer until you push it. Once GitHub receives the file, it starts a hosted runner and executes the architecture contract.

Before you push, do you expect the remote checker to reach the same passing result as your local run?

  • Commit and push the workflow by running:
git add .github/workflows/architecture.yml check_architecture.py main.py orchestrator.py model_provider.py
git commit -m "Add automated architecture contract"
git push

What do these commands do?

  • git add stages the workflow, checker, and three refactored Python modules.
  • git commit records the implementation and its automated contract together.
  • git push sends the complete architecture to GitHub and triggers the workflow.
  • Open your repository in your browser.
  • Click the Actions tab in the repository navigation.
  • Select Architecture Check in the workflow list.
  • Open the newest workflow run.

The first run may take a minute while GitHub prepares the runner. You should then see a green check beside the architecture job and a successful Check architecture step.

Workflow run failed?

  • Open the failed step and read the first error line before changing the workflow.
  • Confirm that check_architecture.py, main.py, orchestrator.py, and model_provider.py were pushed in the same branch.
  • Help me diagnose the failed workflow.
Show the passing contract

A status badge puts the latest workflow result beside the project title. Visitors can see the architecture contract before opening the Actions tab.

  • Record your GitHub username: your GitHub username.
  • Open README.md in the Explorer sidebar.
  • Add the workflow badge by pasting this line:
[![Architecture Check](https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/ai-architecture-nextwork/actions/workflows/architecture.yml/badge.svg)](https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/ai-architecture-nextwork/actions/workflows/architecture.yml)

What does the badge actually prove?

A passing badge shows that the latest checked commit satisfies the rules in check_architecture.py. It gives reviewers visible evidence that the code still matches the boundaries you chose.

The badge cannot prove that every architecture decision is correct. Its value depends on the rules you encode, so incomplete checks create incomplete protection.

The lesson is to turn important design decisions into executable contracts and run them in CI for every change. Architecture stops relying on memory once drift can fail the build.

  • Save README.md.

Before you push the badge, do you expect it to show the same green result as the workflow page?

  • Publish the badge update by running:
git add README.md
git commit -m "Add architecture status badge"
git push
  • Refresh the main page of your GitHub repository.
  • Confirm that the Architecture Check badge appears below the project title with a passing status.

That green badge is the visible payoff: your architecture is designed, implemented, and enforced on every push.

Badge missing or unknown?

  • Confirm that your GitHub username matches the repository owner exactly.
  • Confirm that the workflow filename in the badge URL is architecture.yml.
  • Help me fix the workflow badge.

Secret mission

Ask Your Architecture

Add an Architecture Context boundary, watch CI reject the missing module, then build a grounded endpoint that answers from your own C4 diagram.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses the Gemini API free tier, so there are no ongoing costs. Decide whether to keep your resources, pause them to come back later, or delete them entirely.

Resources you used:

  • The ai-architecture-nextwork folder on your Desktop, including the Python modules, architecture checker, workflow file, local .env, and venv/.
  • GitHub repository with your C4 diagram, Architecture Check workflow, and status badge.
  • Gemini API key created in Google AI Studio.

Keep everything running

No action needed. Choose this if you plan to keep building on this project or revisiting the architecture.

  • Your project folder, GitHub repository, workflow history, and API key remain available for future use.
  • The Gemini API free tier has no charges, so your API key costs nothing to keep active.
  • Your .env file stays inside the local project folder and remains excluded from Git by .gitignore.

Pause - I'll come back to this later

Shut down running processes to free up system resources while keeping your files intact.

  • Stop the FastAPI dev server if it is still running by pressing Ctrl+C in the terminal where it is running.
  • Your project folder, GitHub repository, and API key stay intact for when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Stop the FastAPI dev server if it is still running by pressing Ctrl+C in the terminal.
  • Delete your project folder from the Desktop. On macOS or Linux, run:
rm -rf ~/Desktop/ai-architecture-nextwork
  • On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\ai-architecture-nextwork
  • Delete your GitHub repository.
  • Open your repository page on GitHub.
  • Click Settings in the repository navigation bar.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository and follow the confirmation prompts to complete the deletion.
  • Delete your Gemini API key.
  • Visit aistudio.google.com/app/apikey to open your API keys page.
  • Find the key you created for this project and delete it.

What about the local .env file?

Deleting the ai-architecture-nextwork folder also removes its local .env file. Deleting the key in Google AI Studio revokes the credential itself.

Nice Work!

Nice Work!

Nice work! You built a C4-designed AI service, aligned its Python boundaries with the diagram, and made GitHub Actions enforce that contract on every push.

You've learned how to:

  • Design a C4 Level 2 Container diagram in Mermaid before writing the service, then render it in your GitHub README.
  • Build and refactor an async FastAPI service into clear Model Provider, Prompt Orchestrator, and API boundaries.
  • Turn Mermaid aliases into an executable architecture contract that GitHub Actions checks on every push and displays with a status badge.
  • Secret Mission: Add an Architecture Context boundary and a grounded /ask-architecture endpoint, using a CI failure and recovery to prove the design-first workflow.

Ready to quiz yourself?