<!--
  Routing & Controllers
  From the Iris book, chapter 4, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/routing-controllers/
-->
# Chapter 4: Routing & Controllers

An Iris route maps an HTTP method and a URL path to a chain of handlers.
This chapter covers the registration API, typed path parameters, grouping
with child routers, the constructor-based controller pattern that scales
from a single file to a full service, error-returning handlers through
`rest/errors`, subdomains, and route naming and introspection.

## Background

**The shape of a URL.** A URL such as
`https://example.com/api/v1/users/42?page=2&sort=name` splits into
parts the router cares about differently. The scheme (`https`) and
host (`example.com`) select the server. The path
(`/api/v1/users/42`) is a list of segments separated by slashes and
names the resource being acted on. The query string (everything after
the `?`, here `page=2&sort=name`) is a set of optional key-value
pairs, conventionally used for filtering, paging and sorting rather
than for identity. Routing decides on method and path only; the query
string never chooses the handler, it only tunes what the handler does.

**Static and dynamic segments.** `/users/42` and `/users/97` should
run the same code, so a route pattern mixes fixed segments (`users`)
with placeholders (`{id}`) that match whatever appears in that
position. The placeholder's value is handed to your handler as a
parameter.

**What typed parameters buy you.** Without them, every handler opens
with the same ritual: pull the raw string out of the path, convert it
to a number or date, check the conversion error, answer 400 or 404,
and only then do the real work. A typed parameter such as `{id:int}`
moves that ritual into the route table: the router validates and
converts before any handler of yours runs, so a handler that runs can
trust its input completely. Multiplied across every parameter of every route,
the win is a whole class of "forgot to check" bugs that can no longer
be written, and validation rules that sit in one visible place (the
route) instead of scattered through function bodies.

**What grouping and controllers organize.** Real APIs have shared
structure: everything under `/api/v1` shares that prefix, everything
under `/admin` requires an administrator, every route about books
needs the book service. A group captures the path-level sharing once
(a common prefix plus common middleware), so individual routes stay one
line each. A controller goes a step further and organizes the code:
one struct owns a family of related handlers together with the
dependencies they share, and declares its routes in one method. Groups
organize paths; controllers organize code around those paths.

## Route Registration

Every router (the application itself or any child created with
`Router`) exposes one function per HTTP method. A handler is a
`func(ctx iris.Context)` (the `iris.Handler` type); when you pass more
than one, they run in order and the last is the main handler.

```go
package main

import "github.com/kataras/iris/v14"

func main() {
    app := iris.New()

    app.Get("/", index)                    // GET /
    app.Post("/users", createUser)         // POST /users
    app.Put("/users/{id:int}", updateUser) // PUT /users/42
    app.Delete("/users/{id:int}", deleteUser)

    // One handler for several methods, space or comma separated.
    app.Handle("GET POST", "/contact", contact)

    // Every HTTP method.
    app.Any("/ping", ping)

    app.Listen(":8080")
}

func index(ctx iris.Context) {
    ctx.JSON(iris.Map{"message": "hello"})
}
```

The method functions (`Get`, `Post`, `Put`, `Delete`, `Patch`, `Head`,
`Options`, `Connect`, `Trace`, `Query`) return a `*Route`, so registration
can be chained with route configuration such as `SetName`. `Handle` and
`Any` create one route per method and return a `*RouteRegistry` holding
all of them.

`Query` is HTTP QUERY (RFC 10008): a safe, idempotent read that carries a
body, the natural method for a search or a filtered list.
`iris.MethodQuery` is its name (`"QUERY"`) wherever a method string is
expected, as in `app.Handle("POST QUERY", "/list", list)`, and `Any`
includes it. When the router redirects a request to its trailing-slash
twin, POST, PUT, PATCH and QUERY get a 307 so the body survives the
redirect.

There is no optional-parameter syntax. When a path segment may be absent,
register two routes that share the same handler:

```go
app.Get("/search", search)
app.Get("/search/{term}", search)
```

