<!--
  Building Applications with iris.NewBuilder
  From the Iris book, chapter 3, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/iris-newbuilder/
-->
# Chapter 3: Building Applications with iris.NewBuilder

`iris.NewBuilder()` is the recommended way to assemble an Iris v14
application. It is a fluent chain of configuration steps whose order the
compiler enforces: each step returns the interface of the next, so CORS
cannot come after logging and controllers cannot be registered before
services. Along the way the builder installs the production plumbing you
would otherwise wire by hand: recovery, structured error handling,
compression, access logging, a health endpoint, dependency injection and
graceful shutdown. This chapter walks through every step, backed by what
the builder actually does in `iris_builder.go`, and ends with a complete
application you can run and test.

## Background

**What "wiring" an application means.** Every server program has two
phases: a short setup phase and a long serving phase. In the setup
phase you read configuration (the values that differ per deployment:
addresses, credentials, allowed origins), construct your services and
their dependencies, register routes and middleware, and decide what
must happen at shutdown. Wiring is that setup phase, and it is where
most production bugs are born, not because any single step is hard,
but because the steps depend on each other and their order matters.

**Cross-cutting concerns.** Some behavior belongs to every request
rather than to any one handler: logging what happened, recovering from
crashes, compressing responses, checking which websites may call you,
formatting errors consistently. These are called cross-cutting concerns
because they cut across all your routes. Handled naively, each one is a
line you must remember in every handler; handled as middleware wired
once at startup, they disappear from your handlers entirely.

**Startup and shutdown order.** Resources start in dependency order
(the database connection opens before the service that queries it) and
must stop in a sensible order too: stop accepting new requests, let
in-flight ones finish, then close what startup opened. Forgetting the
shutdown half leaks connections and loses buffered logs. A good wiring
layer pairs every "open" with its "close" at the moment you write it.

**The builder pattern.** A builder is an object you configure through a
chain of method calls and finish with one final call (here `Build()`)
that produces the assembled product. The chain reads top to bottom like
a checklist, which is the point: the whole application's wiring is one
visible expression instead of registrations scattered across files.
Iris adds a stricter twist: each step returns a different type that
offers only the next legal steps, so skipping a step or calling them
out of order is not a bug you find under traffic. It is code that does
not compile. The class of mistake this removes is exactly the class
that is hardest to test for: middleware registered after the routes it
should protect, recovery forgotten, shutdown code never registered.

## Why the Builder

A production HTTP API needs more than routes: panics must become logged
500 responses instead of dropped connections, errors need one consistent
JSON shape, responses should compress, requests should be logged,
shutdown must close what startup opened, and middleware must run in the
right order. None of that is hard individually; getting the order right
every time is. The builder encodes the order once, in the framework, and
gives you named steps for the decisions that are actually yours: which
origins to allow, what to log, how to map your errors, which services to
inject.

The result is that a whole application reads as a single expression:

```go
iris.NewBuilder().
    Prefix("/api").
    AllowOrigin("https://example.com").
    Compression(true).
    LogRequests(true).
    Health(true, "production", "team@example.com").
    Errors(errors.NewOptions().
        MapErrors(errors.NotFound, ErrNoteNotFound)).
    Deferrables(func() { log.Println("bye") }).
    Services(store, NewNoteService).
    RouterMiddlewares().
    Middlewares().
    API("/notes", NewNotesAPI).
    Build().
    Listen(":8080")
```

`iris.NewBuilder(configurators ...iris.Configurator)` creates the
underlying application (any configurators you pass apply first) and
registers the `*iris.Application` itself as a dependency, so controllers
and custom components can receive it through injection.

## The Step Chain

Each builder method returns a narrower interface that exposes only the
next legal steps:

\[Diagram: Each builder step returns the interface for the next one, so the compiler walks you through the chain in order; four steps return their own interface and may be repeated, and Build ends the chain with an application.]

Three of the stages allow repetition. `Errors` and `Deferrables` return
`ServiceBuilder` again, so you may call them in either order or several
times before `Services` advances the chain. `RouterMiddlewares` returns
`MiddlewareBuilder`, so it can be called repeatedly before `Middlewares`
advances. `API` returns `APIBuilder`, so you mount as many groups as you
need before the terminal `Build`.

The order is not decorative. Because each step returns a different type,
skipping a stage is a compile error, not a runtime surprise. This is
also why real applications contain the two seemingly empty calls:

