Building Web Apps & APIs

Expert · 20 min read · ▶ live playground · ✦ checkpoint

In lesson 23.2, your Python code acted like a careful web client. Server-side code is Python running behind a URL, receiving requests from clients and returning responses on purpose.

There is no live server in this browser lesson. The runnable examples simulate routing and validation with the standard library; examples that need installed packages, ports, or real project setup are static.

Requests become handler calls

A web framework is a library that handles the HTTP machinery around your code, so you can write the application behavior. A route is a method-and-path rule such as GET /books, and a route handler is the Python function the framework calls when a request matches that rule.

Think of the framework as a front desk. The request arrives, the desk checks the route card, sends the request to the right worker, and wraps the worker's answer as an HTTP response.

from dataclasses import dataclass
import json
 
 
@dataclass
class Request:
    method: str
    path: str
    body: str = ""
 
 
def get_books(request):
    body = json.dumps({"items": ["Practical Python", "Clean APIs"]})
    return 200, {"content-type": "application/json"}, body
 
 
def create_book(request):
    payload = json.loads(request.body or "{}")
    title = payload.get("title", "").strip()
    if not title:
        body = json.dumps({"error": "title is required"})
        return 422, {"content-type": "application/json"}, body
 
    body = json.dumps({"created": title})
    return 201, {"content-type": "application/json"}, body
 
 
routes = {
    ("GET", "/books"): get_books,
    ("POST", "/books"): create_book,
}
 
 
def handle(request):
    handler = routes.get((request.method, request.path))
    if handler is None:
        body = json.dumps({"error": "not found"})
        return 404, {"content-type": "application/json"}, body
    return handler(request)
 
 
requests = [
    Request("GET", "/books"),
    Request("POST", "/books", '{"title": "Clean APIs"}'),
    Request("GET", "/missing"),
]
 
for request in requests:
    status, headers, body = handle(request)
    payload = json.loads(body)
    print(f"{request.method} {request.path} -> {status} {payload}")

GET /books -> 200 {'items': ['Practical Python', 'Clean APIs']}
POST /books -> 201 {'created': 'Clean APIs'}
GET /missing -> 404 {'error': 'not found'}

That is the server-side skeleton: match a route, run a handler, return a status code, headers, and body. Real frameworks add URL parsing, error handling, documentation, security hooks, and deployment integration around that core idea.

Flask, Django, and FastAPI

Flask is a Python microframework, meaning it gives you a small web core and lets you choose many extras yourself. It is popular for small web apps, APIs, prototypes, and projects where you want a thin layer around your own code.

Django is a batteries-included framework, meaning it ships with many built-in web app parts: routing, templates, forms, admin screens, authentication tools, and an ORM. It is popular when you want a full application structure from the start.

FastAPI is a modern typed API framework, meaning it uses Python type hints to help define, validate, and document JSON APIs. It is popular for REST APIs where clear request and response shapes matter.

The choice is not "which one is best?" It is "which shape fits this project?" Flask is a small workbench, Django is a stocked workshop, and FastAPI is an API desk with labeled forms.

FastAPI's API shape

A request model is a declared shape for data the client sends. A response model is a declared shape for data your API promises to return. Pydantic is a validation library that turns typed model classes into runtime checks, and validation is checking incoming data before your handler trusts it.

This is the FastAPI shape only; it is not run here:

Static shape only - not run in this browser lesson:
 
from fastapi import FastAPI
from pydantic import BaseModel, Field
 
app = FastAPI()
 
class BookIn(BaseModel):
    title: str = Field(min_length=1)
    pages: int = Field(gt=0)
 
class BookOut(BaseModel):
    id: int
    title: str
    pages: int
 
@app.post("/books", response_model=BookOut)
def create_book(book: BookIn):
    saved_id = save_book_to_database(book.title, book.pages)
    return {"id": saved_id, "title": book.title, "pages": book.pages}

The useful idea is not the decorator syntax yet. The useful idea is the contract: clients send a JSON body shaped like BookIn, and the API responds with JSON shaped like BookOut.

Try it - validate a request body

This playground imitates the validation idea without FastAPI or Pydantic. Change the example bodies and watch which requests become 201 responses and which become 422 validation errors.

python — playgroundlive
⌘/Ctrl + Enter to run

Framework validation does not make your business rules disappear. It gives your route handler cleaner inputs and gives clients clearer error responses when the request body has the wrong shape.

Flask and Jinja page shape

An HTML page route is a route handler that returns browser-readable HTML instead of JSON. A template is a text file with placeholders for dynamic values, and Jinja is Flask's common template engine for filling those placeholders.

This is a Flask/Jinja shape only; it is not run here:

Static shape only - not run in this browser lesson:
 
# app.py
from flask import Flask, render_template
 
app = Flask(__name__)
 
@app.get("/members/<name>")
def member_profile(name):
    books = load_books_for_member(name)
    return render_template("profile.html", name=name, books=books)
 
# templates/profile.html
<h1>{{ name }}</h1>
<ul>
  {% for book in books %}
    <li>{{ book.title }}</li>
  {% endfor %}
</ul>

APIs usually return JSON for programs. Web app pages usually return HTML for humans. Many real products do both: HTML for browser screens, JSON for mobile apps and integrations.

Databases and authentication

A per-request database connection is a database conversation opened or borrowed for one incoming request and closed or returned when the response is finished. This keeps requests from sharing messy hidden state.

Database driver and ORM details depend on the project, so this shape is static:

Static shape only - not run in this browser lesson:
 
def handle_request(request):
    db = open_database_connection()
    try:
        book = db.fetch_one(
            "SELECT id, title FROM books WHERE id = ?",
            [request.path_params["book_id"]],
        )
        if book is None:
            return json_response({"error": "not found"}, status=404)
        return json_response({"id": book.id, "title": book.title})
    finally:
        db.close()

Authentication is proving who the user or client is, and authorization is deciding what that identity may do. For browser apps, a session cookie is a small browser-stored value that points to a server-side login session. For APIs, a bearer token is a secret credential sent in an Authorization header, often by scripts, mobile apps, or other services.

Password hashing is storing a one-way password fingerprint instead of the raw password, the original readable password the user typed. Never store raw passwords. A real app uses a proven password hashing tool, stores the hash, and verifies later login attempts against that hash.

Static shape only - not run in this browser lesson:
 
# Signup
password_hash = password_hasher.hash(form.password)
save_user(email=form.email, password_hash=password_hash)
 
# Login
user = find_user_by_email(form.email)
if user and password_hasher.verify(form.password, user.password_hash):
    create_session_cookie(user.id)      # browser page login
    issue_bearer_token(user.id)         # API client login

Those auth pieces are security-sensitive. The beginner rule is firm: use framework and library defaults where possible, keep secrets out of source code, and never invent your own password storage scheme.

Checkpoint

Answer all three to mark this lesson complete

You now have the server-side map: routes call handlers, handlers validate input, frameworks shape responses, and real apps connect to databases and auth systems carefully. Next, deployment means putting that app where other machines can reach it, with production servers, environment config, containers, and cloud hosting.

+50 XP on completion