# Book 4 — Phoenix: The Web Layer

```elixir
Mix.install([
  {:kino, "~> 0.14"},
  {:plug, "~> 1.16"},
  {:bandit, "~> 1.5"},
  {:jason, "~> 1.4"},
  {:req, "~> 0.5"},
  {:ecto_sqlite3, "~> 0.17"}
])
```

## Setup

Companion to `book4-phoenix.html`. We won't generate a full Phoenix project inside a notebook — instead we'll build Phoenix's actual ingredients by hand: a Plug pipeline, a real HTTP server running *in this notebook*, and Ecto schemas/changesets/queries against a real (in-memory SQLite) database. After this, a generated Phoenix app will read like a familiar arrangement of parts.

> **Note:** the first run takes a minute — `Mix.install` compiles the SQLite driver.

## 1. Plug: the whole web abstraction

```elixir
# A plug is a function: conn in, conn out. Let's look at a bare conn:
conn = Plug.Test.conn(:get, "/hello?name=world")

# It's just a struct — inspect the interesting fields:
Map.take(conn, [:method, :request_path, :query_string, :status, :assigns, :halted])
```

```elixir
# Function plugs — and a pipeline is literally the |> operator:
defmodule MyPlugs do
  import Plug.Conn

  def put_request_id(conn, _opts) do
    assign(conn, :request_id, :erlang.unique_integer([:positive]))
  end

  def authenticate(conn, _opts) do
    case get_req_header(conn, "authorization") do
      ["Bearer secret-token"] -> assign(conn, :user, "ada")
      _ -> conn |> send_resp(401, "unauthorized") |> halt()
    end
  end

  def greet(conn, _opts) do
    send_resp(conn, 200, "hello #{conn.assigns.user} (req ##{conn.assigns.request_id})")
  end
end

# Run a request through the pipeline — WITH auth header:
Plug.Test.conn(:get, "/")
|> Plug.Conn.put_req_header("authorization", "Bearer secret-token")
|> MyPlugs.put_request_id([])
|> MyPlugs.authenticate([])
|> MyPlugs.greet([])
|> Map.take([:status, :resp_body])
```

```elixir
# Without the header, authenticate/2 halts — greet never runs.
# Plug.run/2 respects the halted flag (unlike a raw pipe):
Plug.Test.conn(:get, "/")
|> Plug.run([
  {&MyPlugs.put_request_id/2, []},
  {&MyPlugs.authenticate/2, []},
  {&MyPlugs.greet/2, []}
])
|> Map.take([:status, :resp_body, :halted])
```

That's Phoenix's entire middleware story. `halt/1` + immutable conn = no `next()`, no ordering bugs from mutation.

## 2. A real HTTP server, in this notebook

`Plug.Router` is a mini-Phoenix-router. Bandit is the same web server a Phoenix app uses. This is a genuinely working web service:

```elixir
defmodule NotebookAPI do
  use Plug.Router

  plug :match
  plug Plug.Parsers, parsers: [:json], json_decoder: Jason
  plug :dispatch

  get "/hello/:name" do
    send_resp(conn, 200, "Hello, #{name}! You are being served by #{inspect(self())}")
  end

  post "/echo" do
    send_resp(conn, 200, "you posted: #{inspect(conn.body_params)}")
  end

  match _ do
    send_resp(conn, 404, "not found")
  end
end

# Boot it under Bandit (kill any previous instance on re-run):
if pid = Process.whereis(:notebook_api), do: Process.exit(pid, :normal)
{:ok, server} = Bandit.start_link(plug: NotebookAPI, port: 4321)
Process.register(server, :notebook_api)

"listening on http://localhost:4321 — also try it in your browser!"
```

```elixir
# Call our own server. Run this cell a few times — the PID in the response
# CHANGES each request: process-per-request, live.
Req.get!("http://localhost:4321/hello/livebook").body
```

```elixir
Req.post!("http://localhost:4321/echo", json: %{lang: "elixir", book: 4}).body
```

```elixir
# 200 concurrent requests? 200 concurrent processes. No pool tuning, no event loop:
1..200
|> Task.async_stream(fn i -> Req.get!("http://localhost:4321/hello/user#{i}").status end,
  max_concurrency: 50
)
|> Enum.frequencies_by(fn {:ok, status} -> status end)
```

### Exercise 4.1

Add a `get "/crash"` route to a copy of `NotebookAPI` that calls `raise "boom"`, restart the server, and request it with `Req.get("http://localhost:4321/crash", retry: false)`. Then request `/hello/again` — confirm the server still works. One request's crash is one process's crash.

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
# Add inside the router:
# get "/crash" do
#   _ = conn
#   raise "boom"
# end

