Build a Simple “Country Store” With FastAPI, Templates, and Tailwind
Learning Templates, Models and Routes in Python

This tutorial walks you through building a minimal “country store” web app in Python. You’ll start from a clean FastAPI-based starter, strip it down to the essentials, and then add a header, a product grid, and a basic data model. It’s designed for hands-on learning: short steps, copy-paste code, and visible results.
Watch the source walkthrough here: FastOpp Beginner - Country Store Pt 1 - templates, models, routes.
What you’ll build
A FastAPI app using a templates folder for HTML.
A minimal homepage with a Tailwind-styled header and a responsive product grid.
A simple
Foodmodel using SQLModel (with Pydantic typing) and a route that passes data into the template.
Prerequisites
Python installed.
uvinstalled for fast env and script runs. The video usesuvfor all commands.
1) Get the starter and prune it
Download the starter repo as a ZIP and rename the folder so you can treat it as your project. The video intentionally avoids cloning to keep things one-way and simple. Then remove extraneous directories so you can focus on the core app. The stock demo is intentionally feature-rich, which can distract at first. We’ll remove most of it and build up a clean page.
Example shell commands (run from the project root):
# Show dotfiles to confirm structure
ls -a
# Remove folders you don’t need in this tutorial
rm -rf .github blog docs tests
These deletions mirror the video’s “reduce noise first” approach so the learning path stays focused.
2) Initialize, create a superuser, and run the server
The project uses SQLite by default. Initialize the database, create an admin user, and boot the dev server:
uv run oppman.py db # initialize the database
uv run oppman.py superuser # create a superuser
uv run oppman.py runserver # start the dev server (http://127.0.0.1:8000)
You should see the app on port 8000.
Before

3) Simplify the homepage template
Open templates/index.html. Find the <main>...</main> block and delete almost everything inside, leaving one wrapper <div> for content. The goal is a blank canvas. Then start by adding a headline:
<h1>Food Products</h1>
If you expected a big, pre-styled <h1>, remember Tailwind is utility-first. You opt into visual styles via classes, not element names. For example:
<h1 class="text-2xl font-bold">Food Products</h1>
That change applies explicit size and weight. Tailwind doesn’t style h1 by default.
4) Swap in your own logo and tidy the header/footer
I found this online. Since this is just for practice, I am not worrying about copyright issues.

Drop your logo in static/images/<your_store>/ and update the header include:
File: templates/partials/header.html
<img
src="{{ url_for('static', path='/images/jesse_store/country_store.png') }}"
alt="Jesse's Country Store"
class="h-10 w-auto"
/>
<span class="ml-2 font-semibold">Jesse’s Country Store</span>
Removing More
Remove menu sections you won’t use. Remove everything from
<!-- Demo Pages -->
to
<!-- Management - Only show when logged in -->
Quick note: The Management menu item only shows when logged in. Are you logged in? I wasn't. You just created the superuser. You can login.
Do same thing for Management, delete everything between
<!-- Management - Only show when logged in -->
and
<!-- Database Status -->
Keep “Database Status” visible; it’s handy later when you start persisting data.
Apply the same cleanup in templates/partials/footer.html: point the image to your logo, remove irrelevant links, and keep a single copyright line. notes transcript
Tip: If you strip too much, you can always refer back to the original files you downloaded.
After

5) Add a responsive product grid with Jinja2
Start with a simple loop to render placeholder products. In templates/index.html, under your headline:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for i in range(5)%}
<div>
product {{ i }}
</div>
{% endfor %}
</div>
Also, note there there is a div surrounding the loop:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
It should show this:

