Build a Python Backend Portfolio

Build and deploy a responsive Python portfolio powered by a typed FastAPI API.

Introduction

30 Second Summary

A polished portfolio can still hide the engineering work behind it. Recruiters need proof they can inspect quickly.

In this project, you will build a responsive developer portfolio powered by FastAPI. Your project cards come from a typed Python API that recruiters can inspect through generated documentation.

What You'll Build

When you finish, one live Vercel link presents your responsive portfolio alongside an inspectable Python backend.

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

  • A responsive portfolio that keeps your backend-first message clear on wide screens and narrow screens.
  • A typed REST API that proves its behavior through JSON-backed project cards plus a Pydantic validation receipt.
  • A public GitHub repository paired with a live Vercel deployment where recruiters can test interactive OpenAPI documentation.
  • Secret Mission: Add a technology selector that filters projects through a backend query parameter.

Are there any prerequisites?

This project assumes basic familiarity with Python, HTML, CSS, and JavaScript.

You also start with a Linux environment where Visual Studio Code, Git, GitHub, Vercel, and Firefox are ready to use.

Before We Start

Before any hands-on work begins, lock in what you want recruiters to remember about you. Your positioning statement will guide every portfolio choice that follows.

Set Up the FastAPI Workspace

Your positioning statement gives this portfolio a clear target. A clean FastAPI workspace makes that target repeatable on your computer and Vercel.

A project-local virtual environment isolates the pinned dependencies from other Python projects. You will create the starter files in Visual Studio Code before running the first page locally.

In this step, get ready to:
  • Create the project structure in Visual Studio Code.
  • Add the pinned dependencies and starter files.
  • Run the starter portfolio through the local FastAPI server.
Create the project structure

The project keeps the Python application beside a dedicated public/ directory. This layout gives the backend one clear place to serve the starter page from.

  • Navigate to your Desktop in the Linux file manager.
  • Use the new-folder control to create backend-first-portfolio.

You should see backend-first-portfolio/ on your Desktop.

  • Start Visual Studio Code from your Linux application launcher.
  • Use the folder-opening control in Visual Studio Code to select backend-first-portfolio/.

You should see backend-first-portfolio at the top of the file sidebar.

  • Use the new-folder control in the file sidebar to create public/ inside backend-first-portfolio/.
  • Confirm that public/ appears beneath the project name.
  • Use the new-file control in the file sidebar to create requirements.txt inside backend-first-portfolio/.
  • Use the new-file control again to create .gitignore inside backend-first-portfolio/.

You should now see both configuration files beside public/ in the file sidebar.

  • Use the new-file control to create app.py inside backend-first-portfolio/.
  • Select public/ in the file sidebar before creating index.html inside it.

Your file tree should show requirements.txt, .gitignore, app.py, and public/index.html.

Add pinned files and the starter page

A version pin tells the installer to use one exact package release. These pins keep local development aligned with the deployed project.

  • Select requirements.txt in the file sidebar.
  • Replace its contents with the pinned dependencies below:
fastapi==0.143.0
pydantic==2.14.0
uvicorn==0.54.0

What do these pins control?

  • FastAPI 0.143.0 provides the web application and frontend-serving feature.
  • Pydantic 2.14.0 provides typed data models for the API you build later.
  • Uvicorn 0.54.0 runs the FastAPI application during local development.
  • Save requirements.txt.
  • Confirm that the editor shows exactly three dependency lines.

Do the dependency lines look different?

  • Check that each package name uses lowercase letters.
  • Check that each version uses two equals signs.

Still stuck? Help me compare my requirements.txt file with the three required package pins.

A .gitignore file identifies generated Python files that do not belong in the repository. The virtual environment can always be rebuilt from requirements.txt.

  • Select .gitignore in the file sidebar.
  • Add the generated-file patterns below:
.venv/
__pycache__/
*.py[cod]

What does this file exclude?

  • .venv/ excludes the project-local environment.
  • __pycache__/ excludes Python's cache directories.
  • *.py[cod] excludes generated Python bytecode files.
  • Save .gitignore.
  • Confirm that all three exclusion patterns remain on separate lines.

The starter application needs a FastAPI instance that Uvicorn can discover. Its frontend registration serves files from public/ with lower priority than future API routes.

  • Select app.py in the file sidebar.
  • Add the starter FastAPI application below:
from fastapi import FastAPI

app = FastAPI()


app.frontend("/", directory="public")

What does this application do?

  • FastAPI imports the application class from the installed framework.
  • app holds the application instance that Uvicorn runs.
  • app.frontend("/", directory="public") serves the frontend from public/.
  • Normal API routes retain priority because FastAPI checks them before the frontend files.
  • Save app.py.
  • Confirm that the import appears above app = FastAPI().

Does app.py show a syntax warning?

  • Check that both parentheses in FastAPI() are present.
  • Check that public remains inside matching quotation marks.

Need another pair of eyes? Help me find the syntax difference in my starter app.py file.

The starter HTML gives the root address a visible response. Its heading establishes the backend-first message that the next step expands.

  • Select public/index.html in the file sidebar.
  • Add the starter page below:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Your Name | Python Backend Developer</title>
  </head>
  <body>
    <main>
      <h1 id="hero-title">I design reliable APIs and ship the interface around them.</h1>
    </main>
  </body>
</html>

What does this page provide?

  • The metadata gives the page a character encoding and a responsive viewport.
  • The title identifies the portfolio in the browser tab.
  • The h1 gives the starter page a visible backend-focused headline.
  • Save public/index.html.
  • Confirm that the HTML begins with <!doctype html>.

Does the HTML structure look incomplete?

  • Check that every opening HTML element has its matching closing element.
  • Check that index.html is inside public/.

Need help comparing the structure? Help me find the missing or misplaced HTML element in public/index.html.

✔️ My starter files match

Great. Your starter files now define a repeatable Python environment and a page that FastAPI can serve.

ⓧ I'd like to double check the full code

These are the exact starter files for this step. Compare each one with the matching file in your project.

fastapi==0.143.0
pydantic==2.14.0
uvicorn==0.54.0

Why keep these exact pins?

These three lines make the dependency installation repeatable across the local project and deployment.

.venv/
__pycache__/
*.py[cod]

Why keep these exclusions?

These patterns keep the local environment and generated Python cache files outside version control.

from fastapi import FastAPI

app = FastAPI()


app.frontend("/", directory="public")

Why keep the frontend call last?

The final call registers the frontend as a low-priority fallback so future API declarations can retain priority.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Your Name | Python Backend Developer</title>
  </head>
  <body>
    <main>
      <h1 id="hero-title">I design reliable APIs and ship the interface around them.</h1>
    </main>
  </body>
</html>

Why keep this starter small?

This valid HTML page provides an immediate browser result while leaving the full interface for the next step.

Create the environment and start the server

The saved files describe the project. The activated environment supplies the exact packages needed to run it with Python 3.10 or newer.

  • Use the terminal control in Visual Studio Code to create an integrated terminal inside backend-first-portfolio/.
  • Create the project-local environment by running this command:
python -m venv .venv

What does this command create?

Python creates an isolated environment inside .venv/. The environment keeps this project's packages separate from system-level packages.

  • Confirm that .venv/ appears in the Visual Studio Code file sidebar.

Don't see the virtual environment?

  • Confirm that the terminal path ends with backend-first-portfolio.
  • Check that your Python runtime is 3.10 or newer.

Still blocked? Help me troubleshoot why Python did not create .venv in my backend-first-portfolio folder.

  • Activate the new environment in the same terminal by running this command:
source .venv/bin/activate

What does activation change?

Activation points the current terminal session at the Python tools inside .venv/. Package installations now stay inside this project.

  • Confirm that the terminal prompt includes .venv.

Does the prompt still look unchanged?

  • Check that you ran the activation command from backend-first-portfolio/.
  • Check that .venv/bin/activate exists in the file sidebar.

Need help with activation? Help me activate my Linux Python virtual environment in Visual Studio Code.

  • Install the pinned project dependencies by running this command:
python -m pip install -r requirements.txt

What does this installation use?

The installer reads the three exact package pins from requirements.txt. Each package is installed into the active .venv/ environment.

You should see the installation complete without dependency errors. The terminal prompt returns when all three pinned packages are ready.

Did the dependency installation fail?

  • Confirm that the terminal prompt still includes .venv.
  • Confirm that requirements.txt contains the three exact version pins.
  • Use Python 3.10 or newer if the installer reports a version incompatibility.

Still seeing an installation problem? Help me diagnose my pinned FastAPI dependency installation.

Good progress. The pinned FastAPI, Pydantic, and Uvicorn packages now live inside the project environment.

The development server keeps the current terminal busy while it serves the application. Your saved app.py gives it the application target to run.

Before you run the server, what do you expect the root address to show with public/index.html in place?

  • Start the local development server by running this command:
uvicorn app:app --reload

What does this command do?

  • app:app tells Uvicorn to load the app object from app.py.
  • --reload restarts the development server when saved application files change.
  • Leave the integrated terminal running with the server process.
  • Use your Linux application launcher to start Firefox.
  • Enter http://127.0.0.1:8000 in the browser address bar.

You will see the heading I design reliable APIs and ship the interface around them. on the starter portfolio page. Your FastAPI application is now serving the frontend from public/.

Don't see the starter page?

  • Confirm that the server process is still running in the integrated terminal.
  • Confirm that index.html sits inside public/.
  • Confirm that app.frontend("/", directory="public") appears in app.py.

Still unable to load the page? Help me troubleshoot my local FastAPI starter page.

