Skip to content

Basic Web Server (Flask)

A web server is a program that listens for HTTP requests and responds with content — HTML pages, JSON data, images, anything a browser or API client asks for. In this project you will build a minimal web server with Flask, Python’s most popular micro-framework. The starting point is a single route that returns “Hello, World!” and the finish line is a multi-page application with dynamic URLs, query strings, JSON endpoints, and HTML templates.

You will learn:

  • What a web server actually does and how HTTP works at a conceptual level.
  • How Flask maps URLs to Python functions (routing).
  • How to read data from the URL (path variables and query strings).
  • How to return HTML, JSON, and template-rendered responses.
  • How to run a server in development mode and what to do before deploying it.

When you type https://example.com/about into your browser, this happens:

  1. Your browser opens a TCP connection to the server at example.com on port 443 (HTTPS) or 80 (HTTP).
  2. It sends a textual HTTP request like GET /about HTTP/1.1.
  3. The server reads the request, decides what to do, and sends an HTTP response consisting of a status code (200, 404, 500, …), some headers, and a body (HTML, JSON, etc.).
  4. The browser parses the body and renders it.

A web framework like Flask hides the TCP/HTTP plumbing so you write only the part where you decide what to send back.

  • Python 3.6 or above.
  • A text editor or IDE.
  • Familiarity with the command line.
  • Basic understanding of how to open a URL in a browser. (You do not need to know HTML yet.)

Flask is not in the standard library; you install it with pip:

install
pip install flask

Verify it installed:

verify
python -c "import flask; print(flask.__version__)"

You should see something like 3.0.0.

💡 Best practice: create a virtual environment first so each project gets its own copies of dependencies. On Windows: python -m venv venv && venv\Scripts\activate. On macOS/Linux: python -m venv venv && source venv/bin/activate.

  1. Create a folder named basic-web-server.
  2. Inside it, create basicwebserver.py.
  3. Open the folder in your editor.
Basic Web Server pch.viewSource
Basic Web Server
# Basic Web Server (Flask)

import sys
from flask import Flask # pip install flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello, World!"

def smoke_test():
    """Exercise every GET route once, without starting a server.

    `app.test_client()` dispatches a real request through the real application
    object -- no socket, no port, no waiting. A web project that cannot be
    driven this way cannot be tested either, so this is worth having whether or
    not anything is capturing the output.
    """
    print("smoke test: dispatching one request per route\n")
    with app.test_client() as client:
        rules = sorted(app.url_map.iter_rules(), key=lambda rule: str(rule))
        checked = 0
        for rule in rules:
            if "GET" not in rule.methods or rule.arguments:
                continue
            response = client.get(str(rule))
            body = response.get_data(as_text=True)
            body = " ".join(body.split())[:60]
            print(f"  GET {str(rule):26} {response.status_code}  {body}")
            checked += 1
    print(f"\n{checked} route(s) answered. Pass --serve to start the real "
          f"server instead.")


if __name__ == "__main__":
    # Serving is opt-in, because a run that never returns cannot be
    # tested or captured. With no arguments the file answers every
    # route once and exits; `--serve` starts the real server.
    if "--serve" in sys.argv:
        app.run(debug=True)
    else:
        smoke_test()
run
python basicwebserver.py

You will see output similar to:

text
 * Serving Flask app 'basicwebserver'
 * Debug mode: on
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

Open http://127.0.0.1:5000 (or http://localhost:5000) in your browser. You should see Hello, World!.

Stop the server with Ctrl+C when you are done.

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid

Running the file exactly as it ships takes 0.5 s and prints:

python basicwebserver.py
smoke test: dispatching one request per route
 
  GET /                          200  Hello, World!
 
1 route(s) answered. Pass --serve to start the real server instead.
basicwebserver.py
from flask import Flask
app = Flask(__name__)
  • Flask is the framework’s main class.
  • __name__ tells Flask where to look for templates and static files relative to the current module.
  • app is the application object. Routes, configuration, and extensions all attach to it.
basicwebserver.py
@app.route("/")
def index():
    return "Hello, World!"
  • @app.route("/") is a decorator — it tells Flask “when a request arrives for /, call the function below this line.”
  • index is just a Python function name. Flask does not care what you call it, only that it is unique.
  • The return value becomes the response body. Returning a string sends back HTML with status 200.
basicwebserver.py
if __name__ == "__main__":
    app.run(debug=True)
  • The if __name__ == "__main__": guard ensures the server only starts when you run this file directly — not when another module imports it.
  • debug=True turns on Flask’s debugger and auto-reloader, so saving a code change restarts the server automatically. Never leave debug on in production — it exposes a remote code execution surface.

A real site needs more than one page. Routes can be plain strings, dynamic patterns, or restricted to specific HTTP methods.

more_routes.py
@app.route("/about")
def about():
    return "<h1>About Page</h1><p>This is a basic Flask server.</p>"
 
@app.route("/api/health")
def health():
    return {"status": "ok"}, 200       # dict → JSON automatically
 
@app.route("/user/<username>")
def user_profile(username):
    return f"<h1>Profile of {username}</h1>"
 
