# Routing & Views

In Phase 1 you saw the headline move: `@app.route` maps a URL to a function, and whatever it returns becomes
the page. This phase puts meat on that skeleton - how you handle `/notes/7` when the `7` changes every
request, how the *same* URL does one thing on a `GET` and another on a `POST`, where submitted data lives,
and what your options are for what to hand back.

The mental model: **a Flask view is a function that receives a request and returns a response.** That's the
entire job. The decorator decides *which* requests reach your function; inside, you read what you need off
the request, do some work, and return something Flask can turn into an HTTP response.

## Dynamic URLs - capturing part of the path

A route like `/notes` lists every note. But `/notes/7` should show *one* note - the one with id `7`. That `7`
changes per request, so you can't bake it into the route string. Mark it as a **variable segment** with angle
brackets, then receive it as an argument to the view.

```python
@app.route("/notes/<int:note_id>")
def note_detail(note_id):
    return f"You asked for note #{note_id}"
```

*What just happened:* `<int:note_id>` is a placeholder. Flask matches `/notes/7`, pulls out the `7`, and
passes it to `note_detail` as the `note_id` argument - the name after the colon must match the parameter
exactly. `int:` is a **converter**: it tells Flask "this segment is an integer," so `note_id` arrives as the
number `7`, not the string `"7"`, and `/notes/banana` doesn't match this route at all (a 404 instead of
garbage).

The built-in converters you'll reach for:

| Converter | Matches | Example |
|-----------|---------|---------|
| `<string:x>` | any text without a slash (**the default**) | `/notes/<title>` |
| `<int:x>` | a whole number, given to you as an `int` | `/notes/<int:note_id>` |
| `<path:x>` | text *including* slashes | `/files/<path:subpath>` |

⚠️ If you write `<note_id>` with **no converter**, you get `string` by default - a classic source of "why is
my id a string?" bugs. When a segment is a numeric id, say so: `<int:note_id>`.

## HTTP methods - same URL, different verbs

By default a route only answers `GET` requests (the verb a browser uses to *fetch* a page). But creating a
note is a `POST` - the verb for *submitting* data. The REST idea that the URL names the resource and the
method is the verb you apply to it (see [What a Framework Even Is](/guides/what-a-framework-even-is)) shows
up here directly: `/notes` is the collection, and one route can handle both "show me the notes" (`GET`) and
"add a note" (`POST`).

```python
from flask import request

notes = []  # stand-in storage until Phase 5 brings a real database

@app.route("/notes", methods=["GET", "POST"])
def notes_collection():
    if request.method == "POST":
        notes.append(request.form["title"])
        return f"Added a note. Now have {len(notes)}.", 201
    return "Your notes: " + ", ".join(notes)
```

*What just happened:* `methods=["GET", "POST"]` tells Flask this view answers *both* verbs - without it, a
`POST` to `/notes` would be rejected with a `405 Method Not Allowed`. `request.method` tells you *which* verb
you got, so you branch: `POST` reads the submitted title and creates a note; `GET` lists what's there. This
**create-on-POST, list-on-GET** pattern on a single collection URL is the bread and butter of web apps.
(`notes = []` is throwaway in-memory storage so the example runs - real persistence arrives in Phase 5.)

💡 The `201` in `return ..., 201` is the HTTP status code for "Created" - more on status codes in a moment.

## The request object - where the incoming data lives

📝 **`flask.request`** is how your view reads everything about the incoming request - a single object Flask
hands you (technically per-request, but you import it once and use it anywhere inside a view). The parts
you'll use constantly:

| Attribute | Holds | From |
|-----------|-------|------|
| `request.args` | query-string values | `?q=...&sort=...` |
| `request.form` | submitted form fields | an HTML form's `POST` body |
| `request.json` | a parsed JSON body | an API client's `POST` |
| `request.method` | the HTTP verb | `GET`, `POST`, ... |
| `request.headers` | request headers | `User-Agent`, `Authorization`, ... |

Reading a **query parameter** (the part after `?` in the URL) - handy for search and filtering:

```python
@app.route("/search")
def search():
    term = request.args.get("q", "")
    matches = [n for n in notes if term.lower() in n.lower()]
    return f"Notes matching '{term}': {matches}"
```

*What just happened:* a request to `/search?q=milk` lands here, and `request.args.get("q", "")` pulls `milk`
out of the query string. Using `.get("q", "")` instead of `request.args["q"]` means a missing `?q=` gives you
the empty-string default rather than a `400 Bad Request` - the forgiving way to read optional values.

And reading a **form field** from a submitted body:

```python
@app.route("/notes/new", methods=["POST"])
def create_note():
    title = request.form.get("title", "Untitled")
    notes.append(title)
    return f"Created: {title}", 201
```

*What just happened:* when an HTML form `POST`s to `/notes/new`, the field named `title` shows up in
`request.form`, read here with a fallback. ⚠️ `request.args` and `request.form` are *different* buckets:
`args` is the query string, `form` is the request body - reading from the wrong one is a common "why is this
empty?" moment. We'll build and validate real HTML forms in [Forms & Request Data](04-forms-and-request-data.md).

## Returning responses - your options for what to hand back

A view's return value becomes the HTTP response, and Flask is flexible about what it accepts:

```python
from flask import jsonify, redirect, url_for, abort

@app.route("/notes/<int:note_id>")
def note_detail(note_id):
    if note_id >= len(notes):
        abort(404)                              # 1. bail out with an error page
    return f"<h1>{notes[note_id]}</h1>"         # 2. a string → HTML body

@app.route("/api/notes")
def api_notes():
    return jsonify(notes)                       # 3. Python data → JSON response

@app.route("/notes/<int:note_id>/delete", methods=["POST"])
def delete_note(note_id):
    del notes[note_id]
    return redirect(url_for("api_notes"))       # 4. send the browser elsewhere
```