Your repeatable backend workspace is live. Next, you will turn this starter heading into a responsive full-stack portfolio interface.

Build the Portfolio Interface

Your FastAPI workspace now serves a starter page. A recruiter needs a clear visual result before they inspect the backend evidence behind it.

You will turn that starter page into a responsive portfolio interface. Its HTML will present your backend focus through visible projects, API links, status feedback, and a transparent contact demo.

In this step, get ready to:
  • Build the semantic portfolio structure.
  • Create a responsive dark visual system.
  • Personalize the portfolio and test its narrow layout.
Build the semantic page structure

Semantic elements give each part of the portfolio a clear purpose. The page starts with a complete shell so every later addition can be tested in the browser.

  • Switch back to the open public/index.html tab in Visual Studio Code.
  • Select the existing starter markup with Ctrl+A.
  • Replace the selected markup with this page shell:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta
      name="description"
      content="Backend-focused Python developer portfolio with a live FastAPI backend."
    >
    <title>Your Name | Python Backend Developer</title>
    <link rel="stylesheet" href="/styles.css">
    <script src="/app.js" defer></script>
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="/">Your Name</a>
      <nav aria-label="Primary navigation">
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
        <a href="/docs">API Docs</a>
      </nav>
    </header>

    <main>
    </main>

    <footer>
      <p>Built with Python, FastAPI, HTML, CSS, and JavaScript.</p>
      <a href="/docs">Test the backend</a>
    </footer>
  </body>
</html>

What Does This Markup Do?

  • The metadata gives browsers a page description and instructions for responsive sizing.
  • The header creates direct routes to the projects, contact form, and interactive API documentation.
  • The footer names the technologies behind the finished portfolio.
  • Save public/index.html.
  • Return to http://127.0.0.1:8000 in Firefox.
  • Refresh the page.

You should see your name in the header, three navigation links, and the technology summary in the footer.

Still Seeing the Starter Page?

  • Confirm that you saved the file inside the existing public/ directory.
  • Confirm that Uvicorn is still running in the terminal from the previous step.

Still stuck? Help me troubleshoot why FastAPI is still serving my old public/index.html page.

The hero leads with the role you want recruiters to remember. Its status card also creates a visible home for backend health feedback.

  • In public/index.html, place your cursor on the blank line inside <main>.
  • Add the hero section with this markup:
      <section class="hero" aria-labelledby="hero-title">
        <div>
          <p class="eyebrow">Python backend developer</p>
          <h1 id="hero-title">I design reliable APIs and ship the interface around them.</h1>
          <p class="hero-copy">
            My focus is backend engineering with Python, typed APIs, validation, and
            maintainable systems. I also build responsive frontend experiences that
            make those systems useful.
          </p>
          <div class="hero-actions">
            <a class="button primary" href="#projects">View projects</a>
            <a class="button secondary" href="/docs">Explore the live API</a>
          </div>
        </div>
        <aside class="status-card" aria-label="API status">
          <span class="status-dot" aria-hidden="true"></span>
          <p id="api-status">Checking API...</p>
          <small>FastAPI health endpoint</small>
        </aside>
      </section>

How Does the Hero Present Your Work?

  • The headline positions backend engineering as your primary skill.
  • The two calls to action connect the visual portfolio to its projects and API documentation.
  • The #api-status element provides a precise target for live health feedback in a later step.
  • Add the skills section directly below the hero section:
      <section class="section" aria-labelledby="skills-title">
        <p class="eyebrow">Core capabilities</p>
        <h2 id="skills-title">Backend depth with full-stack delivery</h2>
        <div class="skill-list" aria-label="Technical skills">
          <span>Python</span>
          <span>FastAPI</span>
          <span>REST APIs</span>
          <span>Data validation</span>
          <span>SQL</span>
          <span>JavaScript</span>
          <span>Responsive CSS</span>
          <span>Git</span>
        </div>
      </section>

Why Use a Skills List?

The skills list gives recruiters a quick technical summary. Its labels connect backend depth to the browser skills demonstrated by the page.

  • Save public/index.html.
  • Refresh the portfolio in Firefox.

You should see the backend-focused hero, two action links, an API status card, and a technical skills section.

Hero or Skills Missing?

  • Check that both new sections sit between the opening and closing <main> tags.
  • Check that every opening <section> has a matching closing tag.

Need another pair of eyes? Help me find the HTML nesting problem in my hero and skills sections.

The selected-work area makes the backend connection visible. The contact section explains that its future write operation validates a payload without storing or emailing it.

  • Place your cursor directly below the closing tag for the skills section.
  • Add the selected-work section with this markup:
      <section class="section" id="projects" aria-labelledby="projects-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Selected work</p>
            <h2 id="projects-title">Projects loaded from my API</h2>
          </div>
          <a href="/api/projects">View JSON</a>
        </div>
        <div class="project-grid" id="project-grid" aria-live="polite">
          <p class="loading-message">Loading projects from the API...</p>
        </div>
      </section>

How Is the Project Area Prepared?

  • The #project-grid container defines where selected work belongs.
  • The live-region attribute allows future loading and error messages to be announced by assistive technology.
  • The JSON link gives recruiters a direct path to the backend project data.
  • Add the contact section directly below the selected-work section:
      <section class="section contact" id="contact" aria-labelledby="contact-title">
        <div>
          <p class="eyebrow">Backend validation demo</p>
          <h2 id="contact-title">Send a test payload</h2>
          <p>
            This form sends JSON to a typed FastAPI endpoint. It validates and
            acknowledges the payload, but it does not store or email messages.
          </p>
        </div>
        <form id="contact-form">
          <label>
            Name
            <input name="name" autocomplete="name" required>
          </label>
          <label>
            Email
            <input name="email" type="email" autocomplete="email" required>
          </label>
          <label>
            Message
            <textarea name="message" rows="5" required></textarea>
          </label>
          <button class="button primary" type="submit">Validate message</button>
          <p id="form-status" role="status"></p>
        </form>
      </section>

Why Is the Contact Demo Transparent?

  • The form collects the three fields expected by the future typed request model.
  • The description states that the demo validates input without claiming to send or store messages.
  • The #form-status element provides a visible destination for the future backend receipt.
  • Save public/index.html.
  • Refresh the portfolio in Firefox.

You should see the selected-work heading, its loading message, the contact explanation, and all three form fields.

Contact Form Missing or Misaligned?

  • Confirm that the projects and contact sections appear before the closing </main> tag.
  • Check that #contact-form contains the name, email, message, button, and status elements.

Still stuck? Help me compare my portfolio form markup with the required public/index.html structure.

Use these tabs to confirm the complete baseline markup before you personalize its visible text.

✔️ Awesome, I've got everything!

Great. Save public/index.html before moving to the visual system.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta
      name="description"
      content="Backend-focused Python developer portfolio with a live FastAPI backend."
    >
    <title>Your Name | Python Backend Developer</title>
    <link rel="stylesheet" href="/styles.css">
    <script src="/app.js" defer></script>
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="/">Your Name</a>
      <nav aria-label="Primary navigation">
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
        <a href="/docs">API Docs</a>
      </nav>
    </header>

    <main>
      <section class="hero" aria-labelledby="hero-title">
        <div>
          <p class="eyebrow">Python backend developer</p>
          <h1 id="hero-title">I design reliable APIs and ship the interface around them.</h1>
          <p class="hero-copy">
            My focus is backend engineering with Python, typed APIs, validation, and
            maintainable systems. I also build responsive frontend experiences that
            make those systems useful.
          </p>
          <div class="hero-actions">
            <a class="button primary" href="#projects">View projects</a>
            <a class="button secondary" href="/docs">Explore the live API</a>
          </div>
        </div>
        <aside class="status-card" aria-label="API status">
          <span class="status-dot" aria-hidden="true"></span>
          <p id="api-status">Checking API...</p>
          <small>FastAPI health endpoint</small>
        </aside>
      </section>

      <section class="section" aria-labelledby="skills-title">
        <p class="eyebrow">Core capabilities</p>
        <h2 id="skills-title">Backend depth with full-stack delivery</h2>
        <div class="skill-list" aria-label="Technical skills">
          <span>Python</span>
          <span>FastAPI</span>
          <span>REST APIs</span>
          <span>Data validation</span>
          <span>SQL</span>
          <span>JavaScript</span>
          <span>Responsive CSS</span>
          <span>Git</span>
        </div>
      </section>

      <section class="section" id="projects" aria-labelledby="projects-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Selected work</p>
            <h2 id="projects-title">Projects loaded from my API</h2>
          </div>
          <a href="/api/projects">View JSON</a>
        </div>
        <div class="project-grid" id="project-grid" aria-live="polite">
          <p class="loading-message">Loading projects from the API...</p>
        </div>
      </section>

      <section class="section contact" id="contact" aria-labelledby="contact-title">
        <div>
          <p class="eyebrow">Backend validation demo</p>
          <h2 id="contact-title">Send a test payload</h2>
          <p>
            This form sends JSON to a typed FastAPI endpoint. It validates and
            acknowledges the payload, but it does not store or email messages.
          </p>
        </div>
        <form id="contact-form">
          <label>
            Name
            <input name="name" autocomplete="name" required>
          </label>
          <label>
            Email
            <input name="email" type="email" autocomplete="email" required>
          </label>
          <label>
            Message
            <textarea name="message" rows="5" required></textarea>
          </label>
          <button class="button primary" type="submit">Validate message</button>
          <p id="form-status" role="status"></p>
        </form>
      </section>
    </main>

    <footer>
      <p>Built with Python, FastAPI, HTML, CSS, and JavaScript.</p>
      <a href="/docs">Test the backend</a>
    </footer>
  </body>