```go
Services(store).
RouterMiddlewares(). // nothing custom, still required to move on
Middlewares().       // nothing custom, still required to move on
API("/notes", NewNotesAPI)
```

`Services` returns `MiddlewareBuilder`, and the only way out of
`MiddlewareBuilder` is `Middlewares()`, so every chain passes through it
even with no arguments. The calls are not no-ops either: the first of
them triggers the builder's one-time middleware setup described in
[RouterMiddlewares and Middlewares](#step-reference). Strictly speaking
`RouterMiddlewares()` could be omitted, because `Middlewares()` invokes
it internally, but writing both keeps the two middleware layers visible
at the call site.

One consequence of the changing return types: you cannot hold the chain
in a single variable and reassign it across steps. Compute conditional
values first, then build in one uninterrupted expression:

```go
origin := "https://example.com"
if env == "development" {
    origin = "*"
}

app := iris.NewBuilder().
    Prefix("/api").
    AllowOrigin(origin).
    Compression(env == "production").
    LogRequests(env != "production").
    Health(true, env, "team@example.com").
    Services().
    Middlewares().
    Build()
```

## Step Reference

### Prefix and NoPrefix

`Prefix("/api")` creates a child router with `app.Router("/api")` and
points every later step at it: CORS, access logging, your middlewares
and all `API()` groups mount under that prefix. Use it for versioning
(`/api/v1`) or to keep the API apart from static content. `NoPrefix()`
targets the application root instead; it is the same as `Prefix("")`.

The one deliberate exception is the health endpoint, which always
registers at the root (see below).

### AllowOrigin

CORS (Cross-Origin Resource Sharing) is the browser's permission
system for cross-site calls: JavaScript served from `https://app.com`
may not call an API on a different origin unless that API's responses
explicitly allow it in headers. For some requests the browser first
sends a preflight (an automatic `OPTIONS` request that asks "may I?")
and proceeds only when the answer permits. The middleware writes those
headers and answers preflights for you; your only decision is which
origins to allow.

`AllowOrigin` configures the CORS middleware. The argument is trimmed
and lowercased, then routed:

- `"none"`: no CORS middleware is installed at all.
- `"*"`: allow every origin (public, credential-free APIs only).
- `"https://app.com"`: exactly one origin.
- `"https://app.com,https://admin.app.com"`: a comma-separated list.

When enabled, the builder registers
`cors.New().HandleErrorFunc(errors.FailedPrecondition.Err).
AllowOrigin(originLine).Handler()` with `UseRouter`, so it runs before
routing and handles preflight requests for every path under the prefix.
Wiring `HandleErrorFunc` to the errors package means a disallowed origin
is rejected with the same structured JSON error body as the rest of your
API, instead of an anonymous plain-text response.

### Cors

`AllowOrigin` covers the common case in one string. Everything else the
middleware can do (credentials, exposed headers, max age, the origin
extractor, a different error handler) needs the CORS value itself, which
is what the alternative step at this stage takes:

```go
iris.NewBuilder().
    Prefix("/api").
    Cors(cors.New().
        HandleErrorFunc(errors.FailedPrecondition.Err).
        AllowOrigins("https://app.com", "https://*.admin.com").
        ExposeHeaders("X-Total-Count").
        AllowCredentials()).
    Compression(true).
    // ...
```

The two steps are the same call underneath: `AllowOrigin(line)` is the
shorthand for
`Cors(cors.New().HandleErrorFunc(errors.FailedPrecondition.Err).AllowOrigin(line))`.
Both register through `UseRouter` at the same position in the chain, so
preflights are answered before routing either way. Pass `nil` and no
middleware is installed, the same as `AllowOrigin("none")`.

Your value is used exactly as given, with no defaults merged into it.
That one difference matters: `cors.New()` on its own carries the
middleware's default error handler, so a `Cors` step that omits
`HandleErrorFunc` rejects disallowed origins with a bare 403 rather than
the structured error body `AllowOrigin` installs. Set it yourself when
you want the two to behave alike.

`AllowCredentials` is worth a note. A credentialed cross-origin request
(one carrying cookies or an `Authorization` header) is only permitted
when the response says `Access-Control-Allow-Credentials: true`, and
browsers refuse that header when the allowed origin is the wildcard `*`.
The middleware follows the browser rule: with a wildcard origin it
suppresses the credentials header unless you called `AllowCredentials`
explicitly. In practice a browser always sends `Origin` and the
middleware echoes that origin back rather than `*`, so credentials are
advertised normally; the wildcard case arises for requests that carry no
`Origin` header at all, where `AllowCredentials` is the way to opt in.

### Compression

Compression shrinks response bodies before they cross the network
(JSON and HTML typically compress to a fraction of their size), trading
a little CPU for less bandwidth and faster responses on slow links.
Client and server agree on the algorithm through headers, which is the
negotiation described below.

`Compression(true)` only records a flag; the actual
`middleware/compress` handler is installed later, during the one-time
middleware setup, and only if a handler named `iris.compression` is not
already registered. The middleware negotiates the response encoding from
the client's `Accept-Encoding` header (gzip, deflate, br, snappy, s2,
zstd)
and also decompresses request bodies that arrive with a
`Content-Encoding` header, which is the "read" half of the parameter's
`enableReadWriteCompression` name. Pass `false` while debugging if you
want to read raw response bodies on the wire.