*What just happened:* four ways to respond, all from plain `return`:

1. **`abort(404)`** stops the view immediately and makes Flask return its standard `404 Not Found` page (any
   status works - `abort(403)`, etc.) - the clean way to say "this doesn't exist."
2. **A string** becomes the response body, served as HTML - exactly what you saw in Phase 1.
3. **`jsonify(notes)`** turns Python data (a list, a dict) into a proper JSON response with the right
   `Content-Type` header - the seed of the JSON API in [Building a JSON API](08-building-a-json-api.md).
4. **`redirect(url_for("api_notes"))`** sends the browser to another URL - the standard move after a
   successful `POST` so a refresh doesn't re-submit.

You can also return a **tuple** to set the status code: `return "Created", 201`. Body first, status second.

📝 Notice `url_for("api_notes")` in that redirect. **`url_for` builds a URL from the *view function's name*,
not a hardcoded path.** Pass it `"api_notes"` and Flask returns `/api/notes`. For routes with variable
segments, pass the values as keyword arguments: `url_for("note_detail", note_id=7)` gives you `/notes/7`.

⚠️ Don't hardcode URLs like `redirect("/api/notes")`. Change the route string to `/v2/notes` and every
hardcoded path silently breaks, while `url_for("api_notes")` keeps working because it's tied to the function,
not the path text.

## The flow - and why views stay thin

💡 The whole cycle never changes: **Flask matches the incoming URL to a view function, hands that function the
request, and sends back whatever the function returns.** Match → call → respond - the same request/response
loop every framework runs, laid bare here (the point made in
[What a Framework Even Is](/guides/what-a-framework-even-is)). Routing, the request object, and response
helpers are the three sides of that one loop.

Because the view *is* that seam, ⚠️ **keep it thin.** A good view does three things and stops: parse what it
needs from the request, call a function that does the actual work, and return a response. The business
logic - how a note is validated, how it's saved, how search actually ranks - belongs in plain functions or
modules you call *from* the view, not stuffed inside it. Database access especially lives elsewhere (that's
[Working with a Database](05-database-with-sqlalchemy.md)).

```python
# thin view: parse → delegate → respond
@app.route("/notes/new", methods=["POST"])
def create_note():
    title = request.form.get("title", "")
    note = add_note(title)                 # the real work lives in add_note()
    return redirect(url_for("note_detail", note_id=note.id))
```

*What just happened:* the view reads one field, calls `add_note` (a regular function that owns the logic),
and redirects - readable, and `add_note` stays testable on its own without faking a web request. Right now
your views return raw HTML strings, which gets ugly fast - next phase we hand that job to **templates**.

## Recap

1. **Dynamic URLs** use angle brackets: `<int:note_id>` captures a path segment and passes it to the view.
   Converters (`int`, `string`, `path`) type and constrain the match - ⚠️ no converter means `string` by
   default, so numeric ids arrive as text unless you write `<int:...>`.
2. **HTTP methods** are declared with `methods=["GET", "POST"]`; branch on `request.method` to do "list on
   GET, create on POST" from a single collection URL.
3. **`flask.request`** carries the incoming data: `request.args` (query string), `request.form` (form body),
   `request.json`, `request.headers`, `request.method`. Use `.get(key, default)` for optional values.
4. **Responses** come from `return`: a string (HTML), a `(body, status)` tuple, `jsonify(...)` for JSON,
   `redirect(url_for(...))` to send the browser elsewhere, or `abort(404)` to bail out with an error.
5. 📝 **`url_for("view_name")`** builds URLs from the view function's name - ⚠️ never hardcode paths, or
   they break the moment a route changes.
6. 💡 The whole framework core is one loop: **match the URL → call the view with the request → return a
   response.** Keep views thin - parse, delegate to real functions, respond.

## Quick check

Make sure the request → response cycle stuck:

```quiz
[
  {
    "q": "You write `@app.route(\"/notes/<note_id>\")` (no converter) and request `/notes/7`. What is `note_id` inside the view?",
    "choices": [
      "The string \"7\", because with no converter the default is `string`",
      "The integer 7, because Flask detects it's numeric",
      "A 404 error, because the route doesn't match",
      "None, because you forgot the `int:` converter"
    ],
    "answer": 0,
    "explain": "With no converter, Flask uses `string` by default, so `note_id` is the text \"7\". Write `<int:note_id>` to receive a real integer."
  },
  {
    "q": "A user submits an HTML form with a field named `title` via POST. Where do you read it?",
    "choices": [
      "`request.form[\"title\"]` (or `.get`) - form fields live in the request body",
      "`request.args[\"title\"]` - all submitted values live in args",
      "`request.headers[\"title\"]`",
      "`request.method[\"title\"]`"
    ],
    "answer": 0,
    "explain": "Submitted form fields live in `request.form` (the request body). `request.args` is the URL query string (`?q=...`) - a different bucket."
  },
  {
    "q": "Why is `redirect(url_for(\"api_notes\"))` better than `redirect(\"/api/notes\")`?",
    "choices": [
      "`url_for` builds the URL from the view function's name, so it keeps working if you change the route's path string",
      "`url_for` is faster because it skips URL parsing",
      "Hardcoded paths cause a 404 in production but work in development",
      "There's no difference; both are equally fine"
    ],
    "answer": 0,
    "explain": "`url_for` ties the URL to the view function, not the literal path. Change the route to `/v2/notes` and `url_for` still resolves correctly, while a hardcoded `/api/notes` silently breaks."
  }
]
```