</html>
Style the responsive visual system

The semantic page now has the right content hierarchy. A local CSS file will turn it into a responsive dark interface without introducing another build system.

  • Select the public/ directory in the Visual Studio Code Explorer sidebar.
  • Click the New File button in the Explorer toolbar.
  • Enter styles.css as the file name.
  • Add the theme variables and global page rules:
:root {
  color-scheme: dark;
  --background: #08111f;
  --surface: #101c2f;
  --surface-light: #17263d;
  --text: #f4f7fb;
  --muted: #aab7ca;
  --accent: #63e6be;
  --accent-dark: #0d4d3f;
  --border: #2a3c56;
  --danger: #ff8787;
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background:
    radial-gradient(circle at top right, rgba(99, 230, 190, 0.12), transparent 32rem),
    var(--background);
  color: var(--text);
  line-height: 1.6;
}

What Does the Visual Foundation Control?

  • The custom properties keep the background, surfaces, text, accent, border, and error colors consistent.
  • The system-font stack keeps the project local and avoids an external font request.
  • The global box model makes element sizing predictable across the page.
  • Continue below the existing CSS with the link and page-width rules:
a {
  color: var(--accent);
  text-decoration: none;
}

a:hover,
a:focus-visible {
  text-decoration: underline;
}

.site-header,
main,
footer {
  width: min(1120px, calc(100% - 2rem));
  margin-inline: auto;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: 1.25rem;
}

How Does the Page Stay Readable?

The shared width rule centers the header, main content, and footer within a readable maximum width. The header layout keeps the brand and navigation on opposite sides.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see a dark radial background, mint links, and centered page content with space around the header.

Stylesheet Not Loading?

  • Confirm that the file is named public/styles.css.
  • Confirm that the page head contains href="/styles.css".

Need help? Help me troubleshoot why public/styles.css is not loading through my FastAPI frontend.

  • Add the brand, navigation, and hero layout rules below the existing CSS:
.brand {
  color: var(--text);
  font-size: 1.1rem;
  font-weight: 800;
}

nav {
  display: flex;
  gap: 1rem;
}

.hero {
  min-height: 70vh;
  display: grid;
  grid-template-columns: 1.5fr 0.7fr;
  gap: 3rem;
  align-items: center;
  padding-block: 5rem;
}

How Does the Hero Use Space?

The grid gives the main pitch more room than the status card. Its minimum height creates a clear opening view without fixing the section to one screen size.

  • Continue with the eyebrow and heading typography:
.eyebrow {
  margin: 0 0 0.5rem;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 850px;
  margin-bottom: 1.25rem;
  font-size: clamp(2.6rem, 7vw, 5.5rem);
  line-height: 1.02;
}

Why Use Fluid Heading Sizes?

The main heading scales with the browser width while staying inside readable minimum and maximum sizes. The eyebrow style creates a repeatable label for each major section.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see a two-column hero with a large headline, a smaller status-card column, and uppercase mint section labels.

Hero Still Appearing as Plain Text?

  • Check that the markup uses class="hero" and class="eyebrow" exactly.
  • Check the CSS above the hero rule for a missing closing brace.

Still stuck? Help me find why my .hero or .eyebrow CSS rules are not applying.

  • Add the supporting hero text and button foundations below the heading rules:
.hero-copy,
.section > p,
.contact > div > p {
  max-width: 720px;
  color: var(--muted);
  font-size: 1.08rem;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 0.7rem;
  padding: 0.75rem 1rem;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}

What Makes the Actions Consistent?

The shared button class gives links and buttons the same spacing, shape, and weight. The flexible action row can wrap when the browser becomes narrower.

  • Continue with the button variants and card surfaces:
.button.primary {
  background: var(--accent);
  color: #04120e;
}

.button.secondary {
  border-color: var(--border);
  background: var(--surface);
  color: var(--text);
}

.status-card,
.project-card,
.contact form {
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: rgba(16, 28, 47, 0.88);
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.18);
}

.status-card {
  padding: 1.5rem;
}

Why Share a Card Surface?

The status card, project cards, and contact form use one visual surface. That repetition makes separate features feel like parts of the same product.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see filled primary and secondary actions plus a bordered status card with a soft shadow.

Buttons or Card Surface Missing?

  • Confirm that each action has the shared button class plus its primary or secondary class.
  • Confirm that the API status element uses class="status-card".

Need help? Help me debug the button and status-card class selectors in my portfolio.

  • Add the status text and indicator styles below the card rules:
.status-card p {
  margin-bottom: 0.25rem;
  font-weight: 800;
}

.status-card small,
.project-card p,
.loading-message {
  color: var(--muted);
}

.status-dot {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  margin-right: 0.5rem;
  border-radius: 50%;
  background: var(--danger);
}

.status-card.online .status-dot {
  background: var(--accent);
  box-shadow: 0 0 1rem rgba(99, 230, 190, 0.7);
}

How Does the Status Indicator Work?

The default dot uses the danger color. A future health check can add the online class to switch the dot to a glowing accent.

  • Continue with section spacing and skill tags:
.section {
  padding-block: 4rem;
}

.skill-list,
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.skill-list span,
.tag-list span {
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.4rem 0.75rem;
}

Why Let Skill Tags Wrap?

The flexible lists keep each label intact while allowing the collection to flow onto new lines. This prevents the skills row from overflowing on smaller screens.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see a red status dot, muted supporting text, generous section spacing, and pill-shaped skill labels.

Skill Tags Not Forming Pills?

  • Confirm that the skills container uses class="skill-list".
  • Confirm that every skill is wrapped in a <span> element.

Still stuck? Help me troubleshoot why the .skill-list span styles are not applying.

  • Add the selected-work heading and three-column grid rules:
.section-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.project-card {
  padding: 1.4rem;
}

How Is Selected Work Organized?

The section heading keeps its title and backend link aligned. The project grid prepares three equal columns for selected work on wider screens.

  • Continue with project highlights and the contact layout:
.project-card h3 {
  font-size: 1.35rem;
}

.project-highlight {
  border-left: 3px solid var(--accent);
  padding-left: 0.8rem;
}

.contact {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: 2rem;
  align-items: start;
}

How Does the Contact Layout Balance?

The contact explanation uses a slightly smaller column than the form. The project highlight rule creates a reusable visual cue for backend decisions and measurable outcomes.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see the selected-work title aligned with its JSON link and the contact explanation beside the form.

Contact Section Still Stacked on a Wide Window?

  • Confirm that the contact section has both section and contact classes.
  • Confirm that your browser window is wider than 760px.

Need help? Help me determine why my .contact grid is not using two columns.

  • Add the form layout and label rules below the contact grid:
.contact form {
  display: grid;
  gap: 1rem;
  padding: 1.5rem;
}

label {
  display: grid;
  gap: 0.35rem;
  color: var(--muted);
  font-weight: 700;
}

input,
textarea,
select {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 0.55rem;
  background: var(--surface-light);
  color: var(--text);
  padding: 0.75rem;
  font: inherit;
}

What Makes the Form Usable?

Each label stacks its text above a full-width control. The inputs inherit the page typography and use a lighter surface to separate editable fields from the form card.

  • Continue with focus feedback, form status spacing, and footer layout:
input:focus,
textarea:focus,
select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

#form-status {
  min-height: 1.5rem;
  margin: 0;
  color: var(--accent);
}

footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  border-top: 1px solid var(--border);
  padding-block: 2rem;
  color: var(--muted);
}

Why Reserve Space for Form Status?

The minimum height gives future API feedback a stable place to appear. The visible focus outline helps keyboard users identify the active form control.

  • Save public/styles.css.
  • Refresh the portfolio in Firefox.
  • Press Tab until the name field receives focus.

You should see a mint outline around the focused field, consistent form spacing, and a bordered footer at the bottom of the page.

Form Controls Still Using Browser Defaults?

  • Check that the input selector group includes input, textarea, and select.
  • Check the preceding CSS block for an unclosed brace.

Still stuck? Help me debug why my form controls and focus outlines are not styled.

Responsive rules change the layout when the available width becomes tight. The same content then reads as one vertical flow on a narrow browser window.

  • Add the responsive media query at the end of public/styles.css.
@media (max-width: 760px) {
  nav {
    display: none;
  }

  .hero,
  .contact,
  .project-grid {
    grid-template-columns: 1fr;
  }

  .hero {
    gap: 1rem;
    padding-block: 3rem;
  }

  .section-heading,
  footer {
    align-items: start;
    flex-direction: column;
  }
}

What Changes on Narrow Screens?

  • The navigation hides to protect the limited header width.
  • The hero, contact area, and project grid switch to one column.
  • The selected-work heading and footer stack their content vertically.
  • Save public/styles.css.
  • Refresh the portfolio in Firefox.

You should see the finished dark visual system with a structured hero, skill pills, selected-work area, contact card, and footer.

Responsive Rules Not Applying?

  • Confirm that the media query appears after the desktop layout rules.
  • Confirm that its closing brace appears at the end of the file.

Need help? Help me find why my max-width media query does not switch the portfolio to one column.

Use these tabs to compare the complete stylesheet before the final personalization check.

✔️ Awesome, I've got everything!

Your responsive visual system is complete. Save public/styles.css before testing it.

ⓧ I'd like to double check the full code