6) Define a data model with SQLModel
Next, create a minimal Food model that you can use right away. For now, we’re not using a database. But that would be a next step.
File: models.py
Add this under your user model (or near other models). You can grab an existing model, Products, copy-paste, and add as a next section in under User.
The class is Food, the tablename is foods.
import uuid
from sqlmodel import SQLModel, Field
class Food(SQLModel, table=True):
__tablename__ = "foods" # type: ignore
id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(max_length=100, nullable=False)
description: str | None = Field(default=None, nullable=True)
SQLModel builds on Pydantic, so your fields are validated and you get excellent editor hints and autocompletion.
You don’t write self.id or self.name; because you're extending SQLModel, which handles it for you.
Later you can run migrations and store real data. For now, you’ll construct a few
Foodobjects in the route and pass them to the template.
7) Pass data to the template from the home route
Open the main pages route and send a list of foods to the template.
File: routes/pages.py
Find the home route:
@router.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
"""Home page"""
Import the model at the top:
from models import Food
Inside the read_root function, instantiate some items and pass them to the template context:
foods: list[Food] = [
Food(name="burrito", description="tasty food"),
Food(name="quesadilla", description="cheesy food"),
Food(name="nachos", description="gooey food"),
Food(name="tacos", description="crunchy food"),
]
return templates.TemplateResponse(
"index.html",
{
"request": request,
"title": "Country Store · Food Specials",
"foods": foods,
},
)
The video demonstrates first passing strings, then upgrading to Food objects. Both approaches work, but using the model sets you up for persistence.
8) Render model fields in the template
Update your grid loop to use the foods variable:
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for food in foods %}
<div class="bg-white rounded-2xl shadow-lg p-4">
<div class="text-xl font-bold">{{ food.name }}</div>
<div class="text-sm text-gray-600">{{ food.description }}</div>
</div>
{% endfor %}
</div>
Reload the page. You should see real names and descriptions instead of Product 0…4. This small change models a common production pattern: define types, construct instances in the route, and render them via Jinja2.

9) Verify live styling changes
With uv run oppman.py runserver active, edits to templates are visible on refresh. As you iterate:
Tweak Tailwind classes (
text-xl,font-bold,rounded-2xl,shadow-lg,p-4).Resize the browser manually to confirm the grid collapses from three columns to one on narrow screens.
10) Common gotchas
“Why doesn’t my
<h1>look big?” Tailwind is opt-in. Add classes liketext-2xlandfont-bold. Element names do not imply styling.“My logo doesn’t show.” Confirm the path matches your folder structure and file extension, and that you use
url_for('static', path='...')in the template include.“I don’t see ‘Management’ in the header.” That section only appears when logged in. If you removed it for simplicity, that’s fine; re-add it later from the original template if needed.
11) Where to go next
Persist your
Fooditems. Replace hard-coded objects with records stored via SQLModel. Migrate your schema and add CRUD routes.Add images per item. Extend
Foodwith animage_urlfield and render images in the grid cards.Build a detail page. Add a
/foods/{id}route and a detail template with more information.Introduce forms. Add a simple “add food” form. Validate with Pydantic types and show user feedback on errors.
These enhancements follow naturally from the learning flow in the video: templates → styling → models → routes → data.
Full step list (copy-paste friendly)
Download the starter ZIP and rename the folder for your project.
Prune unneeded directories:
.github,blog,docs,tests.Initialize DB:
uv run oppman.py db.Create superuser:
uv run oppman.py superuser.Run dev server:
uv run oppman.py runserver.Simplify
templates/index.html: keep a single wrapper<div>. Add<h1 class="text-2xl font-bold">Food Products</h1>.Swap logo in
templates/partials/header.htmland clean the header/footer. Keep Database Status.Add grid and a simple Jinja2 loop for placeholders.
Create model
Foodinmodels.pywithid,name,description.Update route in
routes/pages.pyto passfoodsinto the template.Render fields in the template with
{{ food.name }}and{{ food.description }}.
Closing
You now have a compact FastAPI app that renders server-side templates with Tailwind utilities and a typed model!
The flow you followed—start simple, delete distractions, add one concern at a time—lets you see each concept clearly: routes map URLs to handlers, templates display data, models define structure and validation, and utility classes give you predictable, responsive styling without a heavyweight CSS framework.
From here, persisting data, adding forms, and introducing authentication are incremental extensions, not re-writes.
Rewatch any segment of the YouTube walkthrough to reinforce a step, then keep iterating.
Source: FastOpp Beginner - Country Store Pt 1 - templates, models, routes




