add a DB
This commit is contained in:
parent
3b2db12fbf
commit
969f686c03
@ -22,11 +22,14 @@ cd backend
|
|||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
export DATABASE_URL="postgresql+psycopg2://user:password@localhost:5432/dbname"
|
||||||
uvicorn app:app --reload --port 5000
|
uvicorn app:app --reload --port 5000
|
||||||
```
|
```
|
||||||
|
|
||||||
FastAPI will run on `http://localhost:5000`.
|
FastAPI will run on `http://localhost:5000`.
|
||||||
|
|
||||||
|
Database health check is available at `http://localhost:5000/api/db/health`.
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -6,6 +6,7 @@ COPY requirements.txt ./
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY app.py ./
|
COPY app.py ./
|
||||||
|
COPY db.py ./
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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 = FastAPI()
|
||||||
app.add_middleware(
|
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("/hello")
|
||||||
@app.get("/api/hello")
|
|
||||||
def hello():
|
def hello():
|
||||||
return {"message": "Hello from FastAPI bro"}
|
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
|
fastapi==0.111.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
SQLAlchemy==2.0.31
|
||||||
uvicorn[standard]==0.30.1
|
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;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
width: min(780px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #d9dee7;
|
border: 1px solid #d9dee7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -36,3 +43,46 @@
|
|||||||
color: #b00020;
|
color: #b00020;
|
||||||
margin-top: 8px;
|
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() {
|
function App() {
|
||||||
const [message, setMessage] = useState("loading...");
|
const [message, setMessage] = useState("loading...");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [itemName, setItemName] = useState("");
|
||||||
|
const [itemStatus, setItemStatus] = useState("");
|
||||||
|
const [itemError, setItemError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@ -41,10 +69,34 @@ function App() {
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
<div className="card">
|
<div className="card-grid">
|
||||||
<h2>API Response</h2>
|
<div className="card">
|
||||||
<p className="message">{message}</p>
|
<h2>API Response</h2>
|
||||||
{error ? <p className="error">{error}</p> : null}
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user