:root {
  color-scheme: dark;
  --background: #08111f;
  --surface: #101c2f;
  --surface-light: #17263d;
  --text: #f4f7fb;
  --muted: #aab7ca;
  --accent: #63e6be;
  --accent-dark: #0d4d3f;
  --border: #2a3c56;
  --danger: #ff8787;
  font-family: Inter, ui-sans-serif, system-ui, sans-serif;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background:
    radial-gradient(circle at top right, rgba(99, 230, 190, 0.12), transparent 32rem),
    var(--background);
  color: var(--text);
  line-height: 1.6;
}

a {
  color: var(--accent);
  text-decoration: none;
}

a:hover,
a:focus-visible {
  text-decoration: underline;
}

.site-header,
main,
footer {
  width: min(1120px, calc(100% - 2rem));
  margin-inline: auto;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: 1.25rem;
}

.brand {
  color: var(--text);
  font-size: 1.1rem;
  font-weight: 800;
}

nav {
  display: flex;
  gap: 1rem;
}

.hero {
  min-height: 70vh;
  display: grid;
  grid-template-columns: 1.5fr 0.7fr;
  gap: 3rem;
  align-items: center;
  padding-block: 5rem;
}

.eyebrow {
  margin: 0 0 0.5rem;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  max-width: 850px;
  margin-bottom: 1.25rem;
  font-size: clamp(2.6rem, 7vw, 5.5rem);
  line-height: 1.02;
}

h2 {
  font-size: clamp(1.8rem, 4vw, 3rem);
  line-height: 1.15;
}

.hero-copy,
.section > p,
.contact > div > p {
  max-width: 720px;
  color: var(--muted);
  font-size: 1.08rem;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 0.7rem;
  padding: 0.75rem 1rem;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}

.button.primary {
  background: var(--accent);
  color: #04120e;
}

.button.secondary {
  border-color: var(--border);
  background: var(--surface);
  color: var(--text);
}

.status-card,
.project-card,
.contact form {
  border: 1px solid var(--border);
  border-radius: 1rem;
  background: rgba(16, 28, 47, 0.88);
  box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.18);
}

.status-card {
  padding: 1.5rem;
}

.status-card p {
  margin-bottom: 0.25rem;
  font-weight: 800;
}

.status-card small,
.project-card p,
.loading-message {
  color: var(--muted);
}

.status-dot {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  margin-right: 0.5rem;
  border-radius: 50%;
  background: var(--danger);
}

.status-card.online .status-dot {
  background: var(--accent);
  box-shadow: 0 0 1rem rgba(99, 230, 190, 0.7);
}

.section {
  padding-block: 4rem;
}

.skill-list,
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.skill-list span,
.tag-list span {
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  padding: 0.4rem 0.75rem;
}

.section-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.project-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

.project-card {
  padding: 1.4rem;
}

.project-card h3 {
  font-size: 1.35rem;
}

.project-highlight {
  border-left: 3px solid var(--accent);
  padding-left: 0.8rem;
}

.contact {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: 2rem;
  align-items: start;
}

.contact form {
  display: grid;
  gap: 1rem;
  padding: 1.5rem;
}

label {
  display: grid;
  gap: 0.35rem;
  color: var(--muted);
  font-weight: 700;
}

input,
textarea,
select {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 0.55rem;
  background: var(--surface-light);
  color: var(--text);
  padding: 0.75rem;
  font: inherit;
}

input:focus,
textarea:focus,
select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

#form-status {
  min-height: 1.5rem;
  margin: 0;
  color: var(--accent);
}

footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  border-top: 1px solid var(--border);
  padding-block: 2rem;
  color: var(--muted);
}

@media (max-width: 760px) {
  nav {
    display: none;
  }

  .hero,
  .contact,
  .project-grid {
    grid-template-columns: 1fr;
  }

  .hero {
    gap: 1rem;
    padding-block: 3rem;
  }

  .section-heading,
  footer {
    align-items: start;
    flex-direction: column;
  }
}
Personalize and test the interface

A portfolio becomes credible when every visible claim matches work you can explain. Focus the copy on backend decisions, trade-offs, and outcomes you can defend in a conversation.

  • Replace both visible instances of Your Name in public/index.html with your real professional name.
  • Update the hero role with the backend role you want recruiters to remember.
  • Update the hero headline with a truthful description of the systems you build.
  • Remove any skill label that you cannot support with a real example.
  • Add only skill labels that match your actual work.
  • Save public/index.html.

Lead With Evidence

Use measurable outcomes when you have them. A concrete reliability improvement or processing-time reduction gives a recruiter something useful to ask about.

Before narrowing the browser, which parts of the wide layout do you expect to stack into one column?

  • Return to http://127.0.0.1:8000 in Firefox.
  • Refresh the portfolio.
  • Drag the right edge of the browser window inward until the content reaches a narrow mobile-style width.

You should see the navigation hide. The hero, project area, and contact section should reflow into one column. The selected-work heading and footer should stack vertically.

  • Widen the browser window again.
  • Click Explore the live API in the hero.

You should reach the interactive API documentation at http://127.0.0.1:8000/docs.

Layout Not Reflowing?

  • Confirm that the page head still contains the viewport metadata.
  • Confirm that the responsive media query sits at the end of public/styles.css.
  • Confirm that the browser content area becomes narrower than 760px.

Still stuck? Help me troubleshoot why my responsive portfolio does not reflow on a narrow Firefox window.

That visual layer is now doing its job. Next, you will make the selected-work area depend on live backend data and expose the missing API capability.

Load Projects from the API

Your responsive portfolio already presents your backend skills through a polished interface. Now the project area needs to receive its content from your FastAPI application.

Static cards can only claim full-stack ability. This step makes the browser request project data so the missing backend capability becomes visible before you build it.

In this step, get ready to:
  • Replace the static project cards with a loading state.
  • Request project data through the browser Fetch API.
  • Inspect the missing endpoint as an intentional full-stack gap.
Replace static cards with a loading state

The HTML project section needs one stable container for content returned by the backend. A loading message gives visitors immediate feedback while the request is running.

  • In the Visual Studio Code Explorer sidebar, select public/index.html.
  • Find the selected-work section containing id="projects".
  • Replace that entire section with the code below:
      <section class="section" id="projects" aria-labelledby="projects-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Selected work</p>
            <h2 id="projects-title">Projects loaded from my API</h2>
          </div>
          <a href="/api/projects">View JSON</a>
        </div>
        <div class="project-grid" id="project-grid" aria-live="polite">
          <p class="loading-message">Loading projects from the API...</p>
        </div>
      </section>

What does this code do?

  • The project-grid container gives JavaScript one place to display loading text, project cards, or an error.
  • The aria-live="polite" attribute lets assistive technology announce updates without interrupting the visitor.
  • The loading message confirms that the browser is waiting for project data.
  • The View JSON link gives visitors a direct path to the backend response.
  • Save public/index.html.
  • Switch back to the Firefox tab from earlier.
  • Refresh the portfolio page.

Good progress. The project area now shows Loading projects from the API... instead of static cards.

Still seeing static project cards?

  • Confirm that you replaced the complete section containing id="projects".
  • Check that the replacement container still uses id="project-grid".
  • Save public/index.html before refreshing the page again.

Still stuck? Help me replace my static portfolio cards with the project loading container

✔️ Awesome, I've got everything!

Your project section now has the loading container that JavaScript updates.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta
      name="description"
      content="Backend-focused Python developer portfolio with a live FastAPI backend."
    >
    <title>Your Name | Python Backend Developer</title>
    <link rel="stylesheet" href="/styles.css">
    <script src="/app.js" defer></script>
  </head>
  <body>
    <header class="site-header">
      <a class="brand" href="/">Your Name</a>
      <nav aria-label="Primary navigation">
        <a href="#projects">Projects</a>
        <a href="#contact">Contact</a>
        <a href="/docs">API Docs</a>
      </nav>
    </header>

    <main>
      <section class="hero" aria-labelledby="hero-title">
        <div>
          <p class="eyebrow">Python backend developer</p>
          <h1 id="hero-title">I design reliable APIs and ship the interface around them.</h1>
          <p class="hero-copy">
            My focus is backend engineering with Python, typed APIs, validation, and
            maintainable systems. I also build responsive frontend experiences that
            make those systems useful.
          </p>
          <div class="hero-actions">
            <a class="button primary" href="#projects">View projects</a>
            <a class="button secondary" href="/docs">Explore the live API</a>
          </div>
        </div>
        <aside class="status-card" aria-label="API status">
          <span class="status-dot" aria-hidden="true"></span>
          <p id="api-status">Checking API...</p>
          <small>FastAPI health endpoint</small>
        </aside>
      </section>

      <section class="section" aria-labelledby="skills-title">
        <p class="eyebrow">Core capabilities</p>
        <h2 id="skills-title">Backend depth with full-stack delivery</h2>
        <div class="skill-list" aria-label="Technical skills">
          <span>Python</span>
          <span>FastAPI</span>
          <span>REST APIs</span>
          <span>Data validation</span>
          <span>SQL</span>
          <span>JavaScript</span>
          <span>Responsive CSS</span>
          <span>Git</span>
        </div>
      </section>

      <section class="section" id="projects" aria-labelledby="projects-title">
        <div class="section-heading">
          <div>
            <p class="eyebrow">Selected work</p>
            <h2 id="projects-title">Projects loaded from my API</h2>
          </div>
          <a href="/api/projects">View JSON</a>
        </div>
        <div class="project-grid" id="project-grid" aria-live="polite">
          <p class="loading-message">Loading projects from the API...</p>
        </div>
      </section>

      <section class="section contact" id="contact" aria-labelledby="contact-title">
        <div>
          <p class="eyebrow">Backend validation demo</p>
          <h2 id="contact-title">Send a test payload</h2>
          <p>
            This form sends JSON to a typed FastAPI endpoint. It validates and
            acknowledges the payload, but it does not store or email messages.
          </p>
        </div>
        <form id="contact-form">
          <label>
            Name
            <input name="name" autocomplete="name" required>
          </label>
          <label>
            Email
            <input name="email" type="email" autocomplete="email" required>
          </label>
          <label>
            Message
            <textarea name="message" rows="5" required></textarea>
          </label>
          <button class="button primary" type="submit">Validate message</button>
          <p id="form-status" role="status"></p>
        </form>
      </section>
    </main>

    <footer>
      <p>Built with Python, FastAPI, HTML, CSS, and JavaScript.</p>
      <a href="/docs">Test the backend</a>
    </footer>
  </body>