### LogRequests and LogRequestsWith

`LogRequests(true)` is a shortcut for `LogRequestsWith(ac)` with the
framework's tuned `middleware/accesslog` configuration:

- JSON lines, indented, with human-readable timestamps, written to
  `access_log.json` and mirrored to stdout.
- Per request: latency, status code, method, path, client IP, request
  and response bodies, and their byte sizes.
- Panics are logged too (`PanicLog`), with multi-line errors preserved.
- Synchronous writes (`Async = false`), so nothing is lost on crash.

The handler registers with `UseRouter` on the builder's router, and its
`Close` method is added as an application closer, so buffered output is
flushed on shutdown. `LogRequests(false)` installs nothing.

When the defaults do not fit, build your own access log and pass it at
the same position; a `nil` value is ignored:

```go
ac := accesslog.File("access_log.json")
ac.RequestBody = false // do not log request payloads
ac.AddFields(func(ctx iris.Context, f *accesslog.Fields) {
    f.Set("tenant", ctx.Header("X-Tenant-ID"))
})

// ... .Compression(true).LogRequestsWith(ac). ...
```

Either way, add `access_log.json` to `.gitignore`.

### Health and NoHealth

A health endpoint is a URL whose only job is to answer "is this
process alive, and which build is it?". Monitoring systems poll it and
alert, restart or reroute traffic when it stops answering.

`Health(true, env, developer)` registers `GET /health` at the
application root, intentionally outside the prefix: load balancers,
Kubernetes probes and uptime monitors ask for `/health`, not
`/api/v1/health`. The handler comes from `middleware/modrevision` and
responds with plain text containing the server name and environment
(`Iris Server (production)`), the build revision and build time when the
binary carries module build info, the developer line, detected cloud
container information, and a final `OK`.

The `developer` string does double duty: the builder remembers it and,
during the one-time middleware setup, installs a handler that sets a
`Server: Iris` header on every response plus an `X-Developer` header
when the string is non-empty. That makes it trivial to tell from any
response which team owns a deployment. `NoHealth()` skips the endpoint
and leaves the developer empty, so no `X-Developer` header is emitted.

### Errors

`Errors(*errors.Options)` accepts the central error map from
`github.com/kataras/iris/v14/rest/errors`. The builder stores the
options and, during the one-time middleware setup, registers
`opts.Handler()` with `UseRouter`; the handler attaches your options to
each request context, so every error helper in the framework consults
your map when it renders a response.

`errors.NewOptions()` starts from useful defaults, sending
`database/sql.ErrNoRows` to `NOT_FOUND` and the `kataras/jwt` token
errors to `UNAUTHENTICATED`. You extend it with your own mappings:

```go
Errors(errors.NewOptions().
    MapErrors(errors.NotFound, ErrNoteNotFound).
    MapErrors(errors.PermissionDenied, ErrArchiveLocked).
    MapErrorFunc(errors.Internal, func(err error) error {
        return errors.Private(err) // hide details from clients
    }))
```