## Path Parameters

A dynamic path segment is written as `{name}` or `{name:type}`. The type
is a *macro*: it validates the raw segment before your handler runs and
converts it to a Go value. Without a type, `{name}` means `{name:string}`.

One type in the table deserves a definition: a UUID (universally
unique identifier) is a 128-bit identifier written as 36 hexadecimal
characters, such as `9f86d081-8292-4c4c-812f-0a1b2c3d4e5f`. Because a
freshly generated UUID is unique for all practical purposes, services
use them as record IDs that any machine can mint without coordinating
with a central counter, which is why they appear in this book's
examples wherever a database ID crosses a URL.

### Built-in Parameter Types

| Type             | Syntax                      | Go type          | Matches                                                                          |
| ---------------- | --------------------------- | ---------------- | -------------------------------------------------------------------------------- |
| `string`         | `{name}` or `{name:string}` | `string`         | any single path segment (default)                                                |
| `int`            | `{id:int}`                  | `int`            | integers (alias: `number`)                                                       |
| `int8`..`int64`  | `{n:int64}`                 | `int8`..`int64`  | sized integers (`int64` alias: `long`)                                           |
| `uint`..`uint64` | `{n:uint64}`                | `uint`..`uint64` | unsigned integers                                                                |
| `bool`           | `{v:bool}`                  | `bool`           | `strconv.ParseBool` values: 1, 0, t, f, true, false, ... (alias: `boolean`)      |
| `alphabetical`   | `{name:alphabetical}`       | `string`         | letters and spaces only                                                          |
| `file`           | `{name:file}`               | `string`         | letters, digits, underscore, dash, dot; no spaces                                |
| `path`           | `{rest:path}`               | `string`         | multi-segment wildcard; must be the last segment                                 |
| `uuid`           | `{id:uuid}`                 | `uuid.UUID`      | any RFC 9562 UUID, whatever its version (alias kept for compatibility: `uuidv4`) |
| `mail`           | `{addr:mail}`               | `string`         | e-mail address, syntax check only                                                |
| `email`          | `{addr:email}`              | `string`         | e-mail address, syntax plus MX domain lookup                                     |
| `date`           | `{d:date}`                  | `time.Time`      | date in YYYY/MM/DD format                                                        |
| `weekday`        | `{day:weekday}`             | `time.Weekday`   | day name or 0-6                                                                  |

### Constraints

Constraints follow the type, separated by spaces. A trailing `else N`
sets the HTTP status code the router responds with when validation fails.

| Constraint        | Applies to      | Example                            | Meaning                                    |
| ----------------- | --------------- | ---------------------------------- | ------------------------------------------ |
| `min(n)`          | string, numbers | `{name:string min(3)}`             | minimum length (string) or value (numbers) |
| `max(n)`          | string, numbers | `{name:string max(20)}`            | maximum length (string) or value (numbers) |
| `range(a,b)`      | numbers         | `{age:int range(1,120)}`           | value between a and b, inclusive           |
| `regexp(expr)`    | string          | `{code:string regexp(^[A-Z]{3}$)}` | regular expression match                   |
| `prefix(s)`       | string          | `{name:string prefix(user_)}`      | must start with s                          |
| `suffix(s)`       | string          | `{name:string suffix(.pdf)}`       | must end with s                            |
| `contains(s)`     | string          | `{name:string contains(admin)}`    | must contain s                             |
| `eq(v)`           | string          | `{state:string eq(active)}`        | exact equality                             |
| `eqor(v1,v2,...)` | string          | `{flag:string eqor(on,off)}`       | equals any listed value                    |
| `else N`          | all             | `{id:int else 400}`                | status code on failure                     |

The `string` macro is the master type: when another type has no function
of the requested name, the string version is used. It compares against
the value's text, so `{id:int prefix(4)}` and `{id:uuid prefix(a6)}`
work as they read.