</html>
Create the project-loading script

The browser now needs a function that requests /api/projects. The function checks the response before parsing its JSON data.

  • Select the public/ folder in the Explorer sidebar.
  • Use the new-file control at the top of the Explorer sidebar.
  • Name the file app.js.
  • Add the project-loading logic by copying the code below:
const projectGrid = document.querySelector("#project-grid");

async function loadProjects() {
  try {
    const response = await fetch("/api/projects");
    if (!response.ok) {
      throw new Error(`Projects request returned ${response.status}`);
    }

    const projects = await response.json();
    projectGrid.replaceChildren(...projects.map(createProjectCard));
  } catch (error) {
    projectGrid.textContent =
      "The frontend is ready, but /api/projects is not available yet.";
  }
}

loadProjects();

What does this code do?

  • The projectGrid constant keeps a reference to the loading container.
  • The fetch("/api/projects") call asks the same-origin backend for project data.
  • The response.ok check sends unsuccessful responses into the error path.
  • The response.json() call parses a successful response into JavaScript data.
  • The catch block turns a failed request into a clear message for visitors.
  • Save public/app.js.

✔️ Awesome, I've got everything!

Your script now requests project data when the page loads.

ⓧ I'd like to double check the full code

const projectGrid = document.querySelector("#project-grid");

async function loadProjects() {
  try {
    const response = await fetch("/api/projects");
    if (!response.ok) {
      throw new Error(`Projects request returned ${response.status}`);
    }

    const projects = await response.json();
    projectGrid.replaceChildren(...projects.map(createProjectCard));
  } catch (error) {
    projectGrid.textContent =
      "The frontend is ready, but /api/projects is not available yet.";
  }
}

loadProjects();
Inspect the intentional API gap

The Network Monitor records each browser request after it opens. This lets you connect the visible page state to the exact backend response.

  • Return to the portfolio tab in Firefox.
  • Press Ctrl+Shift+E (Linux or Windows) or Cmd+Opt+E (macOS) to open the Network Monitor.

Before you refresh, do you think the project request can succeed with the current backend?

  • Refresh the portfolio page.

You'll see The frontend is ready, but /api/projects is not available yet. in the project area. The request for /api/projects shows a 404 status in the Network Monitor.

That result is intentional. The browser now proves that the interface depends on a backend capability that does not exist yet.

Not seeing the intentional error state?

  • Confirm that public/app.js is saved inside the public/ folder.
  • Check that public/index.html contains <script src="/app.js" defer></script> inside the document head.
  • Reload the page after the Network Monitor opens so it can record the request.
  • Confirm that the local server process from earlier is still running.

Still stuck? Help me debug why my portfolio does not show the missing /api/projects error state

You have exposed the exact gap between the interface and its backend. Next, you'll build the typed endpoint that turns this failed request into live project cards.

Build the Typed Portfolio API

Your frontend has already proved the gap. It can request project data, but the missing backend endpoint leaves every card empty.

Now you'll make FastAPI own that data through typed REST endpoints. Pydantic models will validate the JSON structure and describe it in the generated OpenAPI documentation.

In this step, get ready to:
  • Model your portfolio projects with typed Python data.
  • Create health and project endpoints with typed return values.
  • Render the live API data safely in the portfolio interface.
Model the project data

A typed model defines the shape every project must follow. This gives the backend one consistent contract for project names, summaries, technologies, and highlights.

  • In app.py, select everything above app.frontend("/", directory="public").
  • Replace the selected code with the following foundation:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Project(BaseModel):
    id: int
    name: str
    summary: str
    technologies: list[str]
    highlight: str

What does this code do?

  • The BaseModel import provides the base class for typed project data.
  • The Project model requires five fields with specific Python types.
  • The technologies field accepts a list of technology names for each project.
  • Save app.py.
  • Return to the terminal panel from earlier.

You should see Uvicorn reload the application without a Python traceback. That confirms the model syntax is valid.

Seeing a Python traceback?

  • Check that BaseModel is imported from pydantic.
  • Check that every field inside Project is indented by four spaces.

Still stuck? Help me fix my Project Pydantic model

The model is the contract. The next block creates three placeholder records that prompt you to describe real work without claiming projects or results you do not have.

  • In app.py, place your cursor between the Project model and app.frontend("/", directory="public").
  • Paste the typed project data below:
PROJECTS: list[Project] = [
    Project(
        id=1,
        name="Your strongest API project",
        summary="Replace this with the problem, architecture, and result of a real backend project.",
        technologies=["Python", "FastAPI", "PostgreSQL"],
        highlight="Explain one backend decision, such as validation, caching, or idempotency.",
    ),
    Project(
        id=2,
        name="Your systems or automation project",
        summary="Show how your Python code moved data, automated work, or improved reliability.",
        technologies=["Python", "Docker", "Redis"],
        highlight="Add a measurable result or a trade-off you handled.",
    ),
    Project(
        id=3,
        name="Your full-stack project",
        summary="Describe the user-facing workflow and the API behavior that powers it.",
        technologies=["JavaScript", "FastAPI", "Python"],
        highlight="Connect a frontend interaction to a concrete backend capability.",
    ),
]

How is the project data structured?

  • The PROJECTS annotation declares a list containing only Project objects.
  • Each record follows the same field contract defined by the model.
  • The placeholder copy asks for real architecture choices and measurable outcomes before publication.
  • Save app.py.
  • Return to the terminal panel from earlier.

You should see another successful reload without a traceback. The backend now holds three validated project records.

Does the project data stop the reload?

  • Check that every Project( entry closes with ).
  • Check that the complete list closes with ].
  • Check that every field name matches the Project model exactly.

Still stuck? Help me find the syntax error in my PROJECTS list

Add typed read endpoints

The frontend needs a route that returns the project list. A separate health route gives the status card a quick way to confirm that the backend is responding.

  • In app.py, locate app.frontend("/", directory="public").
  • Paste the health endpoint immediately above that line:
@app.get("/api/health")
def health() -> dict[str, str]:
    return {"status": "ok"}

What does the health endpoint prove?

  • The @app.get("/api/health") decorator registers a read endpoint.
  • The return annotation promises a dictionary with string keys and string values.
  • The response provides a small status value that the frontend can check.
  • Save app.py.

Before you test the new route, what response do you expect from a healthy backend?

  • Return to the browser tab from earlier.
  • Enter http://127.0.0.1:8000/api/health in the address bar.

You should see {"status":"ok"} in the browser. The backend now exposes a working health signal.

Does the health route show the portfolio page?

  • Confirm that the health endpoint appears above app.frontend("/", directory="public") in app.py.
  • Confirm that the browser path ends with /api/health.

Still stuck? Help me debug why my FastAPI health route is not returning JSON

The health route proves the server responds. The project route now needs to expose the typed records that the frontend requested in the previous step.

  • In app.py, place your cursor between the health function and app.frontend("/", directory="public").
  • Paste the project endpoint below:
@app.get("/api/projects")
def list_projects() -> list[Project]:
    return PROJECTS

Why use a typed return value?

  • The list[Project] annotation describes the complete response shape.
  • FastAPI validates the returned project data against that annotation.
  • FastAPI also adds the project schema to its interactive documentation.
  • Save app.py.
  • Enter http://127.0.0.1:8000/api/projects in the browser address bar.

You should see a JSON array containing three project objects. Each object includes the five fields declared by Project.

Does the project route return an error?

  • Confirm that PROJECTS appears above the route that returns it.
  • Confirm that the return annotation uses the exact model name Project.
  • Confirm that the project route appears above app.frontend("/", directory="public").

Still stuck? Help me fix my typed projects endpoint

Compare your backend file with the cumulative version before connecting the interface.

✔️ Awesome, I've got everything!

Great. Save app.py before moving to the frontend.

ⓧ I'd like to double check the full code

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Project(BaseModel):
    id: int
    name: str
    summary: str
    technologies: list[str]
    highlight: str


PROJECTS: list[Project] = [
    Project(
        id=1,
        name="Your strongest API project",
        summary="Replace this with the problem, architecture, and result of a real backend project.",
        technologies=["Python", "FastAPI", "PostgreSQL"],
        highlight="Explain one backend decision, such as validation, caching, or idempotency.",
    ),
    Project(
        id=2,
        name="Your systems or automation project",
        summary="Show how your Python code moved data, automated work, or improved reliability.",
        technologies=["Python", "Docker", "Redis"],
        highlight="Add a measurable result or a trade-off you handled.",
    ),
    Project(
        id=3,
        name="Your full-stack project",
        summary="Describe the user-facing workflow and the API behavior that powers it.",
        technologies=["JavaScript", "FastAPI", "Python"],
        highlight="Connect a frontend interaction to a concrete backend capability.",
    ),
]


