add a DB
This commit is contained in:
parent
3b2db12fbf
commit
969f686c03
@ -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
|
||||
|
||||
@ -6,6 +6,7 @@ COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py ./
|
||||
COPY db.py ./
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
|
||||
@ -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
38
backend/db.py
Normal 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)
|
||||
@ -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
35
docker-compose.yml
Normal 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:
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,11 +69,35 @@ function App() {
|
||||
</p>
|
||||
</header>
|
||||
<main className="app-main">
|
||||
<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>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user