DEV Community

Taumai flores
Taumai flores

Posted on

From Zero to Deployed: Building a Full-Stack App with Python in 2 Hours

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
Enter fullscreen mode Exit fullscreen mode

Create the following file structure:

taskmaster/
├── main.py
├── database.py
├── models.py
├── templates/
│   └── index.html
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Hour 2, Part 2 — Deploy to Production

Generate your requirements.txt:

pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Create a Procfile (Railway uses this to start your app):

web: uvicorn main:app --host 0.0.0.0 --port $PORT
Enter fullscreen mode Exit fullscreen mode

Push to GitHub and connect your repo to Railway:

  1. Go to railway.appNew ProjectDeploy from GitHub
  2. Select your repo
  3. Railway auto-detects Python and installs dependencies
  4. 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)