@app.get("/api/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/api/projects")
def list_projects() -> list[Project]:
    return PROJECTS


app.frontend("/", directory="public")

Use this reference to confirm the model, data, routes, and frontend declaration appear in this order.

Connect the interface to live JSON

The backend response is ready, but the browser still needs a safe renderer for each project. The renderer will create DOM elements and assign text through textContent.

  • In public/app.js, select the existing const projectGrid = document.querySelector("#project-grid"); line.
  • Replace that line with the following DOM references and card renderer:
const apiStatus = document.querySelector("#api-status");
const statusCard = document.querySelector(".status-card");
const projectGrid = document.querySelector("#project-grid");

function createProjectCard(project) {
  const article = document.createElement("article");
  article.className = "project-card";

  const title = document.createElement("h3");
  title.textContent = project.name;

  const summary = document.createElement("p");
  summary.textContent = project.summary;

  const highlight = document.createElement("p");
  highlight.className = "project-highlight";
  highlight.textContent = project.highlight;

  const tags = document.createElement("div");
  tags.className = "tag-list";
  project.technologies.forEach((technology) => {
    const tag = document.createElement("span");
    tag.textContent = technology;
    tags.append(tag);
  });

  article.append(title, summary, highlight, tags);
  return article;
}

How does safe rendering work?

  • The browser creates a separate article for every project returned by the API.
  • The textContent assignments treat API values as text.
  • The technology loop creates one tag element for every item in the project data.
  • The final append call assembles each card in a predictable order.
  • Save public/app.js.
  • Refresh http://127.0.0.1:8000 in the browser.

You should see three project cards where the missing-endpoint message appeared before. That earlier failure is fixed by real backend data.

Are the project cards still missing?

  • Confirm that createProjectCard appears above loadProjects in public/app.js.
  • Confirm that loadProjects still passes each project to createProjectCard.
  • Confirm that /api/projects returns three objects in the browser.

Still stuck? Help me debug why my API project cards are not rendering

The project cards prove the data route works. The existing status card can now call the health endpoint and show whether the API is online.

  • In public/app.js, locate async function loadProjects().
  • Paste the health-check function immediately above it:
async function checkHealth() {
  try {
    const response = await fetch("/api/health");
    if (!response.ok) {
      throw new Error(`Health check returned ${response.status}`);
    }

    const health = await response.json();
    if (health.status !== "ok") {
      throw new Error("Unexpected health response");
    }

    apiStatus.textContent = "API online";
    statusCard.classList.add("online");
  } catch (error) {
    apiStatus.textContent = "API unavailable";
    statusCard.classList.remove("online");
  }
}

How does the health check update the page?

  • The function requests /api/health and rejects unsuccessful responses.
  • The parsed status must equal ok before the success state appears.
  • A successful check changes the status text and activates the green online styling.
  • Any failed check restores the unavailable state without breaking the rest of the page.
  • At the bottom of public/app.js, place your cursor immediately above loadProjects();.
  • Add the health-check call below:
checkHealth();

Why call the function here?

Calling checkHealth() at the bottom runs the request when the page loads. The status card then reflects the current backend response.

Before you refresh, which two visible parts of the portfolio should now depend on backend responses?

  • Save public/app.js.
  • Refresh http://127.0.0.1:8000 in the browser.

You should see API online in the status card. You should also see all three JSON-backed project cards.

Does the status stay unavailable?

  • Confirm that checkHealth(); appears above loadProjects(); at the bottom of public/app.js.
  • Confirm that the page still contains #api-status and .status-card.
  • Confirm that http://127.0.0.1:8000/api/health returns the expected status object.

Still stuck? Help me debug my frontend health check

Compare your frontend script with the cumulative version before running the documentation check.

✔️ Awesome, I've got everything!

Great. Save public/app.js before checking the API documentation.

ⓧ I'd like to double check the full code

const apiStatus = document.querySelector("#api-status");
const statusCard = document.querySelector(".status-card");
const projectGrid = document.querySelector("#project-grid");

function createProjectCard(project) {
  const article = document.createElement("article");
  article.className = "project-card";

  const title = document.createElement("h3");
  title.textContent = project.name;

  const summary = document.createElement("p");
  summary.textContent = project.summary;

  const highlight = document.createElement("p");
  highlight.className = "project-highlight";
  highlight.textContent = project.highlight;

  const tags = document.createElement("div");
  tags.className = "tag-list";
  project.technologies.forEach((technology) => {
    const tag = document.createElement("span");
    tag.textContent = technology;
    tags.append(tag);
  });

  article.append(title, summary, highlight, tags);
  return article;
}

async function checkHealth() {
  try {
    const response = await fetch("/api/health");
    if (!response.ok) {
      throw new Error(`Health check returned ${response.status}`);
    }

    const health = await response.json();
    if (health.status !== "ok") {
      throw new Error("Unexpected health response");
    }

    apiStatus.textContent = "API online";
    statusCard.classList.add("online");
  } catch (error) {
    apiStatus.textContent = "API unavailable";
    statusCard.classList.remove("online");
  }
}

async function loadProjects() {
  try {
    const response = await fetch("/api/projects");
    if (!response.ok) {
      throw new Error(`Projects request returned ${response.status}`);
    }

    const projects = await response.json();
    projectGrid.replaceChildren(...projects.map(createProjectCard));
  } catch (error) {
    projectGrid.textContent =
      "The frontend is ready, but /api/projects is not available yet.";
  }
}

checkHealth();
loadProjects();

Use this reference to confirm the DOM renderer, health check, project loader, and startup calls appear in this order.

Before the final check, do you expect the documented project response to match the cards on the portfolio page?

  • Enter http://127.0.0.1:8000/docs in the browser address bar.
  • Select GET /api/health in the interactive documentation.
  • Run the health request with the endpoint controls.
  • Select GET /api/projects in the interactive documentation.
  • Run the project request with the endpoint controls.

You should see successful JSON responses for both endpoints. The project response should contain the same three records shown as cards on the portfolio page.

That is the backend proof recruiters can inspect: typed data, live responses, generated schemas, and a frontend powered by the same API.

Your portfolio now proves a typed read path from Python models to browser-rendered cards. Next, you'll add a validated write operation and ship the complete application.

Validate Contact Requests and Deploy

Your FastAPI portfolio already serves typed project data. That foundation is working because the status badge confirms the API is online.

A recruiter still needs a typed write operation they can test outside your computer. This step adds backend validation to the contact demo.

A clear README makes the architecture easy to inspect. GitHub stores the versioned source.

Vercel turns that source into a public portfolio. Your finished deployment becomes evidence that recruiters can open for themselves.

In this step, get ready to:
  • Validate contact form submissions through a typed FastAPI endpoint.
  • Publish a documented project repository to GitHub.
  • Verify the live Vercel deployment across the portfolio and API documentation.
Add typed contact validation

Pydantic models describe the contact request and its receipt. FastAPI uses those types to validate incoming JSON before your endpoint runs.

  • In app.py, find the closing line of the Project model.
  • Insert these contact models above PROJECTS by copying the code below:
class ContactMessage(BaseModel):
    name: str
    email: str
    message: str


class ContactReceipt(BaseModel):
    accepted: bool
    message: str

What do these models define?

  • ContactMessage defines the three text fields that every contact request must contain.
  • ContactReceipt defines the response that confirms whether validation succeeded.
  • The model types become part of the generated API schema. Recruiters can inspect that contract through /docs.
  • Save app.py.
  • Check the Uvicorn terminal for a completed reload.

The server should reload without showing a Python traceback. Your contact schemas are now available to the application.

Seeing a Python error after the reload?

  • Check that both classes start at the left edge of app.py.
  • Confirm that every field line is indented with four spaces.
  • Make sure the models sit below Project and above PROJECTS.

Still stuck? Help me debug my Pydantic contact models

The models define the contract. The endpoint now accepts that contract and returns a typed receipt without storing or emailing the submitted message.

  • In app.py, find the list_projects() function.
  • Insert the contact endpoint below that function by copying the code below:
@app.post("/api/contact")
def submit_contact(message: ContactMessage) -> ContactReceipt:
    return ContactReceipt(
        accepted=True,
        message=f"Thanks, {message.name}. Your message passed backend validation.",
    )

How does the endpoint validate requests?

  • @app.post("/api/contact") registers the write operation at the contact path.
  • message: ContactMessage requires the request body to match the contact schema.
  • -> ContactReceipt declares the response type exposed in the API documentation.
  • The returned message uses the validated name field to make the receipt visible and specific.
  • Save app.py.
  • Check the Uvicorn terminal for another completed reload.

Before you refresh the documentation, do you expect the new contact operation to appear beside the two existing read operations?

  • Return to http://127.0.0.1:8000/docs in Firefox.
  • Refresh the page.
  • Expand POST /api/contact to inspect its request and response schemas.

You should see all three API operations. The new operation should show the fields from ContactMessage and the response shape from ContactReceipt.

Missing the contact operation?

  • Confirm that the contact route appears above app.frontend("/", directory="public") in app.py.
  • Check the Uvicorn terminal for an import or indentation error.
  • Refresh /docs after the server finishes reloading.

Need another pair of eyes? Help me find why POST /api/contact is missing from FastAPI docs

✔️ My contact endpoint appears in the docs

Great. Save app.py before moving to the browser integration.

ⓧ I want to double check app.py

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Project(BaseModel):
    id: int
    name: str
    summary: str
    technologies: list[str]
    highlight: str


class ContactMessage(BaseModel):
    name: str
    email: str
    message: str