# You'll get a 500 (Bandit rescues and reports), and the follow-up request succeeds:
# {:ok, %{status: 500}} then %{status: 200}
```

</details>

## 3. Ecto: schemas and changesets

```elixir
defmodule Product do
  use Ecto.Schema
  import Ecto.Changeset

  schema "products" do
    field :name, :string
    field :price, :decimal
    field :stock, :integer, default: 0
    timestamps()
  end

  def changeset(product, attrs) do
    product
    |> cast(attrs, [:name, :price, :stock])
    |> validate_required([:name, :price])
    |> validate_number(:price, greater_than: 0)
    |> validate_length(:name, min: 2)
    |> unique_constraint(:name)
  end
end

# Changesets work with NO database — they're pure validation pipelines.
# Untrusted, stringly-typed input (like real form/JSON params):
good = Product.changeset(%Product{}, %{"name" => "Keyboard", "price" => "49.99"})
bad = Product.changeset(%Product{}, %{"name" => "K", "price" => "-5"})

{good.valid?, bad.valid?, bad.errors}
```

```elixir
# A changeset is a diff, not an object: it tracks exactly what changed.
existing = %Product{name: "Keyboard", price: Decimal.new("49.99"), stock: 10}

Product.changeset(existing, %{"stock" => "8", "price" => "49.99"}).changes
# price didn't actually change → not in changes. Minimal UPDATEs for free.
```

## 4. A real Repo (in-memory SQLite)

```elixir
defmodule Repo do
  use Ecto.Repo, otp_app: :notebook, adapter: Ecto.Adapters.SQLite3
end

# The Repo is a supervised process tree (Book 3!) — start it and migrate:
case Repo.start_link(database: ":memory:", pool_size: 1) do
  {:ok, _pid} -> :started
  {:error, {:already_started, _pid}} -> :already_running
end

Ecto.Adapters.SQL.query!(Repo, """
CREATE TABLE IF NOT EXISTS products (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT, price DECIMAL, stock INTEGER DEFAULT 0,
  inserted_at TEXT, updated_at TEXT
)
""")

:ok
```

```elixir
# Insert through changesets — invalid data can't reach the DB:
{:ok, kb} = Repo.insert(Product.changeset(%Product{}, %{"name" => "Keyboard", "price" => "49.99", "stock" => "10"}))
{:ok, _} = Repo.insert(Product.changeset(%Product{}, %{"name" => "Mouse", "price" => "19.99", "stock" => "50"}))
{:ok, _} = Repo.insert(Product.changeset(%Product{}, %{"name" => "Monitor", "price" => "199.00", "stock" => "3"}))

{:error, changeset} = Repo.insert(Product.changeset(%Product{}, %{"name" => "Bad", "price" => "-1"}))

{kb.id, changeset.errors}
```

```elixir
# Composable queries — build them up like data, run them explicitly:
import Ecto.Query

cheap = from p in Product, where: p.price < 100, order_by: [asc: p.price]

# refine the query further (it's just a value):
cheap_in_stock = from p in cheap, where: p.stock > 0, select: {p.name, p.price}

Repo.all(cheap_in_stock)
```

```elixir
# Update = read, changeset, write. Explicit, no auto-save:
kb
|> Product.changeset(%{"stock" => "9"})
|> Repo.update()
```

### Exercise 4.2 — the context layer

Write a `Shop` module (context) with `list_products/0`, `create_product/1` (attrs → `{:ok, p} | {:error, cs}`), and `sell/2` (`name`, `qty`) that decrements stock but returns `{:error, :insufficient_stock}` if stock would go negative. The web layer of Book 5 will call functions shaped exactly like these.

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
defmodule Shop do
  import Ecto.Query

  def list_products, do: Repo.all(from p in Product, order_by: p.name)

  def create_product(attrs) do
    %Product{} |> Product.changeset(attrs) |> Repo.insert()
  end

  def sell(name, qty) do
    product = Repo.one(from p in Product, where: p.name == ^name)

    cond do
      product == nil -> {:error, :not_found}
      product.stock < qty -> {:error, :insufficient_stock}
      true -> product |> Product.changeset(%{stock: product.stock - qty}) |> Repo.update()
    end
  end
end

Shop.sell("Monitor", 2) |> IO.inspect()
Shop.sell("Monitor", 99)
```

(Real code would wrap `sell/2` in a transaction or use `Repo.update_all` with a `where stock >= ^qty` guard to avoid races — worth trying!)

</details>

## 5. The full picture

You've now hand-built every layer of a Phoenix request from parts:

```
Bandit (acceptor pool: 1 process per request)
  → Plug pipeline (parse, auth, halt) — Endpoint
  → Plug.Router match — Router
  → your handler function — Controller
  → Shop.* functions — Context
  → changesets + Repo — Ecto
  → send_resp — View/render
```

A generated Phoenix app (`mix phx.new`) adds HEEx templates, code organization, PubSub wiring, and telemetry around this exact skeleton. Generate one after this book — you'll recognize every file.

**Next:** `book5-liveview.livemd` — keep the process alive after render, and the page becomes a stateful UI.