```go
app.Get("/page/{page:int min(1) max(100) else 400}", pageHandler)
app.Get("/users/{name:string min(3) max(20) else 400}", profileHandler)
app.Get("/files/{filepath:path}", fileHandler)
app.Get("/shelves/{id:uuid}", shelfHandler)
```

When a typed parameter fails validation and no `else` code is given, the
request never reaches your handler. Applications assembled with
`iris.NewBuilder` (whose `RouterMiddlewares` step installs the macro
error handler) render a 400 `INVALID_ARGUMENT` response with the message
"invalid path parameter".

Where that check happens is worth being precise about, because it is not
where most readers assume. The router's trie matches *shape*: segment
count and static segments. `/orders/abc` does match the route
`/orders/{id:int}`. The type check is a handler that route registration
prepends to the route's own chain, so it runs as that route's handler
zero: after any `UseRouter` middleware, and before the root `app.Use`
middleware that would otherwise see the request.

\[Diagram: The macro handler is prepended to the route at registration, the trie matches shape only, and the macro evaluator runs as handler zero of the matched chain, so a bad value is rejected before any handler of yours runs.]

### Accessing Parameters

Typed accessors return the value directly, with no `(value, error)` pairs.
Each has a fixed failure default:

```go
func handler(ctx iris.Context) {
    name := ctx.Params().GetString("name")   // "" if missing
    id := ctx.Params().GetInt("id")          // -1 on failure
    uid := ctx.Params().GetUint64("id")      // 0 on failure
    active := ctx.Params().GetBool("active") // false on failure
    when := ctx.Params().GetDate("d")        // zero time.Time on failure
    sid := ctx.Params().GetUUID("sid")       // zero uuid.UUID on failure

    if v, ok := ctx.Params().Lookup("name"); ok {
        _ = v // the evaluated value as any
    }
}
```

### Custom Macros

Add functions to an existing type or register a new type. Both must
happen before the server starts:

```go
// A new constraint on the string type:
// usage: /status/{status:string equals(active)}
app.Macros().Get("string").RegisterFunc("equals",
    func(expected string) func(string) bool {
        return func(paramValue string) bool {
            return paramValue == expected
        }
    })

// A completely new parameter type:
// usage: /events/{when:datetime}
app.Macros().Register("datetime", "", time.Time{}, false, false,
    func(paramValue string) (any, bool) {
        t, err := time.Parse("2006-01-02T15:04:05", paramValue)
        if err != nil {
            return nil, false
        }
        return t, true
    })
```

## Route Groups

`Router(relativePath, ...)` creates a child router that inherits its
parent's prefix and middleware. Groups nest to any depth, and any
`iris.Handler` passed alongside the path becomes middleware of the
group's matched routes.

```go
package main

import "github.com/kataras/iris/v14"

func main() {
    app := iris.New()

    api := app.Router("/api")
    api.Get("/status", status)  // GET /api/status

    v1 := api.Router("/v1")
    v1.Get("/users", listUsers) // GET /api/v1/users

    // Closure form: configure the child router in place.
    app.Router("/reports", func(r iris.Router) {
        r.Use(requireAuth)
        r.Get("/", listReports)
        r.Get("/{id:int}", getReport)
    })

    // Handlers passed to Router become the group's middleware.
    admin := app.Router("/admin", requireAuth, requireAdmin)
    admin.Get("/stats", stats)

    app.Listen(":8080")
}
```

Three registration points control where middleware runs; they matter
most on groups:

- `UseRouter(handlers...)` runs before the routing decision itself: on
  every request, including not-found (404) and method-not-allowed (405).
- `Use(handlers...)` on the root application also covers 404 and 405
  responses; on a child router it runs only for that group's matched
  routes.
- Handlers listed per route run last, in the order given, before the
  main handler.