class ContactReceipt(BaseModel):
    accepted: bool
    message: str


PROJECTS: list[Project] = [
    Project(
        id=1,
        name="Your strongest API project",
        summary="Replace this with the problem, architecture, and result of a real backend project.",
        technologies=["Python", "FastAPI", "PostgreSQL"],
        highlight="Explain one backend decision, such as validation, caching, or idempotency.",
    ),
    Project(
        id=2,
        name="Your systems or automation project",
        summary="Show how your Python code moved data, automated work, or improved reliability.",
        technologies=["Python", "Docker", "Redis"],
        highlight="Add a measurable result or a trade-off you handled.",
    ),
    Project(
        id=3,
        name="Your full-stack project",
        summary="Describe the user-facing workflow and the API behavior that powers it.",
        technologies=["JavaScript", "FastAPI", "Python"],
        highlight="Connect a frontend interaction to a concrete backend capability.",
    ),
]


@app.get("/api/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/api/projects")
def list_projects() -> list[Project]:
    return PROJECTS


@app.post("/api/contact")
def submit_contact(message: ContactMessage) -> ContactReceipt:
    return ContactReceipt(
        accepted=True,
        message=f"Thanks, {message.name}. Your message passed backend validation.",
    )


app.frontend("/", directory="public")

How should you use this reference?

Compare the model order and route order with your file. Keep app.frontend() at the bottom so API routes retain priority.

The browser needs references to the existing form and status element before it can send a request. These references connect the page markup to the JavaScript submit handler.

  • In public/app.js, find this existing line:
const projectGrid = document.querySelector("#project-grid");

Why locate this line?

The top of public/app.js groups references to page elements. The form references belong beside the existing project grid reference.

  • Replace that line with this three-line group:
const projectGrid = document.querySelector("#project-grid");
const contactForm = document.querySelector("#contact-form");
const formStatus = document.querySelector("#form-status");

What changed in the reference group?

  • contactForm points to the existing form with the #contact-form identifier.
  • formStatus points to the live status area with the #form-status identifier.

The submit handler uses the browser Fetch API to send the form as JSON. It displays the typed receipt after a successful response.

  • In public/app.js, find the final calls to checkHealth() and loadProjects().
  • Insert this submit handler directly above those calls by copying the code below:
contactForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  formStatus.textContent = "Sending JSON to the API...";

  const payload = Object.fromEntries(new FormData(contactForm).entries());

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Contact request returned ${response.status}`);
    }

    const receipt = await response.json();
    formStatus.textContent = receipt.message;
    contactForm.reset();
  } catch (error) {
    formStatus.textContent = "The API could not validate this message.";
  }
});

How does the form reach the API?

  • event.preventDefault() keeps the browser on the portfolio page during submission.
  • FormData collects the named form fields before they become a JavaScript object.
  • JSON.stringify(payload) creates the JSON request body expected by the contact model.
  • contactForm.reset() clears the fields only after the API returns a successful receipt.
  • Save public/app.js.
  • Return to the local portfolio at http://127.0.0.1:8000.

Before you submit the form, do you expect the page to reload or display the receipt in place?

  • Enter Ada Lovelace in the Name field.
  • Enter ada@example.com in the Email field.
  • Enter Testing the typed contact endpoint. in the Message field.
  • Click Validate message.

You should see Thanks, Ada Lovelace. Your message passed backend validation. below the button. The three form fields should clear.

That is the write path working from end to end. Your browser now sends typed JSON and displays the backend receipt.

Seeing the validation failure message?

  • Confirm that contactForm points to #contact-form.
  • Check that the request path is exactly /api/contact.
  • Inspect the Uvicorn terminal for a JavaScript request followed by a server error.

Still blocked? Help me debug the contact form request

✔️ My contact form shows the receipt

Excellent. Save public/app.js so the working browser integration is ready to publish.

ⓧ I want to double check public/app.js

const apiStatus = document.querySelector("#api-status");
const statusCard = document.querySelector(".status-card");
const projectGrid = document.querySelector("#project-grid");
const contactForm = document.querySelector("#contact-form");
const formStatus = document.querySelector("#form-status");

function createProjectCard(project) {
  const article = document.createElement("article");
  article.className = "project-card";

  const title = document.createElement("h3");
  title.textContent = project.name;

  const summary = document.createElement("p");
  summary.textContent = project.summary;

  const highlight = document.createElement("p");
  highlight.className = "project-highlight";
  highlight.textContent = project.highlight;

  const tags = document.createElement("div");
  tags.className = "tag-list";
  project.technologies.forEach((technology) => {
    const tag = document.createElement("span");
    tag.textContent = technology;
    tags.append(tag);
  });

  article.append(title, summary, highlight, tags);
  return article;
}

async function checkHealth() {
  try {
    const response = await fetch("/api/health");
    if (!response.ok) {
      throw new Error(`Health check returned ${response.status}`);
    }

    const health = await response.json();
    if (health.status !== "ok") {
      throw new Error("Unexpected health response");
    }

    apiStatus.textContent = "API online";
    statusCard.classList.add("online");
  } catch (error) {
    apiStatus.textContent = "API unavailable";
    statusCard.classList.remove("online");
  }
}

async function loadProjects() {
  try {
    const response = await fetch("/api/projects");
    if (!response.ok) {
      throw new Error(`Projects request returned ${response.status}`);
    }

    const projects = await response.json();
    projectGrid.replaceChildren(...projects.map(createProjectCard));
  } catch (error) {
    projectGrid.textContent =
      "The frontend is ready, but /api/projects is not available yet.";
  }
}

contactForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  formStatus.textContent = "Sending JSON to the API...";

  const payload = Object.fromEntries(new FormData(contactForm).entries());

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Contact request returned ${response.status}`);
    }

    const receipt = await response.json();
    formStatus.textContent = receipt.message;
    contactForm.reset();
  } catch (error) {
    formStatus.textContent = "The API could not validate this message.";
  }
});

checkHealth();
loadProjects();

What should you compare?

Check the five element references at the top. Confirm that the submit handler appears before the final page-load calls.

Document and publish the repository

A recruiter should be able to understand the project without reading every source file. The README explains the architecture and gives them a direct testing path.

  • In the VS Code Explorer sidebar, create README.md inside the backend-first-portfolio folder.
  • Add the project overview by copying the Markdown below:
# Backend-First Python Developer Portfolio

A responsive portfolio that leads with Python backend engineering while proving full-stack delivery. FastAPI provides typed project data, request validation, health status, and interactive documentation. HTML, CSS, and JavaScript provide the user-facing experience.

## Features

- Typed project responses with Pydantic
- Validated contact-demo request body
- Automatic interactive documentation at `/docs`
- Responsive frontend that consumes the API
- Loading, success, and error states
- One-project deployment to Vercel

## Architecture

Browser interface -> FastAPI endpoints -> Pydantic validation and typed JSON responses

The contact endpoint is a validation demonstration. It does not persist or email submitted data.

What does the opening document?

  • The overview positions the project as backend-focused full-stack work.
  • The feature list names the behaviors a recruiter can test.
  • The architecture section shows the request path from the browser to typed responses.
  • The contact note clearly states that the demo does not store or email messages.
  • Save README.md.
  • Confirm that README.md appears in the VS Code Explorer sidebar.

The file should now show the title, six features, the architecture flow, and the contact-demo limitation.

Does the Markdown look unstructured?

  • Confirm that the title starts with one # character.
  • Check that section headings start with two # characters.
  • Keep each feature on its own line beginning with a hyphen.

Need help with the structure? Help me check my README headings and feature list

The next section makes the project repeatable. It records the exact local commands and the three API paths exposed by the application.

  • In README.md, add this section below the contact-demo note:
## Run locally

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
uvicorn app:app --reload
```

Open `http://127.0.0.1:8000` for the portfolio and `http://127.0.0.1:8000/docs` for the API documentation.

## API endpoints

- `GET /api/health`
- `GET /api/projects`
- `POST /api/contact`

Why include local setup and endpoints?

The local commands let another developer recreate your environment from the pinned requirements. The endpoint list gives reviewers a quick map of the API surface.

  • Save README.md.
  • Confirm that the local setup block contains four commands.
  • Confirm that the endpoint list contains three operations.

Your README should now show a complete local run path followed by the health, project, and contact operations.

Is the command block rendering as normal text?

  • Check that the opening code fence includes bash.
  • Confirm that the closing code fence sits below the Uvicorn command.
  • Keep a blank line before the API endpoints heading.

Still seeing broken formatting? Help me fix the fenced command block in README.md

The final README sections set an honesty standard for personalization. They also tell reviewers how to test the API and deploy the application.

  • In README.md, add the final sections below the API endpoint list:
## Personalize before publishing

Replace the placeholder name, headline, skills, and `PROJECTS` entries with truthful details. Prioritize the problem, backend architecture, trade-offs, and measurable result for each project.

## Test

1. Open `/docs`.
2. Execute `GET /api/projects` and inspect the typed response.
3. Execute `POST /api/contact` with all required fields.
4. Remove one required field and confirm the API rejects the payload.

## Deployment

Push the repository to GitHub, import it as a Vercel project, and verify the homepage plus `/docs` on the production URL.

What do the final sections add?

  • The personalization note keeps every claim tied to work you actually completed.
  • The test sequence shows how to inspect successful responses and validation failures.
  • The deployment note records the path from GitHub to the production application.
  • Save README.md.
  • Scroll from the title to the deployment section.

You should see one continuous document covering the product, architecture, local setup, endpoints, personalization, testing, and deployment.