@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"Showing post #{post_id} (type: {type(post_id).__name__})"
  • <username> is a path variable — anything in that position is captured and passed to the function.
  • <int:post_id> adds a converter that rejects non-integer values with a 404 automatically.
  • Returning a dictionary makes Flask serialize it to JSON with Content-Type: application/json.

A URL like /search?q=python&page=2 carries query parameters. Use request.args:

query.py
from flask import request
 
@app.route("/search")
def search():
    q = request.args.get("q", "")
    page = int(request.args.get("page", "1"))
    return f"Searching for '{q}' on page {page}"

request.args.get(key, default) returns the value if present, otherwise the default. This avoids KeyError when the user omits a parameter.

Embedding HTML in Python strings gets ugly fast. Use templates instead.

  1. Create a templates/ folder next to basicwebserver.py.
  2. Inside it, create index.html:
    templates/index.html
    <!doctype html>
    <html>
      <head><title>{{ title }}</title></head>
      <body>
        <h1>Hello, {{ name }}!</h1>
        <ul>
          {% for item in items %}
            <li>{{ item }}</li>
          {% endfor %}
        </ul>
      </body>
    </html>
  3. Render it from Python:
    render.py
    from flask import render_template
     
    @app.route("/greet/<name>")
    def greet(name):
        return render_template(
            "index.html",
            title="Greeting",
            name=name,
            items=["Apples", "Bananas", "Cherries"],
        )

The {{ ... }} is variable substitution, {% ... %} is control flow. Jinja2 (Flask’s template engine) auto-escapes variables to prevent XSS — never undo that without a very good reason.

To accept form submissions, declare the methods explicitly:

form.py
from flask import request, redirect, url_for
 
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        return redirect(url_for("user_profile", username=username))
    return '''
      <form method="post">
        <input name="username" required>
        <button type="submit">Log in</button>
      </form>
    '''
  • request.form reads application/x-www-form-urlencoded bodies (standard HTML forms).
  • request.get_json() reads JSON bodies (typical for API clients).
  • redirect(url_for("name")) sends the browser to another route by function name, not URL — safer when URLs change.
ProblemCauseFix
ModuleNotFoundError: No module named 'flask'Flask not installed in the active interpreterpip install flask inside the right virtualenv
Server “not responding”You stopped it, or the port is already in useCheck the terminal; use app.run(port=5001) to switch ports
Changes do not show upBrowser cached, or debug mode offHard refresh (Ctrl+Shift+R) or set debug=True
404 on /aboutRoute defined but server not restarted in non-debug modeRestart the script, or use debug=True
Static files (CSS, images) not loadingThey must live under static/ and be referenced with url_for('static', filename='style.css')Move them and use url_for

Once your server actually runs, the security checklist begins:

  • Never run debug=True in production. It exposes a Python shell to anyone who can reach the server.
  • Validate every piece of input — path variables, query strings, form bodies, headers.
  • Use HTTPS in production. Front Flask with Nginx or a managed platform that terminates TLS.
  • Set a real SECRET_KEY before using sessions or CSRF: app.config["SECRET_KEY"] = os.environ["FLASK_SECRET"].
  • Do not trust the client. A path that opens a file based on user input is one of the easiest ways to write a directory-traversal bug.
  • JSON APIs for mobile apps, single-page apps, IoT devices.
  • Internal dashboards for monitoring or admin tools.
  • Form back-ends for static sites that need a contact endpoint.
  • Webhook receivers for GitHub, Stripe, Slack, etc.
  • Prototyping machine-learning models behind an HTTP endpoint.

The built-in development server is single-threaded and not designed for real traffic. For production use a WSGI server:

prod
pip install gunicorn
gunicorn basicwebserver:app --workers 4 --bind 0.0.0.0:8000

Or run inside a Docker container behind Nginx. PaaS options like Render, Railway, Fly.io, or PythonAnywhere abstract the details if you just want to deploy.

todo_api.py
todos = []
 
@app.route("/todos", methods=["GET", "POST"])
def todos_route():
    if request.method == "POST":
        todos.append(request.get_json())
        return {"ok": True}, 201
    return {"items": todos}

Put files in static/, point a route at index.html, style with CSS.

Use SQLAlchemy (pip install flask-sqlalchemy) to persist data instead of an in-memory list.

Use Flask-Login for sessions and Werkzeug for password hashing.

Pair Flask with a Markdown library to render .md files as HTML pages — basis for a simple blog.

This project teaches:

  • HTTP fundamentals — verbs, status codes, request/response.
  • Routing — URLs as a public API to your code.
  • Templating — separating presentation from logic.
  • Server lifecycle — start, listen, serve, shut down.
  • The dev/prod distinction — debug servers vs. WSGI servers.
  • Add a /about and /contact page to practice multi-route apps.
  • Convert your routes into a REST API that stores data in a SQLite database.
  • Read about Flask Blueprints for organizing larger applications.
  • Compare Flask with FastAPI — modern, typed, async-friendly alternative.
  • Deploy your server to a free host (Render, Fly.io, PythonAnywhere).

In about ten lines you built a real web server, and in another twenty you turned it into a multi-route application with templates and JSON endpoints. Every full-stack Python developer started here. Flask’s strength is exactly this — it gets out of the way and lets you focus on what your endpoints actually do. Find the full source on GitHub and explore more web-development projects on Python Central Hub.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading