# Build a URL with Url.parse and SearchParams

URL: /docs/cookbook/url_build
Markdown: /docs/cookbook/url_build.md

> Parse a base URL, set its path and query parameters, and resolve links against it.

Generated by `cargo run -p agent_docs` from `compiler/examples/`. Edit the example files, then regenerate.

Parse a base URL, set its path and query parameters, and resolve links against it.

`Url.parse` is the WHATWG URL parser behind the web's `new URL(s)`: it lowercases the
scheme and host, drops a default port, resolves `.` and `..` segments and percent-encodes
what each component cannot hold, returning `UrlParts` or a `UrlError` with its reason.
Each `with_*` function returns a new value, so the base is never changed. `Url.search`
reads the query as `SearchParams`, an ordered multimap whose `set` replaces the first pair
of a name and keeps the rest of the query; `with_search` writes it back, encoding a space as
`+` and `&`, `=` or `#` in a value as `%26`, `%3D` and `%23`. `to_url` gives the `Url` that
`Request.of` takes. `Url.resolve` is `new URL(link, base)`, the step that turns a redirect's
`Location` header or a relative `next` link into an absolute URL.

The tests check that other parameters survive, that query text cannot add a parameter, that
relative, scheme-relative and absolute links resolve, and that a path-only `Url` (valid for
`Request.url`, but not absolute) and a non-ASCII host fail with `Relative` and `Idna`. No
effects or permissions are needed: a URL value grants nothing; `net` is checked when a
request is sent.

[Source project](https://github.com/icantcodefyi/sakuralang/tree/main/compiler/examples/url_build) · harness mode: `run + test`.

From the repository root, with this checkout's `sakura` on `PATH`:

```sh
cd compiler/examples/url_build
sakura check --json
sakura run
sakura test --json
```

### Manifest and permissions [#manifest-and-permissions]

```toml
[package]
name = "url_build"
version = "0.1.0"
edition = "2026"

[permissions]
```

### Source and tests [#source-and-tests]

`src/main.sak` includes all required imports and tests.

```sakura test id=cookbook-url_build project=url_build
import std/searchparams.{SearchParams}
import std/url.{Url, UrlError, UrlReason}

/// The search endpoint under `base`'s origin, with `query` and `page` set and the base's other
/// parameters kept.
fn search_url(base: Url, query: String, page: Int) -> Url ! {Fail<UrlError>} {
  let parts = Url.parse(base)?
  let params = Url.search(parts).set("q", query).set("page", Int.to_string(page))
  parts.with_path("/v1/search")?.with_search(params).with_fragment(None).to_url()
}

/// A link from a response (a `Location` header, a `next` link) resolved against the URL that
/// was fetched.
fn follow(current: Url, link: String) -> Url ! {Fail<UrlError>} {
  Url.resolve(Url.parse(current)?, link)?.to_url()
}

pub fn main() -> Unit ! {Console, Fail<UrlError>} {
  let base = url"https://api.example.test/v0/items?key=k1&q=old#top"
  let first = search_url(base, "rust & sakura", 2)
  Console.print(first)
  Console.print(follow(first, "?q=next&page=3"))
  Console.print(follow(first, "../v2/items"))
}

test "the query is set and the other parameters are kept" {
  let u = search_url(url"https://api.example.test/v0?key=k1&q=old&page=1", "a b", 4)
  expect_eq(u, Url.from("https://api.example.test/v1/search?key=k1&q=a+b&page=4"))
  let back = Url.search(Url.parse(u)?)
  expect_eq(back.get("q"), Some("a b"))
  expect_eq(back.get_all("page"), ["4"])
}

test "text that would break the query is percent-encoded" {
  let u = search_url(url"https://x.test/", "1+1=2 & more#", 1)
  expect_eq(u, Url.from("https://x.test/v1/search?q=1%2B1%3D2+%26+more%23&page=1"))
}

test "links resolve against the fetched URL" {
  let here = url"https://api.example.test/v1/search?q=a"
  expect_eq(follow(here, "/v1/items/7"), Url.from("https://api.example.test/v1/items/7"))
  expect_eq(follow(here, "//cdn.example.test/x.png"), Url.from("https://cdn.example.test/x.png"))
  expect_eq(follow(here, "http://other.test"), Url.from("http://other.test/"))
}

test "a base without a scheme or with a non-ASCII host is a typed error" {
  expect_fail(fn() { search_url(url"/v0/items", "x", 1) }, UrlError { reason: UrlReason.Relative })
  expect_fail(
    fn() { search_url(Url.from("https://bücher.example/"), "x", 1) },
    UrlError { reason: UrlReason.Idna },
  )
}
```

### Expected stdout [#expected-stdout]

```text
https://api.example.test/v1/search?key=k1&q=rust+%26+sakura&page=2
https://api.example.test/v1/search?q=next&page=3
https://api.example.test/v2/items
```

### Compiler provenance and regeneration [#compiler-provenance-and-regeneration]

Finish only when no holes remain, independent tests pass, the requested target
builds, and contracts/tests/permissions have not been weakened. Repeat the frozen
baseline check after the final edits. Report missing prerequisites and failing
checks explicitly; do not claim a skipped test passed.

These ten projects teach supported Sakura APIs on the JavaScript backend. Each
project is exercised offline by `cargo test -p sakura_cli --test examples`;
HTTP and LLM exchanges are synthetic committed cassettes, with no network or API keys.
Use Node 26 for execution, including its built-in SQLite implementation.

The initial API baseline is compiler revision
[5cae349516c21714d90d36e87f1cfd6debe980c6](https://github.com/icantcodefyi/sakuralang/commit/5cae349516c21714d90d36e87f1cfd6debe980c6).
CI reruns these projects against the compiler in the same checkout. Build that
checkout's CLI and run `sakura --version` and `sakura prompt` before adapting a recipe.
Sakura Lang is pre-1.0: read STABILITY.md before depending on an API. An installed
skill's frontmatter records its compiler version and build revision. These teaching examples are separate from the agent
evaluation tasks.

The source of each page is its example's `recipe.md`, `sakura.toml`,
`src/main.sak` and fixture files. Run `cargo run -p agent_docs` from `compiler/`
to regenerate the cookbook and skill reference. Edit those sources, not the rendered pages.