Is a README section missing?

  • Check that every section begins on a new line.
  • Confirm that the test instructions use the numbers 1 through 4.
  • Compare the section order with the full-file reference below.

Want help comparing the sections? Help me find the missing section in README.md

✔️ My README covers the complete project

Your repository now has a recruiter-friendly guide. Save README.md one more time before committing.

ⓧ I want to double check README.md

# Backend-First Python Developer Portfolio

A responsive portfolio that leads with Python backend engineering while proving full-stack delivery. FastAPI provides typed project data, request validation, health status, and interactive documentation. HTML, CSS, and JavaScript provide the user-facing experience.

## Features

- Typed project responses with Pydantic
- Validated contact-demo request body
- Automatic interactive documentation at `/docs`
- Responsive frontend that consumes the API
- Loading, success, and error states
- One-project deployment to Vercel

## Architecture

Browser interface -> FastAPI endpoints -> Pydantic validation and typed JSON responses

The contact endpoint is a validation demonstration. It does not persist or email submitted data.

## Run locally

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
uvicorn app:app --reload
```

Open `http://127.0.0.1:8000` for the portfolio and `http://127.0.0.1:8000/docs` for the API documentation.

## API endpoints

- `GET /api/health`
- `GET /api/projects`
- `POST /api/contact`

## Personalize before publishing

Replace the placeholder name, headline, skills, and `PROJECTS` entries with truthful details. Prioritize the problem, backend architecture, trade-offs, and measurable result for each project.

## Test

1. Open `/docs`.
2. Execute `GET /api/projects` and inspect the typed response.
3. Execute `POST /api/contact` with all required fields.
4. Remove one required field and confirm the API rejects the payload.

## Deployment

Push the repository to GitHub, import it as a Vercel project, and verify the homepage plus `/docs` on the production URL.

How should you compare the file?

Check every heading and list item against your saved file. Keep the wording truthful when you personalize the portfolio before sharing it.

Git records the finished project as a reproducible snapshot. An empty remote repository can receive that existing history without conflicting starter files.

  • Return to GitHub in your browser.
  • Use the new-repository form to create a repository named backend-first-portfolio.
  • Set the repository visibility to public.
  • Leave every repository initialization option disabled.
  • Finish creating the empty repository.

GitHub should show the setup instructions for an empty repository. Keep that page available because it contains your repository URL.

  • Return to the VS Code terminal from earlier.
  • Initialize the local repository on the main branch by running this command:
git init -b main

What does this command create?

The command starts Git history inside the existing project folder. It names the first branch main from the beginning.

The terminal should report that it initialized a repository in your portfolio folder.

Could Git not initialize the repository?

  • Confirm that the VS Code terminal is attached to the backend-first-portfolio folder.
  • Check that Git is available in the terminal.
  • Use your Linux distribution's Git installation guidance if the branch option is unsupported.

Need environment-specific help? Help me initialize this project on the main Git branch

  • Create the first project snapshot by running these commands:
git add .
git commit -m "Build backend-first portfolio"

What goes into the first commit?

  • git add . stages the application files from the current folder.
  • git commit records those staged files as the first project snapshot.
  • The existing .gitignore keeps .venv/ and Python cache files outside the commit.

Git should report a new commit containing the portfolio source, pinned requirements, and README.

Could Git not create the commit?

  • Read the terminal message to check whether Git needs an author identity.
  • Follow the identity instructions shown by Git.
  • Run the staging and commit commands again after completing that configuration.

Need help with the terminal message? Help me resolve my first Git commit error

Copy the repository URL from the empty GitHub repository page. Replace REMOTE-URL in the command with that copied value.

Git may request GitHub authentication if this terminal is not already connected. Complete the browser or terminal authentication flow if it appears.

Before you publish, do you expect the ignored .venv/ directory to appear in the remote repository?

  • Connect the remote repository and publish main by running these commands:
git remote add origin REMOTE-URL
git push -u origin main

How does the repository reach GitHub?

  • git remote add origin names the GitHub repository as the local repository's origin.
  • git push -u origin main uploads the commit and links the local branch to the remote branch.
  • Return to the GitHub repository page.
  • Refresh the repository page.

You should see README.md, app.py, requirements.txt, and the public directory on the main branch. The ignored .venv/ directory should be absent.

Your source is now public and documented. A recruiter can inspect the architecture before opening the live application.

Did the push fail?

  • Confirm that you replaced REMOTE-URL with the URL from your empty repository.
  • Complete any GitHub authentication flow requested by Git.
  • Check that the destination repository stayed empty before the first push.

Still blocked? Help me push my main branch to GitHub

Deploy and verify the live portfolio

Deployment can feel risky because it makes your work public. This personal, non-commercial portfolio fits the free Vercel Hobby plan within its included usage.

Vercel reads the FastAPI application from app.py and connects deployments to the GitHub repository. Future pushes can create updated deployments automatically.

  • Return to your Vercel dashboard in the browser.
  • Select Add New….
  • Select Project.
  • Choose the backend-first-portfolio GitHub repository.
  • Keep the detected FastAPI project settings.

The first deployment can take a few minutes while Vercel builds the application. Leave the deployment page open while its status is in progress.

  • Start the deployment.
  • Wait for Vercel to show that the production deployment is ready.
  • Record the production address here: your Vercel production URL.

The production address proves that the repository built successfully. The next checks confirm that the frontend and every API path survived deployment.

  • Enter your Vercel production URL in the Firefox address bar.
  • Press Enter.

You should see the responsive portfolio with the API online status. The project cards should load from the deployed backend.

  • Add /api/health to your Vercel production URL in the address bar.
  • Press Enter.

You should see {"status":"ok"} from the production health endpoint.

  • Replace /api/health with /api/projects in the address bar.
  • Press Enter.

You should see the JSON array containing your three typed project records.

  • Replace /api/projects with /docs in the address bar.
  • Press Enter.
  • Count the operations listed in the interactive documentation.

You should see GET /api/health, GET /api/projects, and POST /api/contact.

Before the final check, do you expect the production form to return the same typed receipt as your local form?

  • Return to your Vercel production URL.
  • Enter Ada Lovelace in the Name field.
  • Enter ada@example.com in the Email field.
  • Enter Testing the production contact endpoint. in the Message field.
  • Click Validate message.

You should see Thanks, Ada Lovelace. Your message passed backend validation. on the deployed page. The form fields should reset after the response arrives.

You shipped the complete product. Recruiters can now browse the responsive interface, inspect live JSON, execute the API operations, and test typed backend validation.

Does the production test fail?

  • Open the Vercel deployment details to confirm that the latest GitHub commit was deployed.
  • Check that app.py remains at the top level of the repository.
  • Confirm that requirements.txt contains the three pinned dependencies.

Need deployment help? Help me debug my Vercel FastAPI deployment

Secret mission

Filter Projects Through the Backend

Give recruiters a faster way to inspect the work that matches their interests. You will add a technology selector whose choices become backend query parameters and control the project cards returned by the API.

Clean Up Your Resources

Clean Up Your Resources

This personal portfolio uses Vercel Hobby within its free included usage, so the main project has no ongoing cost. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local virtual environment .venv/ containing the pinned project dependencies.
  • Public GitHub repository containing the portfolio source code and commit history.
  • Connected Vercel Hobby project serving the production portfolio and API.

Keep everything running

No action needed. Choose this if you want recruiters to keep accessing the deployed portfolio and its interactive API documentation.

  • Leave the Vercel project active.
  • Keep the GitHub repository connected for future deployments.
  • Retain .venv/ for future local development.

Pause - I'll come back to this later

Pause the Vercel deployment while preserving its project configuration. Keep your repository and local environment ready for your return.

  • Return to the Vercel project from earlier.
  • Select Settings.
  • Select General.
  • Find Pause Project above the deletion area.
  • Enter the displayed project name in the confirmation prompt.
  • Confirm Pause Project.
  • Keep .venv/ in the local project directory.

Your deployment is now paused while its configuration remains available. You can resume it later from the same area.

Delete - I don't want to use this again

Remove the hosted project, repository, and local environment to start fresh. This is the final option, so pause here if you still need the production URL or commit history.

  • Return to the Vercel project from earlier.
  • Select Settings.
  • Select General.
  • Scroll to the bottom of the page to find Delete Project.
  • Select Delete Project.
  • Complete the confirmation flow.

That closes the hosted side of the project. Vercel removes its deployments, domains, environment variables, and project settings.

  • Return to the public GitHub repository from earlier.
  • Open the repository settings page.
  • Locate the repository deletion control near the bottom of that page.
  • Complete GitHub's confirmation flow.

Your remote source history is now removed. The local project files remain on your Linux machine.

  • Delete .venv/ from the local project directory with your Linux file manager.

The project-local dependencies are now removed. You can recreate them later from requirements.txt if you keep the remaining source files.

Nice Work!

Nice Work!

You did it! Your live portfolio now turns backend engineering claims into a product recruiters can open and test.

You've learned how to:

  • Build a responsive portfolio that leads with truthful evidence of your Python backend work. Show recruiters a polished interface that reflows cleanly on narrow screens.
  • Create typed GET and POST endpoints with Pydantic request validation. Expose their schemas through FastAPI documentation at /docs.
  • Connect browser JavaScript to live JSON data with the Fetch API. Publish the repository through GitHub to a live Vercel deployment.
  • Secret Mission: Extend GET /api/projects to filter PROJECTS with an optional technology query parameter. Connect that backend filter to #technology-filter so the deployed page shows only matching cards.

Ready to quiz yourself?