This commit is contained in:
Liam Fitzpatrick 2026-01-11 17:31:58 -05:00
parent 3b2db12fbf
commit 969f686c03
8 changed files with 231 additions and 6 deletions

View File

@ -22,11 +22,14 @@ cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL="postgresql+psycopg2://user:password@localhost:5432/dbname"
uvicorn app:app --reload --port 5000
```
FastAPI will run on `http://localhost:5000`.
Database health check is available at `http://localhost:5000/api/db/health`.
## Frontend
```bash

View File

@ -6,6 +6,7 @@ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py ./
COPY db.py ./
EXPOSE 5000

View File

@ -1,5 +1,10 @@
from fastapi import FastAPI
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from sqlalchemy import insert
from sqlalchemy.exc import SQLAlchemyError
from db import check_database_connection, create_database_tables, engine, items_table
app = FastAPI()
app.add_middleware(
@ -11,7 +16,46 @@ app.add_middleware(
)
class ItemCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
class ItemResponse(BaseModel):
id: int
name: str
@app.on_event("startup")
def on_startup() -> None:
create_database_tables()
@app.get("/hello")
@app.get("/api/hello")
def hello():
return {"message": "Hello from FastAPI bro"}
@app.get("/db/health")
def db_health():
try:
check_database_connection()
except SQLAlchemyError as exc:
raise HTTPException(status_code=500, detail="Database connection failed") from exc
return {"status": "ok"}
@app.post("/items", response_model=ItemResponse, status_code=201)
def create_item(payload: ItemCreate):
try:
with engine.begin() as connection:
result = connection.execute(
insert(items_table)
.values(name=payload.name)
.returning(items_table.c.id, items_table.c.name)
)
row = result.one()
except SQLAlchemyError as exc:
raise HTTPException(status_code=500, detail="Failed to create item") from exc
return {"id": row.id, "name": row.name}

38
backend/db.py Normal file
View File

@ -0,0 +1,38 @@
import os
from sqlalchemy import (
Column,
DateTime,
Integer,
MetaData,
String,
Table,
create_engine,
text,
)
from sqlalchemy.sql import func
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+psycopg2://postgres:postgres@localhost:5432/postgres",
)
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
metadata = MetaData()
items_table = Table(
"items",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("name", String(255), nullable=False),
Column("created_at", DateTime(timezone=True), server_default=func.now()),
)
def check_database_connection() -> None:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
def create_database_tables() -> None:
metadata.create_all(engine)

View File

@ -1,2 +1,4 @@
fastapi==0.111.0
psycopg2-binary==2.9.9
SQLAlchemy==2.0.31
uvicorn[standard]==0.30.1

35
docker-compose.yml Normal file
View File

@ -0,0 +1,35 @@
services:
db:
image: docker.io/library/postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
backend:
build: ./backend
restart: unless-stopped
environment:
DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/postgres
ports:
- "5000:5000"
depends_on:
- db
frontend:
build: ./frontend
restart: unless-stopped
environment:
BACKEND_URL: http://backend:5000
ports:
- "5173:80"
depends_on:
- backend
volumes:
postgres_data:

View File

@ -18,6 +18,13 @@
justify-content: center;
}
.card-grid {
display: grid;
gap: 24px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
width: min(780px, 100%);
}
.card {
border: 1px solid #d9dee7;
border-radius: 12px;
@ -36,3 +43,46 @@
color: #b00020;
margin-top: 8px;
}
.form {
display: grid;
gap: 12px;
margin-top: 12px;
}
.form-label {
font-weight: 600;
color: #2a3350;
}
.form-input {
border: 1px solid #ccd3df;
border-radius: 8px;
padding: 10px 12px;
font-size: 1rem;
}
.form-input:focus {
outline: 2px solid #7f8cff;
border-color: transparent;
}
.form-button {
border: none;
border-radius: 999px;
padding: 10px 16px;
background: #1f2a44;
color: #ffffff;
font-weight: 600;
cursor: pointer;
}
.form-button:hover {
background: #111a2b;
}
.status {
margin-top: 8px;
color: #1a7f37;
font-weight: 600;
}

View File

@ -4,6 +4,9 @@ import "./App.css";
function App() {
const [message, setMessage] = useState("loading...");
const [error, setError] = useState("");
const [itemName, setItemName] = useState("");
const [itemStatus, setItemStatus] = useState("");
const [itemError, setItemError] = useState("");
useEffect(() => {
let isMounted = true;
@ -32,6 +35,31 @@ function App() {
};
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
setItemStatus("Saving...");
setItemError("");
try {
const response = await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: itemName }),
});
if (!response.ok) {
throw new Error(`Save failed: ${response.status}`);
}
const saved = await response.json();
setItemStatus(`Saved item #${saved.id}: ${saved.name}`);
setItemName("");
} catch (err) {
setItemError(err.message);
setItemStatus("");
}
};
return (
<div className="app">
<header className="app-header">
@ -41,10 +69,34 @@ function App() {
</p>
</header>
<main className="app-main">
<div className="card">
<h2>API Response</h2>
<p className="message">{message}</p>
{error ? <p className="error">{error}</p> : null}
<div className="card-grid">
<div className="card">
<h2>API Response</h2>
<p className="message">{message}</p>
{error ? <p className="error">{error}</p> : null}
</div>
<div className="card">
<h2>Create Item</h2>
<form className="form" onSubmit={handleSubmit}>
<label className="form-label" htmlFor="item-name">
Item name
</label>
<input
id="item-name"
className="form-input"
type="text"
value={itemName}
onChange={(event) => setItemName(event.target.value)}
placeholder="Type a name"
required
/>
<button className="form-button" type="submit">
Save to DB
</button>
</form>
{itemStatus ? <p className="status">{itemStatus}</p> : null}
{itemError ? <p className="error">{itemError}</p> : null}
</div>
</div>
</main>
</div>