The middleware contract is plain: call `ctx.Next()` to continue the
chain, or write a response and return to stop it. The built-in
middleware catalogue is covered in
[Chapter 12](https://iris-go.com/docs/built-in-middleware/).

## Controllers

The primary way to organize routes in Iris is a small convention rather
than a base class: a controller is a plain struct with a constructor
that declares its dependencies and a `Routing(r iris.Router)` method
that declares its routes. The router calls `Routing` once at startup.

```go
// api/shelves.go
package api

import (
    "github.com/kataras/iris/v14"

    "bookshelf/library"
)

// ShelvesAPI groups the /shelves handlers around their dependencies.
type ShelvesAPI struct {
    svc *library.Service
}

// NewShelvesAPI is the controller's dependency declaration: the DI
// container calls it with the registered *library.Service.
func NewShelvesAPI(svc *library.Service) *ShelvesAPI {
    return &ShelvesAPI{svc: svc}
}

// Routing is the controller's route table.
func (api *ShelvesAPI) Routing(r iris.Router) {
    r.Get("/", api.list)
    r.Post("/", api.create)
    r.Get("/{id:uuid}", api.get)
    r.Delete("/{id:uuid}", api.delete)

    // A shelf's books live in their own nested controller; its
    // constructor is resolved by the container exactly like this one.
    r.Router("/{shelf_id:uuid}/books", NewBooksAPI)
}

func (api *ShelvesAPI) list(ctx iris.Context) {
    shelves, err := api.svc.Shelves(ctx)
    ctx.OK(shelves, err)
}

func (api *ShelvesAPI) get(ctx iris.Context) {
    id := ctx.Params().GetUUID("id")

    shelf, err := api.svc.Shelf(ctx, id)
    ctx.OK(shelf, err)
}
```

The `ctx.OK(value, err)` helpers render the value or hand the
error to the central error pipeline; they are covered in
[Chapter 6](https://iris-go.com/docs/response-handling/) and
[Chapter 9](https://iris-go.com/docs/error-handling/).

### Registering Controllers

Pass the constructor to `Router`. Its parameters are resolved from the
dependencies you registered:

```go
app := iris.New()
app.RegisterDependency(library.NewMemStore(), library.NewService)
app.Router("/shelves", api.NewShelvesAPI)
```

In a builder-assembled application the `API` step does the same after
`Services` has populated the container. This is the recommended setup,
shown in [Chapter 3](https://iris-go.com/docs/iris-newbuilder/):

```go
iris.NewBuilder().
    // ... Services(repo, library.NewService), middleware steps ...
    API("/shelves", api.NewShelvesAPI).
    Build().
    Listen(":8080")
```

### Accepted Controller Shapes

`Router(path, c ...any)` accepts, in any combination:

- a `func(r iris.Router)` closure;
- an `iris.Handler` (or a slice of handlers), registered as the group's
  middleware;
- a pointer to a struct implementing `Routing(r iris.Router)`, whose
  exported fields are bound from the registered dependencies;
- a constructor function whose parameters are resolved from the
  dependencies and which returns such a struct, the `NewXxxAPI` form
  used above;
- the helpers `iris.Middleware(handlers...)` and
  `iris.Dependencies(deps...)`, which add group middleware and
  controller-only dependency overrides positionally:

```go
app.Router("/api",
    iris.Middleware(authMiddleware), // group middleware
    iris.Dependencies(specialDep),   // extra deps for what follows
    api.NewShelvesAPI,
    api.NewAdminAPI,
)
```

## Guards and Positional Middleware

A guard is middleware with a yes/no job: it checks one precondition
(the record exists, the user is allowed to write) and either lets the
chain continue or ends the request with the right error. Putting the
check in a guard means the handlers behind it never restate it.

Because guards are methods on the controller struct, they see the same
injected dependencies as the handlers. Two idioms cover most cases: a
*group guard* registered with `r.Use` runs before every route of the
controller, and a *positional guard* placed between path and handler
protects individual routes.

```go
// api/books.go
package api

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"

    "bookshelf/library"
)

// shelfContextKey caches the guard-loaded shelf on the request.
const shelfContextKey = "bookshelf.shelf"

// BooksAPI serves /shelves/{shelf_id:uuid}/books.
type BooksAPI struct {
    svc *library.Service
}

func NewBooksAPI(svc *library.Service) *BooksAPI {
    return &BooksAPI{svc: svc}
}

func (api *BooksAPI) Routing(r iris.Router) {
    // Group guard: every book route needs an existing shelf.
    r.Use(api.requireShelf)

    r.Get("/", api.list)
    // Positional guard: writes also need an unlocked shelf.
    r.Post("/", api.requireUnlocked, api.create)
    r.Put("/{id:uuid}", api.requireUnlocked, api.update)
    r.Delete("/{id:uuid}", api.requireUnlocked, api.delete)
}

// requireShelf loads the shelf once, replies 404 when it does not
// exist and caches it for the handlers that follow.
func (api *BooksAPI) requireShelf(ctx iris.Context) {
    shelfID := ctx.Params().GetUUID("shelf_id")

    shelf, err := api.svc.Shelf(ctx, shelfID)
    if errors.HandleError(ctx, err) {
        return // mapped 404, request stopped.
    }

    ctx.Values().Set(shelfContextKey, shelf)
    ctx.Next()
}

// requireUnlocked rejects writes against a locked shelf.
func (api *BooksAPI) requireUnlocked(ctx iris.Context) {
    shelf, _ := ctx.ValueAs[library.Shelf](shelfContextKey)
    if shelf.Locked {
        errors.PermissionDenied.Detail(ctx, "shelf is locked",
            "unlock the shelf before modifying its books")
        return
    }
    ctx.Next()
}
```

The handlers then read the cached shelf with
`ctx.ValueAs[library.Shelf](shelfContextKey)` instead of
loading it again. The full controller, with its list, create, update and
delete handlers, is developed step by step in the framework's
`TUTORIAL.md`.

## Error-Returning Handlers

Plain handlers return nothing; error handling happens inside them. The
`rest/errors` package offers an alternative: `errors.NewRouter` wraps
any `iris.Router` so that routes accept
`func(ctx iris.Context) error`. A non-nil return value flows through the
central error map: `sql.ErrNoRows` becomes 404 by default, and your
own mappings come from `errors.NewOptions()` as shown in
[Chapter 9](https://iris-go.com/docs/error-handling/).

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"
)

func main() {
    app := iris.New()

    r := errors.NewRouter(app.Router("/api"))

    r.UseRouter(func(ctx iris.Context) error {
        // Runs before routing, even for 404/405 requests.
        // Return nil to continue; a non-nil error is rendered
        // by the error pipeline and stops the request.
        return nil
    })

    r.Get("/books/{id:uuid}", func(ctx iris.Context) error {
        book, err := findBook(ctx.Params().GetString("id"))
        if err != nil {
            return err // mapped centrally.
        }
        return ctx.JSON(book)
    })

    app.Listen(":8080")
}
```

The wrapper mirrors the core router: `Get`, `Post`, `Put`, `Delete` and
the rest register error-returning functions, `Use` and `UseRouter`
accept error-returning middleware, and `Subdomain` returns another
wrapped router. The embedded core router remains accessible for anything
else.

## Subdomains

A subdomain is the label in front of your domain: in
`admin.example.com` it is `admin`. DNS usually points every subdomain
at the same server (a wildcard record), and the server tells them
apart by the `Host` header each request carries, which lets one Iris
application serve `example.com` and `admin.example.com` with entirely
different route tables.

`Subdomain` is available on every router and returns a child router
bound to a host label instead of a path prefix. Calling it on a child
prepends the label, so subdomains nest naturally.

```go
admin := app.Subdomain("admin")
admin.Get("/", dashboard) // admin.example.com/

// Nested: registers "panel.admin."
panel := app.Subdomain("admin").Subdomain("panel")
panel.Get("/", panelIndex) // panel.admin.example.com/

// Wildcard: any subdomain not matched by a static one.
wild := app.WildcardSubdomain()
wild.Get("/", func(ctx iris.Context) {
    ctx.Writef("subdomain: %s", ctx.Subdomain())
})
```

`WildcardSubdomain` is a method of the root application. Matching
enforces dot boundaries: for a registered `admin` subdomain, requests to
`admin` and `staff.admin` match, while `badmin` does not.

## Route Names and Introspection

Every route gets a unique-per-application name, defaulting to method +
subdomain + path (for example `"GET/users/{id:uuid}"`). Override it with
`SetName`, look routes up by name, and build concrete paths from a
route's template:

```go
route := app.Get("/users/{id:uuid}", getUser)
route.SetName("user.get")

r := app.Routes().GetByName("user.get")
path := r.ResolvePath("42") // "/users/42"
```

`None` registers an *offline* route under the virtual method
`iris.MethodNone`; it is not reachable over HTTP but can be inspected
and toggled online by swapping its exported `Method` field. Before the
server starts, `app.RemoveRoute(name)` deletes a route by name and
reports whether it was found.

Registration order rarely matters, because routes are sorted at build
time: static paths sort before parameterized paths of equal depth, and
more specific parameter types (`int`, `uuid`, ...) are evaluated before
the `string` and `path` fallbacks when several routes share a position.
Parameterized routes of equal priority keep their registration order.

## Generic MVC Controllers

Two acronyms first. MVC (Model-View-Controller) is a long-standing way
to organize applications: the model holds data and rules, the view
renders output, and the controller connects requests to both. In an
HTTP API, "controller" is the request-handling struct. CRUD names the
four operations nearly every resource needs: create, read, update,
delete.

For classic resource CRUD, the `controller/mvc` package generates the
route table for you: a controller implements a generic interface and
`mvc.UseController` mounts it, with `With` nesting further controllers
under the same path.

```go
import "github.com/kataras/iris/v14/controller/mvc"

app.Router("/stores", mvc.UseController[*StoreController]().
    With("/books", mvc.UseController[*BookController]()))
```

This chapter's `Routing` convention remains the general-purpose tool;
the generic MVC controllers and their extensions are covered in
[Chapter 24](https://iris-go.com/docs/advanced-topics/).

## Summary

- Method functions (`Get`, `Post`, ...) return a `*Route`; `Handle` and
  `Any` register one route per method and return a `*RouteRegistry`.
- Path parameters are typed macros such as `{id:uuid}` and
  `{n:int min(1)}`, validated before the handler runs, with `else N`
  controlling the failure status and `ctx.Params().GetX` reading the
  values.
- `Router("/prefix")` groups routes; handlers passed to it become group
  middleware, and closures configure the child in place.
- A controller is a struct with a `NewXxxAPI` constructor resolved by
  dependency injection and a `Routing(r iris.Router)` route table;
  controllers nest through `r.Router(path, NewChildAPI)`.
- Guards are controller methods: `r.Use` for the whole group, a
  positional handler for single routes.
- `errors.NewRouter` wraps any router so handlers return `error` into
  the central error pipeline.
- `Subdomain` and `WildcardSubdomain` route by host label; names,
  offline routes and `ResolvePath` cover introspection needs.

## Further Reading

- [MDN: URIs](https://developer.mozilla.org/en-US/docs/Web/URI).
  The anatomy of URLs: scheme, host, path, query and fragment.
- [RFC 3986: Uniform Resource Identifier — Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986):
  the specification that defines path segments and query strings
  precisely.
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110):
  what each method means on a resource, the semantics your route
  tables encode.
- [net/url (pkg.go.dev)](https://pkg.go.dev/net/url): how Go itself
  represents and parses URLs.
- [uuid (pkg.go.dev)](https://pkg.go.dev/uuid): the standard library's
  UUID package (Go 1.27), used by this chapter's examples and by the
  `:uuid` route macro.
- [RFC 9562: Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562):
  the UUID formats and versions the `:uuid` macro accepts.