`MapErrors` matches with `errors.Is` semantics, `MapErrorFunc` and the
context-aware `MapErrorFuncCtx` transform errors, and `MapErrorTypes`
matches by type. The default rendering is a structured JSON object;
chain `SetRenderer(errors.Problem)` for RFC 7807 Problem Details
output. [Chapter 9](https://iris-go.com/docs/error-handling/) covers the error system in
full.

### Deferrables

`Deferrables(closers ...any)` registers shutdown work. Every argument is
forwarded to `app.AddCloser`, which accepts exactly these shapes and
panics at registration time on anything else:

- `func()`
- `func() error` (the error is preserved and joined into the shutdown
  error with `errors.Join`)
- `func(context.Context)`
- `func(context.Context) error`
- `io.Closer` (anything with `Close() error`)
- the context-aware `Closer` from `core/server/closer`
  (`CloseWithContext(context.Context) error`)

Closers run in registration order when the application shuts down through
`app.Shutdown(ctx)`. The built-in interrupt handler drains in-flight
requests but does not run them by itself, so register the shutdown
explicitly if you want closers on Ctrl+C:

```go
iris.RegisterOnInterrupt(app.Shutdown)
```

Because the server's own shutdown is prepended to the closer list, it
releases `Listen` while the remaining closers are still running. Keep
`main` alive until they finish (for example with a channel the last
closer closes) if their work must complete before the process exits.

```go
Deferrables(
    func() { worker.Stop() },
    func() error { return cache.Flush() },
    logFile, // io.Closer
)
```

### Services and the Initializer Hook

`Services(deps ...any)` feeds the dependency injection container that
later constructs your middlewares and controllers. Each argument is
routed by its shape:

| Argument shape                          | What the builder does                                                                                                                                                      |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nil`                                   | Skipped.                                                                                                                                                                   |
| `func()` or `func() error`              | Added as a shutdown closer only; a function with no outputs cannot be a constructor.                                                                                       |
| Value with `Close()` or `Close() error` | Registered as a dependency and as a shutdown closer.                                                                                                                       |
| Value implementing `iris.Worker`        | Registered as a dependency, started after the initializers; at shutdown it is stopped and waited for before its own `Close`, in registration order with the other closers. |
| `iris.Configurator`                     | Applied to the application immediately; not a dependency.                                                                                                                  |
| Anything else                           | Registered as a dependency: values by their type, functions as constructors.                                                                                               |

Constructor functions are called lazily with their parameters resolved
from previously registered dependencies, which is why order matters
inside a `Services` call:

```go
Services(
    store,          // *NoteStore value (has Close: also a closer)
    NewNoteService, // func(*NoteStore) *NoteService
)
```

If registration fails, `Services` panics with the underlying error. That
is deliberate: a broken dependency graph should stop the process at
startup, not surface as confusing behavior under traffic.

Services also get a startup hook. Any registered singleton (a value you
passed or a constructor's output) that implements `iris.Initializer`
runs exactly once inside `Build()`, after dependency injection
completes, in registration order:

```go
type Initializer interface {
    Init(ctx context.Context) error
}
```

Use it for cache warm-up, connection pinging or background pool setup.
An `Init` error aborts the build, making the interface the startup
counterpart of `Deferrables`.

Long-running work has its own interface. A scheduler, a queue consumer or
a cache refresher passed to `Services` as an `iris.Worker` runs inside the
same chain, instead of in a goroutine started next to it:

```go
type Worker interface {
    Run(ctx context.Context) error
    Stop(ctx context.Context) error
}
```

`Run` starts in its own goroutine after every `Initializer` has returned,
with a context that is cancelled at shutdown. An error it returns before
that is logged with the worker's type name and nothing else: a failed
cache refresher should not take down a server that can still answer every
request which does not need it.

`Stop` runs once, in registration order with the closers, and `Shutdown` then waits for `Run`
to return, so a worker is really finished when `Shutdown` reports success
rather than still draining while the process exits from under it. A value
that is both a `Worker` and a closer is stopped, waited for, and only then
closed, because closing the connection a running `Run` is still reading
from is the crash that ordering avoids.

Both halves are bounded. They get the `Shutdown` context when that one
carries a deadline, and a ten second budget when it does not, so a hanging
`Stop` cannot hold the process open. That budget is *per worker*, not per
shutdown, because the closers run one after another: pass a deadline to
`app.Shutdown` to bound the whole chain instead.

A `Worker` is a dependency like any other value, so the same scheduler can
be injected into a controller that wants to trigger it. Only a `Worker` passed to
`Services` as a value is started; one returned by a constructor is injected
but never run, so hand the scheduler itself to `Services`, not its constructor.

### RouterMiddlewares and Middlewares

Two of the built-ins are installed by `NewBuilder` itself, before the
first step runs, because `Prefix` copies the parent's context options and
macros into the child router the moment it is called:

- the macro (path parameter) error handler,
  `errors.DefaultPathParameterTypeErrorHandler`, so a request like
  `/notes/abc` against `{id:int}` renders `400 INVALID_ARGUMENT`
  ("invalid path parameter") instead of a bare 404, and
- the context error handler, `errors.DefaultContextErrorHandler`, so
  failures inside response writers such as `ctx.JSON` render structured
  errors.

These two methods separate the middleware layers, and the first call to
either performs the builder's one-time setup, guarded by a `sync.Once`.
That setup installs the pre-routing chain, in execution order:

1. Your `Errors` options handler, if you provided one. It goes first so
   everything after it, the CORS rejection included, is written through
   the collector, renderer and id generator you configured.
2. The access log, when `LogRequests` asked for one. It logs when
   `ctx.Next` returns rather than from a `defer`, so it has to sit
   outside recovery: otherwise a panicking request is logged nowhere.
3. The recovery middleware with `errors.Internal.LogErr` as its error
   handler: a panic becomes a logged, structured 500 response. Skipped
   when a handler named `iris.recovery` or `iris.errors.recover` is
   already registered.
4. CORS, below recovery so a panic inside its error handler is caught,
   and below the access log so a rejection is recorded.
5. The `Server: Iris` header on every response, plus `X-Developer` when
   `Health` received a developer string.
6. `compress.Safe`, when `Compression(true)` was chosen. `Safe` rather
   than `Handler`: an unbounded decompressor turns a one-megabyte gzip
   body into a gigabyte of memory.
7. `errors.Handler` from `rest/errors`, the last of the built-in router
   middleware and after compression, so what it writes is compressed
   like any other body. A failure status that reaches the client with
   nothing written, from a route or from any middleware registered after
   this point, answers the JSON envelope of [Chapter
   9](https://iris-go.com/docs/error-handling/) instead of an empty body:
   `ctx.StopWithStatus(403)`, `ctx.NotFound()`, the 401 of the `jwt`
   middleware.
8. The rate-limit exceed handler, which is a binding rather than a
   middleware in the chain: responses from `middleware/rate` render as
   the errors package's JSON instead of empty 429s.

When the application is built, the builder also registers
`errors.Handler` as the 404 and the 405 handler, so an unknown path
answers `NOT_FOUND` as JSON and not the text `Not Found`. It fills those
two slots only when they are empty: a `HandleNoRoute` or
`HandleNoMethod` of your own, registered from a `Configurator` passed to
`NewBuilder`, wins. Two rejections are answered before `errors.Handler`
runs and keep their bare status: one from a handler a `Configurator`
registered with `UseRouter`, and the 415 of the compression middleware
for a request body that is not what its `Content-Encoding` says.

One consequence of that order is worth stating plainly: a panic inside
the access log or inside your own `Errors` options handler is above
recovery, so it is not recovered. Both are framework-adjacent code you
register once; keep them simple.

After the built-ins, your own entries register:

- `RouterMiddlewares(...)` uses `UseRouter`: the handlers run before
  routing, for every request that reaches the builder's router,
  including requests that end in 404. Use it for request IDs, security
  headers, metrics and global rate limiting.
- `Middlewares(...)` uses `Use`: the handlers run only for matched
  routes. Use it for authentication and anything that assumes a real
  route.

Both accept plain `iris.Handler` values as well as constructor
functions whose parameters resolve from registered services:

```go
RouterMiddlewares(func(ctx iris.Context) {
    ctx.SetHeader("X-Frame-Options", "DENY")
    ctx.Next()
}).
Middlewares(func(auth *AuthService) iris.Handler {
    return func(ctx iris.Context) {
        user, err := auth.UserFromRequest(ctx)
        if err != nil {
            errors.Unauthenticated.Err(ctx, err)
            return
        }
        ctx.Values().Set("user", user)
        ctx.Next()
    }
})
```

`Middlewares` calls `RouterMiddlewares()` internally before doing its
own work, so the built-ins install exactly once regardless of which
method you call first or how many times.

### API

`API(prefixPath string, controllers ...any)` mounts a route group under
the builder's prefix by delegating to `router.Router(prefixPath, ...)`.
A controller is anything with a `Routing(r iris.Router)` method; you
usually pass its constructor and let injection supply the dependencies,
as the complete example below shows. Plain handlers listed before a
controller act as middleware for that group only:

```go
API("/notes", NewNotesAPI).
API("/admin", basicauth.Default(map[string]string{
    "admin": adminPassword,
}), NewAdminAPI).
```

Call `API` as many times as you have groups. Controllers, grouping and
the helper controllers (`iris.Middleware`, `iris.Dependencies` and
friends) are the subject of [Chapter 4](https://iris-go.com/docs/routing-controllers/).

### Build

`Build()` finalizes the application: it runs the `iris.Initializer`
hooks of your services (once), then compiles the router and middleware
stack. On any misconfiguration it panics; like the `Services` panic,
this is intentional so that a broken application never starts serving.

It returns the `*iris.Application`, which you can serve or test:

```go
app := builder.Build()

// Plain HTTP.
app.Listen(":8080")

// Or with explicit server options (TLS shown; see Chapter 19).
app.Run(iris.ServerOptions{
    Addr:     ":443",
    CertFile: "cert.pem",
    KeyFile:  "key.pem",
})
```

## A Complete Application

The following program is a small but production-shaped notes API. It is
a single file here for readability; the layering (config, domain,
storage, controllers, wiring) is exactly what you would split into
packages as the application grows. The code compiles and runs as shown.

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "sync"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"
    "github.com/kataras/iris/v14/rest/errors"
)

// Config carries the deployment-specific values. iris.BaseConfiguration
// brings Host, Port, AllowOrigin, EnableCompression, RequestLog and the
// framework options, so cfg.Addr() feeds Listen and there is no Addr field
// of our own to keep in step. Environment variables keep the example short;
// Chapter 11 covers configuration in depth, including loading this same
// struct from a YAML document with iris.LoadConfiguration.
type Config struct {
    iris.BaseConfiguration `yaml:",inline"`

    Env       string `yaml:"Env"`
    Developer string `yaml:"Developer"`
    AdminUser string `yaml:"AdminUser"`
    AdminPass string `yaml:"-"`
}

func loadConfig() (Config, error) {
    cfg := Config{
        BaseConfiguration: iris.BaseConfiguration{
            Port:              8080,
            AllowOrigin:       "none",
            EnableCompression: true,
        },
        Env:       "development",
        Developer: "notes-team",
        AdminUser: "admin",
        AdminPass: os.Getenv("ADMIN_PASSWORD"),
    }

    // HOST, PORT, HTTP_PLATFORM_PORT and ALLOW_ORIGIN override the
    // defaults above. iris.LoadConfiguration calls this for you when the
    // configuration arrives as a YAML document.
    cfg.BindEnv()

    if cfg.AdminPass == "" {
        return cfg, fmt.Errorf("ADMIN_PASSWORD is required")
    }

    return cfg, nil
}

// ErrNoteNotFound is a sentinel error; the builder's Errors step maps
// it to a 404 response.
var ErrNoteNotFound = fmt.Errorf("note not found")

type Note struct {
    ID   int    `json:"id"`
    Text string `json:"text"`
}

// NoteInput is the request payload. Implementing Validate makes
// ctx.BindJSON run it automatically after decoding.
type NoteInput struct {
    Text string `json:"text"`
}

func (in NoteInput) Validate() error {
    if in.Text == "" {
        return fmt.Errorf("text: must not be empty")
    }
    return nil
}
```

The storage type demonstrates two builder behaviors at once: it has a
`Close` method, so `Services` registers it as a shutdown closer
automatically, and it implements `iris.Initializer`, so `Init` runs once
inside `Build()`.

```go
type NoteStore struct {
    mu    sync.Mutex
    seq   int
    notes []Note
}

func NewNoteStore() *NoteStore { return &NoteStore{} }

func (s *NoteStore) Init(ctx context.Context) error {
    log.Println("notes: store ready") // ping a real database here.
    return nil
}

func (s *NoteStore) Close() error {
    log.Println("notes: store closed")
    return nil
}

func (s *NoteStore) List(ctx context.Context) ([]Note, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    return append([]Note(nil), s.notes...), nil
}

func (s *NoteStore) Create(ctx context.Context, in NoteInput) (Note, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.seq++
    note := Note{ID: s.seq, Text: in.Text}
    s.notes = append(s.notes, note)
    return note, nil
}

func (s *NoteStore) Get(ctx context.Context, id int) (Note, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    for _, note := range s.notes {
        if note.ID == id {
            return note, nil
        }
    }
    return Note{}, fmt.Errorf("%w: %d", ErrNoteNotFound, id)
}
```

Two controllers: the public notes API and an admin API mounted behind
basic authentication. (Basic authentication is the simplest HTTP login
scheme: the client repeats a username and password in the
`Authorization` header of every request. Served over TLS, it is a
reasonable fit for small internal admin endpoints.)

```go
type NotesAPI struct {
    store *NoteStore
}

func NewNotesAPI(store *NoteStore) *NotesAPI {
    return &NotesAPI{store: store}
}

func (api *NotesAPI) Routing(r iris.Router) {
    r.Get("/", api.list)
    r.Post("/", api.create)
    r.Get("/{id:int}", api.get)
}

func (api *NotesAPI) list(ctx iris.Context) {
    notes, err := api.store.List(ctx)
    ctx.List(notes, err) // an empty store renders [], never null.
}

func (api *NotesAPI) create(ctx iris.Context) {
    input, ok := ctx.BindJSON[NoteInput]()
    if !ok {
        return // 400 already written: bad JSON or failed Validate.
    }

    note, err := api.store.Create(ctx, input)
    ctx.Created(note, err)
}

func (api *NotesAPI) get(ctx iris.Context) {
    note, err := api.store.Get(ctx, ctx.Params().GetInt("id"))
    ctx.OK(note, err) // ErrNoteNotFound renders as 404.
}

type AdminAPI struct {
    store *NoteStore
}

func NewAdminAPI(store *NoteStore) *AdminAPI {
    return &AdminAPI{store: store}
}

func (api *AdminAPI) Routing(r iris.Router) {
    r.Get("/stats", api.stats)
}

func (api *AdminAPI) stats(ctx iris.Context) {
    notes, err := api.store.List(ctx)
    if err != nil {
        errors.HandleError(ctx, err)
        return
    }
    ctx.JSON(iris.Map{"notes": len(notes)})
}
```

Finally the wiring. `newApp` takes its storage as a parameter, so tests
can build the identical application around any store they like:

```go
// newApp assembles the application. Storage arrives as a parameter:
// main passes the real store, tests pass whatever they need.
func newApp(cfg Config, store *NoteStore) *iris.Application {
    return iris.NewBuilder(cfg.Iris).
        Prefix("/api").
        AllowOrigin(cfg.AllowOrigin).
        Compression(cfg.EnableCompression).
        LogRequests(cfg.Env == "development").
        Health(true, cfg.Env, cfg.Developer).
        Errors(errors.NewOptions().
            MapErrors(errors.NotFound, ErrNoteNotFound)).
        Deferrables(func() { log.Println("notes: goodbye") }).
        Services(store).
        RouterMiddlewares().
        Middlewares().
        API("/notes", NewNotesAPI).
        API("/admin", basicauth.Default(map[string]string{
            cfg.AdminUser: cfg.AdminPass,
        }), NewAdminAPI).
        Build()
}

func main() {
    cfg, err := loadConfig()
    if err != nil {
        log.Fatal(err)
    }

    app := newApp(cfg, NewNoteStore())
    app.Listen(cfg.Addr())
}
```

Run it and exercise the behaviors the builder gave you for free:

```sh
ADMIN_PASSWORD=change-me go run .

# 201 {"id":1,"text":"hello"}; an empty text renders
# 400 INVALID_ARGUMENT because ctx.BindJSON runs NoteInput.Validate.
curl -X POST localhost:8080/api/notes \
  -H 'Content-Type: application/json' -d '{"text":"hello"}'

# 404 NOT_FOUND: the central error map at work.
curl localhost:8080/api/notes/42

# 400 INVALID_ARGUMENT: malformed {id:int} path parameter.
curl localhost:8080/api/notes/abc

# 401, then 200 {"notes":1}.
curl localhost:8080/api/admin/stats
curl -u admin:change-me localhost:8080/api/admin/stats

# 200 OK: health lives at the root, outside /api.
curl localhost:8080/health
```

## Testing the Built Application

`Build()` returns a fully assembled `*iris.Application`, and
`httptest.New` drives it in memory: real router, real middleware, real
error map, no network. Because `newApp` accepts its dependencies, a test
constructs the exact production application around a test store:

```go
package main

import (
    "testing"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/httptest"
)

func testConfig() Config {
    return Config{
        BaseConfiguration: iris.BaseConfiguration{AllowOrigin: "none"},
        Env:               "test",
        Developer:         "tests",
        AdminUser:         "admin",
        AdminPass:         "test-password",
    }
}

func TestNotesAPI(t *testing.T) {
    app := newApp(testConfig(), NewNoteStore())
    e := httptest.New(t, app)

    e.POST("/api/notes").WithJSON(NoteInput{Text: "hello"}).
        Expect().Status(httptest.StatusCreated).
        JSON().Object().HasValue("id", 1)

    e.GET("/api/notes/42").Expect().Status(httptest.StatusNotFound)

    e.GET("/api/admin/stats").WithBasicAuth("admin", "test-password").
        Expect().Status(httptest.StatusOK).
        JSON().Object().HasValue("notes", 1)
}
```

[Chapter 16](https://iris-go.com/docs/testing/) expands on this pattern.

## When to Use iris.New Instead

`iris.New()` creates a bare application: no recovery, no error
handling, nothing installed for you. It remains the right tool for
small programs and for setups where you want to control every
registration yourself:

```go
app := iris.New()
app.UseRouter(recovery.Handler)
app.Get("/", func(ctx iris.Context) {
    ctx.JSON(iris.Map{"message": "hello"})
})
app.Listen(":8080")
```

For quick prototypes there is also `iris.Default()`, which is itself
implemented as a builder chain, a good indication of how idiomatic the
builder is:

```go
iris.NewBuilder().
    NoPrefix().
    AllowOrigin("*").
    Compression(true).
    LogRequests(false).
    NoHealth().
    Services().
    RouterMiddlewares(requestid.New()).
    Middlewares()
```

plus debug-level logging. Note the missing `Build()`: `iris.Default()`
hands back an application that has not been built, so routes registered
after the call are still served, exactly as with `iris.New()`. For
anything you intend to deploy, start from `iris.NewBuilder()`.

## Summary

- `iris.NewBuilder()` assembles a production-ready application in one
  expression; each step returns the next step's interface, so the order
  is checked by the compiler.
- The chain is `Prefix`/`NoPrefix`, `AllowOrigin` (`"none"` disables) or
  `Cors` for a fully configured CORS value,
  `Compression`, `LogRequests`/`LogRequestsWith`, `Health`/`NoHealth`
  (always `GET /health` at the root), then `Errors`, `Deferrables` and
  `Services`, then `RouterMiddlewares`/`Middlewares`, then `API` groups
  and `Build`.
- The first middleware call installs the built-ins once: path parameter
  error handling, your error options, recovery, the context error
  handler, `Server`/`X-Developer` headers, compression, `errors.Handler`
  and the rate-limit exceed handler. An unknown path, a wrong method and
  a failure status with no body all answer the `rest/errors` JSON
  envelope. Empty `RouterMiddlewares().Middlewares()`
  calls exist because the type chain must pass through them.
- `Services` routes arguments by shape: zero-output functions become
  closers, values with `Close` become dependency plus closer,
  configurators apply immediately, `nil` is skipped, and registration
  failures panic at startup.
- Services implementing `iris.Initializer` run `Init(ctx)` once inside
  `Build()`, after injection, in registration order; an error aborts
  the build. `Deferrables` is the mirror image at shutdown.
- `Build()` panics on misconfiguration by design, and returns the
  `*iris.Application` for `Listen`, `Run` or `httptest`.

## Further Reading

- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS):
  how browsers police cross-site calls and what a preflight request
  is.
- [MDN: Compression in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Compression):
  how clients and servers negotiate response encoding with
  `Accept-Encoding` and `Content-Encoding`.
- [MDN: HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication):
  the Basic scheme used by the admin API example, and its relatives.
- [RFC 7807: Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc7807):
  the standard error-body format the `errors.Problem` renderer emits.
- [context (pkg.go.dev)](https://pkg.go.dev/context): the standard
  package behind `Init(ctx)`, `Shutdown(ctx)` and cancellation
  throughout the book.
