From Zero to Deployed: Building a Full-Stack App with Python in 2 Hours
You've got two hours, a Python environment, and an idea. That's all you need. In this tutorial, we'll build and deploy a fully functional full-stack web application — backend API, database, and a clean frontend — without cutting corners or hand-waving the hard parts.
What We're Building (and Why This Stack)
We're building a Task Manager API with a minimal frontend — simple enough to finish in a session, complex enough to teach you real patterns you'll reuse on every project.
The stack:
- FastAPI — modern, async-ready Python web framework
- SQLite + SQLAlchemy — zero-config database that works out of the box
- Jinja2 — server-side HTML templating
- Railway — deployment in under 5 minutes
This isn't a toy stack. FastAPI powers production systems at companies like Uber and Netflix. You're learning patterns that scale.
Prerequisites: Python 3.10+, pip, and a free Railway account.
Hour 1, Part 1 — Project Setup and Database Layer
Start by creating your project structure. Open your terminal and run:
mkdir taskmaster && cd taskmaster
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install fastapi uvicorn sqlalchemy jinja2 python-multipart
Create the following file structure:
taskmaster/
├── main.py
├── database.py
├── models.py
├── templates/
│ └── index.html
└── requirements.txt
Set up the database layer in database.py:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./tasks.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Now define your data model in models.py:
from sqlalchemy import Column, Integer, String, Boolean
from database import Base
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), nullable=False)
description = Column(String(500), default="")
completed = Column(Boolean, default=False)
The get_db() generator is a dependency injection pattern — FastAPI will handle opening and closing database sessions automatically. This small pattern prevents 90% of database connection bugs in production.
Hour 1, Part 2 — Building the FastAPI Backend
Now the heart of the application. Here's your complete main.py:
from fastapi import FastAPI, Depends, HTTPException, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import engine, get_db
import models
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="TaskMaster")
templates = Jinja2Templates(directory="templates")
# --- API Routes ---
@app.get("/api/tasks")
def get_tasks(db: Session = Depends(get_db)):
return db.query(models.Task).all()
@app.post("/api/tasks")
def create_task(title: str, description: str = "", db: Session = Depends(get_db)):
task = models.Task(title=title, description=description)
db.add(task)
db.commit()
db.refresh(task)
return task
@app.patch("/api/tasks/{task_id}/complete")
def complete_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(models.Task).filter(models.Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.completed = True
db.commit()
return {"status": "updated"}
@app.delete("/api/tasks/{task_id}")
def delete_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(models.Task).filter(models.Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
db.delete(task)
db.commit()
return {"status": "deleted"}
# --- HTML Routes ---
@app.get("/", response_class=HTMLResponse)
def read_root(request: Request, db: Session = Depends(get_db)):
tasks = db.query(models.Task).all()
return templates.TemplateResponse("index.html", {"request": request, "tasks": tasks})
@app.post("/add-task")
def add_task_form(title: str = Form(...), description: str = Form(""), db: Session = Depends(get_db)):
task = models.Task(title=title, description=description)
db.add(task)
db.commit()
return RedirectResponse(url="/", status_code=303)
Test your backend immediately. Run uvicorn main:app --reload and navigate to http://localhost:8000/docs. FastAPI auto-generates interactive documentation — test every endpoint right there. This is one of the biggest productivity wins of this framework.
Hour 2, Part 1 — The Frontend (Fast and Functional)
Create templates/index.html. No JavaScript framework needed — just clean HTML and CSS:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskMaster</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; background: #f8f9fa; }
h1 { color: #1a1a2e; }
form { background: white; padding: 20px; border-radius: 8px; margin-bottom: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
input, textarea { width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
button { background: #4f46e5; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 15px; }
button:hover { background: #4338ca; }
.task { background: white; padding: 16px 20px; border-radius: 8px; margin-bottom: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); display: flex; justify-content: space-between; align-items: center; }
.task.done { opacity: 0.5; text-decoration: line-through; }
.task-actions a { margin-left: 12px; color: #4f46e5; text-decoration: none; font-size: 13px; }
</style>
</head>
<body>
<h1>⚡ TaskMaster</h1>
<form action="/add-task" method="post">
<input type="text" name="title" placeholder="Task title" required>
<textarea name="description" placeholder="Description (optional)" rows="2"></textarea>
<button type="submit">Add Task</button>
</form>
{% for task in tasks %}
<div class="task {% if task.completed %}done{% endif %}">
<div>
<strong>{{ task.title }}</strong>
{% if task.description %}<p style="margin:4px 0 0; color:#666; font-size:14px;">{{ task.description }}</p>{% endif %}
</div>
<div class="task-actions">
{% if not task.completed %}
<a href="/api/tasks/{{ task.id }}/complete" onclick="fetch(this.href, {method:'PATCH'}); this.closest('.task').classList.add('done'); return false;">✓ Done</a>
{% endif %}
</div>
</div>
{% endfor %}
</body>
</html>
Hour 2, Part 2 — Deploy to Production
Generate your requirements.txt:
pip freeze > requirements.txt
Create a Procfile (Railway uses this to start your app):
web: uvicorn main:app --host 0.0.0.0 --port $PORT
Push to GitHub and connect your repo to Railway:
- Go to railway.app → New Project → Deploy from GitHub
- Select your repo
- Railway auto-detects Python and installs dependencies
- Your app is live in ~3 minutes
Your deployed URL is production-ready. Railway handles SSL certificates, auto-restarts on crashes, and scales automatically.
What You Built — and Where to Take It Next
In two hours, you shipped a full-stack Python application with a REST API, a database, server-rendered HTML, and cloud deployment. Not a tutorial demo — a real, accessible URL you can share right now.
The natural next steps:
- Swap SQLite for PostgreSQL (Railway provides one free — just change
DATABASE_URL) - Add user authentication with FastAPI-Users
- Replace Jinja2 templates with a React or Vue frontend consuming your existing API endpoints
The foundation you built today doesn't get thrown away as the project grows. That's the mark of a solid architecture.
Your action item: Don't just read this — run the code. Have a deployed URL in the next two hours. Then open an issue on your own repo for the first feature you want to add. Shipping beats planning, every single time.
Found this useful? Share it with a developer friend who's been sitting on a project idea. The barrier to entry is lower than they think.
Top comments (0)