---
title: "Quickstart"
description: "Define the routes of a CLI, parse argv, and dispatch on a typed intent"
canonical: https://bomb.sh/docs/router/basics/quickstart/
---

# Quickstart

This page builds a small version of the GitHub CLI, `gh`, with a `pr list` command and a `pr create` command. You define every way into the program, parse `argv` into a typed intent, and dispatch on it.

## Define the routes

`route()` declares an address. `command()` is an address that can also execute. Definitions are immutable composition pipelines, not handler registrations:

```ts
import {
  command,
  description,
  name,
  option,
  route,
  schema,
  toggle,
  version,
} from "@bomb.sh/router";
import * as z from "zod";

const states = z.enum(["open", "closed", "merged", "all"]);

const app = command(
  name("gh"),
  description("Work seamlessly with GitHub from the command line."),
  version("2.62.0"),
  route(
    name("pr"),
    command(
      name("list"),
      option(name("state"), schema(states.default("open"))),
      option(name("limit"), schema(z.number().default(30))),
    ),
    command(
      name("create"),
      option(name("title"), schema(z.string())),
      toggle(name("draft")),
    ),
  ),
);
```

That definition makes these entry points reachable, and no others:

```text
HELP /              VERSION /          EXECUTE /
HELP /pr
HELP /pr/list                          EXECUTE /pr/list
HELP /pr/create                        EXECUTE /pr/create
```

Every route supports help. Version exists only on the root, because only the root calls `version()`. `pr` is a `route()`, not a `command()`, so it has no execute method.

The root name identifies the executable and is not repeated in route IDs. `gh pr list` selects `/pr/list`, not `/gh/pr/list`.

## Route an intent

`parse()` returns a discriminated union of every reachable entry point. Check `ok`, then switch on `method` and `route`. The dispatch stays flat even when the route tree is deep:

```ts
import {
  command,
  description,
  name,
  option,
  route,
  schema,
  toggle,
  version,
} from "@bomb.sh/router";
import * as z from "zod";

const states = z.enum(["open", "closed", "merged", "all"]);

const app = command(
  name("gh"),
  description("Work seamlessly with GitHub from the command line."),
  version("2.62.0"),
  route(
    name("pr"),
    command(
      name("list"),
      option(name("state"), schema(states.default("open"))),
      option(name("limit"), schema(z.number().default(30))),
    ),
    command(
      name("create"),
      option(name("title"), schema(z.string())),
      toggle(name("draft")),
    ),
  ),
);
// ---cut---
import process from "node:process";
import {
  parse,
  printErrors,
  printHelp,
  printVersion,
} from "@bomb.sh/router";

const result = parse(app, { argv: process.argv.slice(2) });

if (!result.ok) {
  console.error(printErrors(result));
  process.exit(1);
}

switch (result.method) {
  case "help":
    console.log(printHelp(result));
    break;

  case "version":
    console.log(printVersion(result));
    break;

  case "execute":
    switch (result.route) {
      case "/pr/list":
        result.model.state; // "open" | "closed" | "merged" | "all"
        result.model.limit; // number
        break;

      case "/pr/create":
        result.model.title; // string
        result.model.draft; // boolean
        break;
    }
}
```

Each branch sees only its own model. Hover any identifier to see the type TypeScript infers.

## Read each route's model

An execute intent carries two views of configuration:

* `model` holds the parameters owned by the selected route.
* `models` holds the model for every route along the selected path, keyed by route ID. Sibling routes are absent from both the value and its type.

For `gh pr create --title "Fix typo" --draft`, `model` is `{ title: "Fix typo", draft: true }`, and `models` is `{ "/": {}, "/pr": {}, "/pr/create": { title: "Fix typo", draft: true } }`.

## What each invocation produces

| Invocation                                | Result                                                                |
| ----------------------------------------- | --------------------------------------------------------------------- |
| `gh pr list`                              | `EXECUTE /pr/list` with `{ state: "open", limit: 30 }`                |
| `gh pr list --state merged --limit 5`     | `EXECUTE /pr/list` with `{ state: "merged", limit: 5 }`               |
| `gh pr create --title "Fix typo" --draft` | `EXECUTE /pr/create` with `{ title: "Fix typo", draft: true }`        |
| `gh --help pr list`                       | `HELP /pr/list`. Help and version target the deepest selected route.  |
| `gh pr`                                   | `method-not-allowed`, because `/pr` does not support execution        |
| `gh pr list --limit lots`                 | `unprocessable-content`. Invalid data never reaches your application. |

Command literals such as `pr` and `list` are routing tokens, not positional arguments. Each option binds to the route segment that owns it, so a parent and a child route can both declare an option with the same name.
