# The Iris Web Framework, complete documentation > The full text of the Iris book, fifth edition, covering Iris 14.0.0 on Go 1.27. > 24 chapters plus a preface and an epilogue. Free, BSD-3-Clause framework. Import path: github.com/kataras/iris/v14 Source: https://github.com/kataras/iris Canonical HTML: https://iris-go.com/docs/ Per-chapter markdown: https://iris-go.com/docs/.md API reference: https://codewiki.google/github.com/kataras/iris Every example in this document was verified against the framework source rather than written from memory. Where a claim here disagrees with the source, the source is right and the disagreement is a bug worth reporting. --- ## Contents - Preface 1. Getting Started 2. Core Concepts 3. Building Applications with iris.NewBuilder 4. Routing & Controllers 5. Reading Requests 6. Response Handling 7. Middleware 8. Dependency Injection 9. Error Handling 10. Authentication & Authorization 11. Configuration Management 12. Built-in Middleware 13. Database Integration 14. Caching Strategies 15. Internationalization (i18n) 16. Testing 17. Monitoring & Observability 18. Security Best Practices 19. Deployment & DevOps 20. Performance Optimization 21. AI Backends 22. Real-World Examples 23. Upgrading to v14 24. Advanced Topics - Epilogue --- ## Preface Iris is a web framework for Go, built for writing HTTP servers, REST APIs and backend services that are fast to run and pleasant to maintain. This book is its professional guide: a complete path from your first route to a tested, production-shaped application, written for Iris v14 and verified against the framework's source code. > **Ask AI about Iris, any time.** Iris has a free, live AI wiki at > [codewiki.google/github.com/kataras/iris](https://codewiki.google/github.com/kataras/iris), > generated by Google directly from the framework's source code and kept in > sync with it (each page shows the exact commit it reflects). It organizes > the codebase into browsable topics with architecture diagrams, and its > built-in chat answers questions about Iris in plain language, grounded > in the actual code. Keep it open next to this book: when you wonder "how > do I..." mid-chapter, ask there and jump back. > > If you would rather have the assistant on your own machine, Plexon AI > ([plexon.ai](https://plexon.ai)) is a desktop assistant for Windows, > macOS and Linux, built by the author of Iris. It ships an Iris skill > built in, covering the same ground as this book in a form written for > an agent to read, and its Software Developer persona turns it on. ### Who this book is for You should be comfortable with Go (syntax, packages, interfaces, the standard library basics) and with HTTP concepts such as methods, headers and status codes. No prior Iris experience is assumed. If you have built services with the standard library's `net/http` or another framework, you will move quickly; the book takes care to explain not just what each API does but why it is shaped the way it is. ### How the book is organized The chapters build on one another and are best read in order the first time; afterwards each stands alone as a reference. - **Chapters 1–3** are the foundation: installation, the framework's core concepts, and `iris.NewBuilder`, the recommended way to assemble a production application. - **Chapters 4–7** cover the request path: routing and controllers, reading requests, writing responses, and the middleware pipeline. - **Chapters 8–11** cover application structure: dependency injection, error handling, authentication and authorization, and configuration. - **Chapters 12–15** tour the built-in middleware, database integration, caching and internationalization. - **Chapters 16–20** are about running in production: testing, observability, security, deployment and performance. - **Chapters 21–24** close with AI backends, complete applications, the upgrade path to v14, and advanced framework extension points. An epilogue sends you off with the habits worth keeping and where to go next. ### Conventions Each chapter opens with a **Background** section that explains the underlying concepts in plain language: what an HTTP header actually is, what a database transaction guarantees, what authentication means. No prior specialty knowledge is assumed. Each chapter closes with a **Further Reading** list of canonical references (MDN, the relevant RFCs, official Go documentation, OWASP) for going deeper. Readers who already know a chapter's fundamentals can skip its Background entirely. Code examples use full import paths (`github.com/kataras/iris/v14/...`) and are complete enough to adapt directly; where an example belongs to a larger program, the chapter says so. Shell commands are written for a POSIX shell and work in PowerShell unless noted. The examples target Go 1.26 or newer. Two companion documents in the framework repository pair well with this book: the hands-on [TUTORIAL.md](https://github.com/kataras/iris/blob/main/TUTORIAL.md), which builds one small service end to end, and the [migration guide](https://github.com/kataras/iris/blob/main/MIGRATION.md) for readers upgrading an application from an earlier major version. This book itself covers v14 only. ### About Iris Iris has been developed in the open since 2016 by Gerasimos Maropoulos ([@kataras](https://github.com/kataras)) together with hundreds of contributors. The framework lives at [github.com/kataras/iris](https://github.com/kataras/iris) and its website is [iris-go.com](https://iris-go.com/). Issues, discussions and contributions are welcome there. This book is maintained separately from the framework, and corrections to it are welcome the same way. --- ## Chapter 1: Getting Started Iris is a web framework for the Go programming language, built for HTTP APIs and web applications. It gives you a fast router with typed path parameters, a single request/response context with safe accessors, dependency injection for wiring services into handlers, and a set of production middleware (recovery, compression, CORS, access logging) that assemble into one coherent stack. The module path is `github.com/kataras/iris/v14` and it requires Go 1.27 or newer. This chapter installs Iris, builds a first application, and previews the production pattern the rest of the book expands on. ### Background If Iris is your first web framework, or Go your first server-side language, this section explains the moving parts before you install anything. If you have built HTTP services before, skip to [Installation](#installation). **What a server does.** A web application is a program that never exits: it listens on a network port (an address such as `:8080`), waits for clients (browsers, mobile apps, `curl`, other services) to connect, and answers each request they send. One request in, one response out; HTTP is that simple loop repeated millions of times. **What a request and a response look like.** Both are short structured messages. A request carries a method (a verb such as `GET` for reading or `POST` for submitting data), a path (`/orders/42`), headers (key-value metadata such as `Accept: application/json`), and sometimes a body (the submitted data itself). The response carries a status code (a three-digit result: `200` success, `404` not found, `500` server error), its own headers, and a body: the HTML, JSON or file the client asked for. Every feature in this book manipulates some part of these two messages. **What the framework does for you.** Go's standard library can already serve HTTP. A framework adds the layer you would otherwise write by hand for every project: routing (deciding which of your functions runs for `GET /orders/42` versus `POST /orders`), parsing (turning raw request text into typed Go values you can trust), responding (turning your Go values back into correct HTTP with the right status code and content type), and the shared machinery around them: logging, crash recovery, security headers. The problem that disappears from your code is plumbing: you write the part that is unique to your application, and the framework supplies the part that is the same in every application. **What a Go module is.** Go installs dependencies per project, not system-wide. A module is a folder with a `go.mod` file that names your project and pins the exact version of every library it uses. `go mod init myapp` creates that file; `go get` adds a dependency to it and downloads the code. There is no separate package manager to install and no virtual environment to activate. The `go` tool you already have does all of it. The examples below use `curl`, a command-line tool (preinstalled on most systems) that sends an HTTP request to a URL and prints the response, so you can poke at your server without a browser. ### Installation You need [Go 1.27+](https://go.dev/dl/) and basic familiarity with the language (the [Go Tour](https://go.dev/tour/) covers everything this book assumes). Create a module and add Iris to it: ```sh mkdir myapp && cd myapp go mod init myapp go get github.com/kataras/iris/v14@latest ``` That is the entire setup. Iris is a regular Go module dependency: no code generators, no CLI tools, no configuration files required to start. ### Your First Application Create `main.go`: ```go package main import "github.com/kataras/iris/v14" func main() { app := iris.New() app.Get("/", func(ctx iris.Context) { ctx.WriteString("Hello, Iris!") }) app.Get("/greet/{name:string}", func(ctx iris.Context) { name := ctx.Params().GetString("name") ctx.JSON(iris.Map{"message": "hello, " + name}) }) app.Get("/orders/{id:int}", func(ctx iris.Context) { id := ctx.Params().GetInt("id") ctx.JSON(iris.Map{"order": id, "status": "pending"}) }) app.Listen(":8080") } ``` Run it and try the routes: ```sh go run main.go curl http://localhost:8080/ # Hello, Iris! curl http://localhost:8080/greet/iris # {"message":"hello, iris"} curl http://localhost:8080/orders/42 # {"order":42,"status":"pending"} curl http://localhost:8080/orders/abc # 404: "abc" is not an int ``` Three things are happening here. `iris.New()` returns an `*iris.Application` with sensible defaults. Route registration methods (`Get`, `Post`, `Put`, `Delete`, `Patch` and the rest) hang directly off the application, and each takes a path pattern plus one or more handlers. Every handler receives an `iris.Context`, the single object you use to read the request and write the response. `ctx.WriteString` writes a plain string body; `ctx.JSON` serializes any Go value (here `iris.Map`, a shorthand for `map[string]any`) and sets the content type. The path patterns carry typed macros. `{name:string}` matches any single path segment; `{id:int}` matches only integers, so a request for `/orders/abc` never reaches your handler. The most useful macros: | Macro | Matches | | --------------- | ------------------------------- | | `{name:string}` | any single path segment | | `{id:int}` | integers | | `{id:uuid}` | UUID v1/v4 | | `{ok:bool}` | true/false, 1/0 | | `{email:email}` | e-mail addresses | | `{path:path}` | the rest of the path, any depth | [Chapter 4](https://iris-go.com/docs/routing-controllers/) covers the full macro language, route grouping with `app.Router` and the controller pattern. ### Reading Parameters Iris exposes request input through typed accessors with safe defaults. There are no `(value, err)` pairs to unpack for simple reads: a missing string is `""`, a missing or malformed number is `-1`. Path parameters come from `ctx.Params()`: ```go app.Get("/users/{id:int}", func(ctx iris.Context) { id := ctx.Params().GetInt("id") // already validated by the macro. ctx.JSON(iris.Map{"user_id": id}) }) ``` Query string values come from `ctx.Query()` the same way: ```go app.Get("/search", func(ctx iris.Context) { q := ctx.Query().GetString("q") page := ctx.Query().GetInt("page") if page < 1 { page = 1 // GetInt returns -1 when the key is absent. } ctx.JSON(iris.Map{"query": q, "page": page}) }) ``` Because the macro already validated `{id:int}`, the handler can trust the value. When you need richer input handling (JSON bodies, forms, headers, validation), [Chapter 5](https://iris-go.com/docs/reading-requests/) covers it in depth. ### Running the Server `app.Listen(addr)` starts an HTTP server on the given address and blocks until the server stops. On SIGINT/SIGTERM it stops accepting new connections and drains in-flight requests gracefully; you do not write any shutdown code for the common case. (SIGINT and SIGTERM are the shutdown signals your process receives when you press Ctrl+C or when a container platform stops it.) If the server cannot start, because the port is taken or a route pattern is malformed, Iris logs the failure and the process exits with status 1. That is why the call takes no error check: a graceful stop returns nil and a failed start never returns at all. [Chapter 19](https://iris-go.com/docs/deployment-devops/) covers the opt-out for a process that runs more than one server. When you need more than an address, pass `iris.ServerOptions` to `app.Run`. TLS is the everyday reason. TLS (Transport Layer Security) is the encryption layer that turns `http://` into `https://`: it keeps traffic private and proves to clients that they are talking to your server, not an impostor. It requires a certificate: a file, signed by a trusted authority, that ties your domain name to your server. Let's Encrypt is a free certificate authority that issues and renews such certificates automatically: ```go // Manual certificates: app.Run(iris.ServerOptions{ Addr: ":443", CertFile: "cert.pem", KeyFile: "key.pem", }) // Automatic certificates from Let's Encrypt: app.Run(iris.ServerOptions{ Addr: ":443", Letsencrypt: true, Hosts: []string{"example.com"}, }) ``` `app.Listen(":8080")` is `app.Run` with a small set of defaults filled in for you: a TCP network, a three-minute keep-alive, socket sharding and the application's log writer. The options struct also covers TLS, timeouts and tunneling; [Chapter 19](https://iris-go.com/docs/deployment-devops/) walks through the production serving choices. ### A First Taste of the Production Stack `iris.New()` plus hand-registered routes is fine for small programs and for learning. For real services, Iris ships a builder that assembles the recommended stack: a compiler-enforced chain of steps that configures CORS, compression, logging, a health endpoint, centralized error handling and dependency injection, then mounts your API controllers. Three of those terms get full chapters later but deserve one line now. Middleware is code that runs before or after every handler: the place for logging, panic recovery and security checks, written once instead of in each handler. CORS (Cross-Origin Resource Sharing) is the browser permission system that decides which web pages served from other domains may call your API. Dependency injection means your constructors declare what they need (a database, a logger) and the framework supplies it, instead of your code reaching for global variables. A controller in this style is a plain struct with a constructor and a `Routing(r iris.Router)` method. Here is the smallest complete example: ```go package main import ( "github.com/kataras/iris/v14" ) // GreetAPI is a controller: a constructor that declares its dependencies // (none yet) and a Routing method that registers its routes. type GreetAPI struct{} func NewGreetAPI() *GreetAPI { return &GreetAPI{} } func (api *GreetAPI) Routing(r iris.Router) { r.Get("/{name:string}", api.greet) } func (api *GreetAPI) greet(ctx iris.Context) { name := ctx.Params().GetString("name") ctx.OK(iris.Map{"message": "hello, " + name}, nil) } func main() { app := iris.NewBuilder(). Prefix("/api"). AllowOrigin("none"). Compression(true). LogRequests(false). Health(true, "development", "you@example.com"). Services(). RouterMiddlewares(). Middlewares(). API("/greet", NewGreetAPI). Build() app.Listen(":8080") } ``` ```sh curl http://localhost:8080/api/greet/iris # {"message":"hello, iris"} curl http://localhost:8080/health # health report, at the root ``` Each step returns the interface of the next, so the compiler enforces the order; the two middleware steps stay in the chain even when you pass them nothing. `RouterMiddlewares()` installs recovery and the framework's error handling for you. The `ctx.OK(value, err)` helper renders the value as 200 JSON, and hands any non-nil error to a central error map you configure once at startup, which is what keeps production handlers this short. As the application grows, the same chain absorbs the real work: `Services(...)` registers repositories and services for injection into constructors like `NewGreetAPI`, and `Errors(...)` maps your domain errors to HTTP status codes in one place. Two references take you there: - `TUTORIAL.md` at the repository root builds a complete, tested service on this stack step by step (DI, error mapping, pagination, httptest). - [Chapter 3](https://iris-go.com/docs/iris-newbuilder/) documents every builder step in depth. ### Project Layout Start small: one `main.go` is a perfectly good layout for the first day. When the program grows past a few handlers, split it into the shape the tutorial uses, three layers with one direction of dependency: ``` myapp/ ├── go.mod ├── main.go # configuration, wiring, startup ├── main_test.go # HTTP tests against the in-memory application ├── api/ # HTTP layer: controllers and thin handlers │ └── greet.go └── catalog/ # your domain package: entities, rules, storage ├── catalog.go └── service.go ``` Name the domain package after your domain (the tutorial's Bookshelf service calls it `library`). The rule that keeps the layout healthy: `api` imports the domain package, and the domain package imports nothing from HTTP. That separation is what makes the service testable with plain Go interfaces later, as [Chapter 16](https://iris-go.com/docs/testing/) shows. ### Summary - Iris installs as a normal Go module: `go mod init`, then `go get github.com/kataras/iris/v14@latest`. - `iris.New()` creates an application; `app.Get(path, handler)` and friends register routes; handlers receive an `iris.Context`. - Path patterns carry typed macros such as `{name:string}` and `{id:int}` that validate before your handler runs. - Read input with `ctx.Params().GetString/GetInt` and `ctx.Query().GetString/GetInt`; missing values return safe defaults (`""`, `-1`), not error tuples. - `app.Listen(addr)` serves HTTP with graceful shutdown built in; `app.Run(iris.ServerOptions{...})` adds TLS, Let's Encrypt and server tuning. - The recommended production stack is `iris.NewBuilder` with controllers and the response helpers; the framework's `TUTORIAL.md` is the full hands-on walkthrough and [Chapter 3](https://iris-go.com/docs/iris-newbuilder/) covers the builder in depth. - Organize growing code as `main.go` plus an `api` package and a domain package, with dependencies pointing only toward the domain. ### Further Reading - [MDN: An overview of HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview): the best first read on how the request/response cycle actually works. - [MDN: HTTP messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages): the exact structure of requests and responses. Method, path, headers, status line, body. - [Managing dependencies (go.dev)](https://go.dev/doc/modules/managing-dependencies): the official guide to Go modules, `go get` and `go.mod`. - [A Tour of Go](https://go.dev/tour/): the interactive introduction to the Go language this book assumes. - [Writing Web Applications (go.dev)](https://go.dev/doc/articles/wiki/): serving HTTP with only the standard library; useful for seeing exactly what a framework saves you. --- ## Chapter 2: Core Concepts Every Iris program is built from four cooperating parts: an **Application** that owns the server lifecycle, a **Router** that maps requests to code, a pooled **Context** that carries one request from start to finish, and plain **handlers** chained into a middleware pipeline. This chapter builds the mental model of how those parts fit together, so that the rest of the book reads as detail rather than surprise. Everything shown here is the v14 API, verified against the framework source. ### Background Chapter 1 showed a request and a response from a distance. The parts of Iris this chapter describes exist to serve that exchange, so it is worth pinning down the vocabulary first. **The anatomy of an HTTP exchange.** Each request names a method, a path, headers and possibly a body. The method is the verb: `GET` reads, `POST` creates or submits, `PUT` replaces, `PATCH` partially updates, `DELETE` removes. The path (`/users/42`) names the thing the verb acts on. Headers are key-value metadata lines: who is asking (`Authorization`), what format the client wants back (`Accept`), what it is sending (`Content-Type`). The body carries the payload itself: form fields, a JSON document, an uploaded file. The response mirrors this with a status code, headers and a body. Status codes come in classes: `2xx` means success, `3xx` redirects the client elsewhere, `4xx` means the client made a mistake (`404` unknown path, `400` malformed input), and `5xx` means your server failed (`500` is the generic failure). You will choose methods and status codes constantly, so this small vocabulary pays for itself quickly. **Router and handler.** A handler is a function you write that serves one request: it reads what it needs and writes a response. The router is the dispatcher in front of all your handlers: it looks at each request's method and path and decides which handler runs. "Routing" is that decision; a "route" is one registered rule, such as `GET /users/{id}` runs `getUser`. Middleware (detailed later in this chapter) is a handler that runs before or after other handlers to do shared work such as logging or authentication. **Why object pooling.** A busy server may handle tens of thousands of requests per second, and Go serves each one on its own goroutine, a lightweight thread that Go can run by the million. If the framework allocated a fresh context object for every request, it would create garbage at the same rate, and Go's garbage collector would burn CPU cleaning it up. Object pooling is the fix: keep a bin of used context objects, wipe one clean for each new request, and drop it back in the bin afterwards, the way a restaurant washes plates instead of buying new ones for every meal. The only cost is a rule you must respect: never keep a context after your handler returns, because the same object will be reused for another request. The Context section below restates that rule where it matters. ### The Big Picture ``` +---------------------------------------------------+ | net/http server app.Listen / app.Run | +---------------------------------------------------+ | Application iris.go | | embeds the root Router builder | +---------------------------------------------------+ | Router core/router | | routes, groups (Router/Child), the mux | +---------------------------------------------------+ | Middleware pipeline UseRouter / Use | +---------------------------------------------------+ | Handlers func(ctx iris.Context)| | one pooled Context per request | +---------------------------------------------------+ ``` A request enters through a standard `net/http` server, is bound to a `Context` taken from a pool, travels through the router's middleware and the matched route's handler chain, and leaves as a written response. The `Context` is then reset and returned to the pool. There is no reflection and no magic in the hot path: a handler is a plain function, a chain is a slice of functions, and `ctx.Next()` moves an index forward. ### The Application `iris.New` returns a `*iris.Application`, a concrete struct defined in `iris.go`. Its routing surface is not its own: the struct embeds the root router builder, so every routing, middleware, and dependency-injection method you call on `app` comes from the embedded `*router.RouterBuilder`. ```go type Application struct { *router.RouterBuilder // routing, middleware, DI methods // unexported: options, context pool, logger, closers... } ``` What the `Application` itself adds is lifecycle management: | Method | Purpose | | ------------------------------------ | ----------------------------------------------------------------------------------------------------- | | `app.Listen(addr)` | Build and serve on a TCP address with sane defaults; a failed start is logged and exits with status 1 | | `app.Run(iris.ServerOptions{...})` | Build and serve with explicit server options; same exit rule as `Listen` | | `app.Build()` | Construct the router and internal state manually | | `app.Shutdown(ctx)` | Gracefully stop the server and registered closers | | `app.Logger()` | The application's structured logger | | `app.EditOptions(fn)` | Modify options before the application is built | | `app.RegisterOnBuild(before, after)` | Hooks around the build step | `app.Listen(":8080")` is a shortcut for `app.Run` with a TCP network, keep-alive, and socket sharding preconfigured. After `Build`, the application is a standard `http.Handler`, which is why it composes cleanly with the Go ecosystem and with `httptest` (see [Chapter 16](https://iris-go.com/docs/testing/)). A minimal but complete program: ```go package main import ( "github.com/kataras/iris/v14" ) func main() { app := iris.New(iris.Options{ Name: "hello", LogLevel: "debug", }) app.Get("/", func(ctx iris.Context) { ctx.Text("Hello, Iris!") }) app.Listen(":8080") } ``` `iris.Options` configures the application (`Name`, `LogLevel`, `Host`, plus nested `Router` and `Context` option structs). A partial literal merges with the defaults: unset fields are filled in, never zeroed. Configuration is covered in depth in [Chapter 11](https://iris-go.com/docs/configuration-management/). For production applications the recommended entry point is the compile-time checked `iris.NewBuilder` chain rather than bare `iris.New`; that is the subject of [Chapter 3](https://iris-go.com/docs/iris-newbuilder/). Everything in this chapter applies to both, because the builder produces the same `*iris.Application`. ### The Context `iris.Context` is an alias for `*context.Context`, the object that represents one request-response cycle. Contexts are pooled: at the start of a request one is acquired from a `sync.Pool`, wired to the incoming `*http.Request` and a wrapped response writer, and after the last handler returns it is reset and released back to the pool. This is why you must not keep a reference to `ctx` in a goroutine that outlives the handler: by the time it runs, the same object may be serving another request. #### It is a context.Context `iris.Context` implements the standard `context.Context` interface (`Deadline`, `Done`, `Err`, `Value`). You pass it directly into services, database drivers, and HTTP clients, and request cancellation propagates all the way down: ```go func (api *BooksAPI) list(ctx iris.Context) { // ctx satisfies context.Context: pass it straight down. books, err := api.svc.Books(ctx) ctx.List(books, err) } ``` For background work that must survive the response, derive a detached context with `ctx.WithoutCancel()`: it is no longer canceled when the request ends, but keeps the request's values. #### Method families at a glance The Context API is large but regular. Almost everything falls into three families: | Family | Representative methods | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Request reading | `Method`, `Path`, `Host`, `RemoteAddr`, `Header(name)`, `ContentType()`, `MediaType()`, `Query()`, `Params()`, `Body()`, `ReadJSON`, `ReadForm`, `ReadQuery`, `ReadBody`, `Cookie`, `CookieValue` | | Response writing | `StatusCode`, `SetHeader`, `AddHeader`, `SetContentType`, `JSON`, `Text`, `HTML`, `Writef`, `Write`, `Redirect`, `SendFile`, `ServeFile`, `SetCookie`, `ResponseHeader`, `ResponseWriter()` | | Flow control | `Next`, `Skip`, `StopExecution`, `IsStopped`, `StopWithStatus`, `StopWithError`, `StopWithJSON`, `StopWithText`, `Values()`, `SetErr`, `GetErr` | Two naming rules keep the request and response sides apart: - `ctx.Header(name)` reads a **request** header. `ctx.SetHeader` writes a **response** header, and `ctx.ResponseHeader(name)` reads one back. - `ctx.ContentType()` and `ctx.MediaType()` describe the **request** body. The response-side counterparts are `ctx.ResponseContentType()` and `ctx.ResponseMediaType()`. Path and query values come through typed accessors that return a value directly, with a type-specific default on failure instead of a tuple: `GetString` returns `""`, `GetInt` returns `-1`, `GetBool` returns `false`. ```go package main import ( "github.com/kataras/iris/v14" ) func search(ctx iris.Context) { term := ctx.Params().GetString("term") // "" when missing page := ctx.Query().GetInt("page") // -1 when missing or invalid if page <= 0 { page = 1 } ctx.SetHeader("X-Page-Size", "20") // response header ctx.JSON(iris.Map{"term": term, "page": page}) } func main() { app := iris.New() app.Get("/search/{term}", search) app.Listen(":8080") } ``` #### Request-scoped values Middleware communicates with later handlers through `ctx.Values()`, a per-request key-value store, freed together with the context: ```go func requireShelf(ctx iris.Context) { shelf := loadShelf(ctx) // ... 404 and return when not found ctx.Values().Set("shelf", shelf) ctx.Next() } func listBooks(ctx iris.Context) { // Type-safe retrieval with the generic method. shelf, ok := ctx.ValueAs[*Shelf]("shelf") if !ok { // The route is not behind requireShelf: a wiring bug, not a client one. errors.Internal.Log(ctx, "shelf value missing from the request store") return } ctx.List(shelf.Books, nil) } ``` Reading request bodies (`ReadJSON`, `ReadForm`, and the `ctx.BindJSON[T]()` method with automatic validation) is covered in [Chapter 5](https://iris-go.com/docs/reading-requests/); response rendering in [Chapter 6](https://iris-go.com/docs/response-handling/). ### The Router The router has two lives. At design time it is a **builder**: you register routes, groups, and middleware on it. At build time (`app.Build`, called by `Listen`/`Run`) it compiles everything into a **mux**: the internal `ServeMux` that performs the actual matching of method, subdomain, and path for every request. Registration cost therefore never shows up at serve time. ("Mux" is short for multiplexer: the single component every request enters, which fans each one out to the correct handler chain. Go's standard library uses the same word for its `http.ServeMux`.) #### Routes Routes are registered with HTTP-verb methods (`Get`, `Post`, `Put`, `Delete`, `Patch`, `Head`, `Options`), with `Handle` for explicit or multiple methods, with `Any` for all methods, or with `Resource` for `GET`+`HEAD`: ```go app.Get("/about", about) app.Post("/contact", contact) app.Handle("GET,POST", "/form", form) ``` Path segments can be dynamic, typed parameters called macros: ```go app.Get("/users/{id:uuid}", getUser) // validated UUID app.Get("/posts/{page:int}", listPosts) // integer app.Get("/files/{p:path}", serveTree) // wildcard, may contain / app.Get("/tags/{name}", tagged) // untyped = string // Macro functions refine a type: app.Get("/products/{id:int min(1)}", product) ``` A parameter that fails its type check does not match the route; in builder-built applications a malformed typed parameter renders a `400 INVALID_ARGUMENT` response. The full macro catalog and custom macro registration live in [Chapter 4](https://iris-go.com/docs/routing-controllers/). #### Groups: Router and Child `Router(relativePath, ...)` creates a child router mounted under a path prefix. A child inherits its parent's middleware, context options, and dependency container; new routes it registers land in the one shared route registry. Three call styles cover most needs: ```go // 1. Plain group, optionally with middleware handlers. api := app.Router("/api/v1") api.Get("/health", health) users := api.Router("/users") // nested: /api/v1/users users.Get("/", listUsers) users.Get("/{id:uuid}", getUser) // 2. Function form: configure the group in one place. app.Router("/admin", func(r iris.Router) { r.Use(requireAdmin) r.Get("/stats", stats) }) // 3. Controller form: a constructor resolved by dependency injection // (see the last section of this chapter). app.Router("/books", NewBooksAPI) ``` `Child(relativePath, handlers...)` is the underlying primitive that clones the builder for a sub-path; `Router` builds on it and additionally accepts functions and controllers. Subdomain groups exist too: `app.Subdomain("admin")` returns a router whose routes match only `admin.` requests. #### When nothing matches If the mux finds no route, the request runs the no-route chain (default: `404 Not Found`). If the path exists but the method does not, the request runs the no-method chain (default: `405 Method Not Allowed`), but only when you have asked for it. `Router.FireMethodNotAllowed` defaults to false, and until you set it a wrong-method request lands in the no-route chain like any other miss. Both chains are replaceable: ```go app.HandleNoRoute(func(ctx iris.Context) { errors.NotFound.Message(ctx, "no route matches this path") }) app.HandleNoMethod(func(ctx iris.Context) { errors.MethodNotAllowed.Message(ctx, "this path does not accept that method") }) ``` Those defaults are the plain status text on an `iris.New()` application. An application built with `iris.NewBuilder()` fills both chains with `errors.Handler` from `rest/errors`, so a miss answers a JSON `NOT_FOUND` or `METHOD_NOT_ALLOWED` instead. It fills them when the application is built and only when they are empty, so on a builder application your own handlers have to be registered from a `Configurator` passed to `iris.NewBuilder`, or on the unbuilt value `iris.Default()` returns; a `HandleNoRoute` called on the built application does not replace the JSON answer. Structured error responses via the `rest/errors` package are the topic of [Chapter 9](https://iris-go.com/docs/error-handling/). ### Handlers and the Middleware Pipeline A handler is any `func(ctx iris.Context)`, the `iris.Handler` type. There is exactly one signature; middleware and final handlers are the same kind of function, distinguished only by position in the chain. A handler either finishes the response or calls `ctx.Next()` to hand over to the next handler. Code after `ctx.Next()` runs on the way back out, which is how timing, logging, and cleanup middleware work: ```go package main import ( "time" "github.com/kataras/iris/v14" "github.com/kataras/iris/v14/rest/errors" ) func timing(ctx iris.Context) { start := time.Now() ctx.Next() // run the rest of the chain ctx.Application().Logger().Debugf("%s %s took %s", ctx.Method(), ctx.Path(), time.Since(start)) } func requireToken(ctx iris.Context) { if ctx.Header("Authorization") == "" { errors.Unauthenticated.Message(ctx, "missing Authorization header") return // no Next: the chain ends here } ctx.Next() } func main() { app := iris.New() app.Use(timing) app.Get("/private", requireToken, func(ctx iris.Context) { ctx.Text("secret") }) app.Listen(":8080") } ``` ``` request --> timing --> requireToken --> handler | response <-- timing <------------------ (return) ``` Not calling `ctx.Next()` ends the chain quietly; `ctx.StopExecution()` (and the `StopWith*` shortcuts) additionally marks the context stopped so that any later `Next` calls are ignored. `ctx.IsStopped()` reports that state. #### Use, UseRouter, UseRelevant Where you attach middleware determines which requests it sees. The three registration methods, verified against `core/router/router_builder.go`: | Method | On the root app | On a child router | | ------------- | ------------------------------------------------ | -------------------------------------------------------- | | `UseRouter` | Before routing: every request, including 404/405 | Same, guarded by the child's path prefix (and subdomain) | | `Use` | All matched routes, plus the 404 and 405 chains | Matched routes of that child and its children only | | `UseRelevant` | Matched routes only, never 404/405 | Same as `Use` | The root-versus-child difference in `Use` is the one that surprises people: `app.Use(m)` also runs `m` for requests that match no route at all, because the root's `Use` handlers are prepended to the no-route and no-method chains. On a child router, `Use` delegates to `UseRelevant`, so it affects only routes registered under that child. When you want root middleware that skips 404s, say so explicitly with `app.UseRelevant(m)`. `UseRouter` runs even earlier: before the mux looks at the path. That makes it the right place for access logging, request IDs, rate limiting, and anything that must observe every request. From a child, `UseRouter` still registers globally but wraps the handler in a prefix check, so it fires only for requests under that child's path. Two more variants exist: `UseOnce` and `UseRelevantOnce` insert a handler or replace an already-registered instance of it, which keeps middleware idempotent when setup code may run twice. And below all of this, `app.WrapRouter` wraps the entire router at the `net/http` level for integrations that need raw `http.Handler` semantics. Order matters: a route's chain is assembled when the route is registered, so register middleware before the routes it should affect. ### The Request Lifecycle From accepted connection to released context: \[Diagram: A request passes the router wrappers, borrows a Context from the pool, runs the UseRouter chain into the mux, and either branches to a redirect or the 404 and 405 chains or enters the matched route's frozen chain before the Context is reset and returned to the pool.] Step 3 and step 8 are the pooling boundary. Between them the `Context` is exclusively yours; outside them it does not exist from your code's point of view. Step 5 is pure matching against the compiled mux: the trie compares segment count and static segments, and a typed parameter such as `{id:int}` is *not* checked here. That check is a handler the router prepends to the matched route's own chain, which is why it appears as handler 0 of step 6. No route's handlers run before the router has decided, which is what makes the `UseRouter` versus `Use` distinction meaningful. Graceful shutdown belongs to the same lifecycle at application scale: `app.Shutdown(ctx)` stops the server and runs everything registered with `app.AddCloser`. Deployment patterns around it are covered in [Chapter 19](https://iris-go.com/docs/deployment-devops/). ### Where Dependency Injection Sits Handlers keep the single `func(ctx iris.Context)` signature; dependencies do not appear as handler parameters. Instead, constructors declare what they need, and the router resolves those constructors from registered services when routes are set up: at startup, never per request. The application-level primitives: ```go package main import ( "log" "github.com/kataras/iris/v14" ) type Greeter struct{ Prefix string } func main() { app := iris.New() if err := app.RegisterDependency(&Greeter{Prefix: "Hello, "}); err != nil { log.Fatal(err) } // HandlerFunc resolves the function's parameters from the // registered dependencies and returns a plain iris.Handler. app.Get("/{name}", app.HandlerFunc(func(g *Greeter) iris.Handler { return func(ctx iris.Context) { ctx.Text(g.Prefix + ctx.Params().GetString("name")) } })) app.Listen(":8080") } ``` The same mechanism powers the controller style you saw in the router section: `app.Router("/books", NewBooksAPI)` calls the constructor with arguments resolved from the registered services, then invokes the returned struct's `Routing(r iris.Router)` method once to register its routes: ```go type BooksAPI struct { svc *library.Service } func NewBooksAPI(svc *library.Service) *BooksAPI { return &BooksAPI{svc: svc} } func (api *BooksAPI) Routing(r iris.Router) { r.Get("/", api.list) r.Get("/{id:uuid}", api.get) } ``` In builder-based applications the services are registered through the `Services(...)` step of the `iris.NewBuilder` chain, and any service implementing `Init(ctx context.Context) error` is initialized once inside `Build()`, after injection. The full resolution rules (constructors, struct field binding, per-router containers) are the subject of [Chapter 8](https://iris-go.com/docs/dependency-injection/). ### Summary - The **Application** (`iris.go`) owns lifecycle: build, serve, shutdown. Its routing API comes from the embedded root `*router.RouterBuilder`, and after `Build` it is a standard `http.Handler`. - The **Context** is pooled per request and implements `context.Context`, so it flows into services and database calls. Its methods split into request reading, response writing, and flow control; `Header` reads the request while `SetHeader`/`ResponseHeader` handle the response. - The **Router** is a builder at design time and a compiled mux at serve time. Groups come from `Router`/`Child`, unmatched requests run the `HandleNoRoute`/`HandleNoMethod` chains. - **Middleware** placement is semantic: `UseRouter` runs before routing for every request, root `Use` covers all routes plus 404/405, child `Use` (and `UseRelevant`) covers matched routes only. - **Dependency injection** happens at startup: constructors such as `NewBooksAPI(svc)` are resolved from registered services, and handlers keep their plain signature. With the model in place, the next chapter introduces the recommended way to assemble production applications: the compile-time checked builder chain. ### Further Reading - [MDN: HTTP request methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods): what each verb means and which are safe to repeat. - [MDN: HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status): the full status-code catalog with plain-English meanings. - [MDN: HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers): reference for the metadata lines on both sides of the exchange. - [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110): the authoritative specification behind methods, headers and status codes. - [net/http (pkg.go.dev)](https://pkg.go.dev/net/http): the standard library server Iris builds on; after `Build` the application is one of its `http.Handler`s. - [sync.Pool (pkg.go.dev)](https://pkg.go.dev/sync#Pool): the Go primitive behind context pooling. --- ## 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. --- ## 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. --- ## Chapter 5: Reading Requests Every handler receives an `iris.Context`, and half of its surface exists to answer one question: what did the client send? This chapter covers that half: request metadata, typed path and query accessors, body decoding with the recommended `ctx.BindJSON` path and the lower-level `ctx.Read*` family, pagination input, forms and file uploads, cookies, body size limits, and the application-level validation hook. Writing responses is the subject of [Chapter 6](https://iris-go.com/docs/response-handling/). ### Background An HTTP request is a small, structured text message. Its first line names a method and a path: the method says what kind of action the client intends (`GET` reads, `POST` creates, `PUT` replaces, `DELETE` removes), and the path says which resource the action targets. Headers come next, then optionally a body. Data can arrive in four distinct places, and each place has a job: - **The path** identifies the resource. In `/users/42`, the `42` is a path parameter (part of the URL itself), so it is always present and always text until you convert it. - **The query string** is everything after the `?`. In `/search?q=iris&page=2`, `q` and `page` are query parameters: they refine a request (filters, sorting, page numbers) without changing which resource it targets. - **Headers** are key-value metadata about the request: who the client is (`User-Agent`), what credentials it carries (`Authorization`), what format the body is in (`Content-Type`). - **The body** is the payload itself: the JSON document to store, the form fields, the uploaded file. Only some methods carry one; a `GET` usually does not. The `Content-Type` header tells the server how to interpret the body's bytes, using a media type (also called a MIME type) such as `application/json` or `text/html`. JSON is the dominant one for APIs: a plain-text format defined by RFC 8259, with exactly six shapes: objects (`{}`), arrays (`[]`), strings, numbers, booleans and `null`. Because it is plain text, every language can produce and read it, which is why it became the common tongue between web services. HTML forms have their own two media types. A simple form posts `application/x-www-form-urlencoded`: field names and values joined into one `name=value&...` string, essentially a query string moved into the body. A form with a file input posts `multipart/form-data`: the body is split into parts separated by a boundary marker, each part carrying its own headers, so text fields and raw file bytes travel in one request. One rule shapes everything in this chapter: never trust the client. Every piece of a request (path, query, headers, body, cookies, even the declared `Content-Type`) is chosen by the sender, and the sender is not always a well-behaved browser. Anyone can send any bytes with a one-line `curl` command, so client-side checks (a form's `required` attribute, a JavaScript validator) are conveniences, not guarantees. Server-side validation is the real gate, and it is what this chapter's reading helpers build in: `ctx.BindJSON` decodes *and* validates before your logic runs, the typed accessors refuse malformed numbers, and body limits stop a client from streaming gigabytes at your decoder. The problem that disappears from your code is manual string parsing and defensive `if` ladders: handlers receive typed, validated values or never run at all. ### Request Metadata The basics (method, path, host) are one call away: ```go package main import "github.com/kataras/iris/v14" func main() { app := iris.New() app.Get("/info", info) app.Listen(":8080") } func info(ctx iris.Context) { ctx.JSON(iris.Map{ "method": ctx.Method(), // "GET" "path": ctx.Path(), // "/info" "uri": ctx.FullRequestURI(), // "http://localhost:8080/info" "scheme": ctx.Scheme(), // "http://" "host": ctx.Host(), // "localhost:8080" "hostname": ctx.Hostname(), // "localhost" "subdomain": ctx.Subdomain(), // "" unless e.g. api.example.com "ip": ctx.RemoteAddr(), }) } ``` When you need the raw `*http.Request`, `ctx.Request()` returns it. #### The client address `ctx.RemoteAddr()` returns the client IP. By default it parses only the transport-level `Request.RemoteAddr`: proxy headers such as `X-Forwarded-For` are ignored, because any client can forge them. Enable them only when your server sits behind a proxy you control: ```go app := iris.New(iris.Options{ Context: iris.ContextOptions{ RemoteAddrHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, }, }) ``` With `RemoteAddrHeaders` set, `RemoteAddr` scans the listed headers in order and returns the first public IP; candidates inside the private ranges listed in `RemoteAddrPrivateSubnets` are skipped unless you also set `RemoteAddrHeadersForce`. The related `ctx.Addr()` returns the first candidate even when it belongs to a private network. Never base security decisions solely on a client-reported address. #### Request headers `ctx.Header(name)` reads a **request** header. The response counterpart is `ctx.ResponseHeader(name)`, covered in [Chapter 6](https://iris-go.com/docs/response-handling/): ```go auth := ctx.Header("Authorization") agent := ctx.Header("User-Agent") ``` One deliberate quirk: when the client repeats a header, `ctx.Header` returns all values joined by `", "` instead of only the first (which is what `http.Header.Get` would do). Code that compares the result against a secret therefore rejects a request carrying two copies of the header rather than silently honoring the first one. Headers also bind to a struct through the `header` field tag: ```go type ClientInfo struct { UserAgent string `header:"User-Agent"` RequestID string `header:"X-Request-ID"` } var info ClientInfo if err := ctx.ReadHeader(&info); err != nil { errors.InvalidArgument.Err(ctx, err) return } ``` #### Content type of the request `ctx.ContentType()` parses the request's `Content-Type` header into a structured value; `ctx.MediaType()` returns just the media type string, and `ctx.Charset()` and `ctx.ContentLength()` complete the picture: ```go ct := ctx.ContentType() // ContentType{MediaType, Charset, Boundary, ...} mt := ctx.MediaType() // e.g. "application/json" cs := ctx.Charset() // e.g. "utf-8", "" when absent n := ctx.ContentLength() // int64, 0 when the header is missing ``` These are request-side methods. The response equivalents are `ctx.ResponseContentType()` and `ctx.ResponseMediaType()`. ### Path and Query Parameters #### Path parameters Dynamic route segments (`{id:int64}`, `{slug}`; see [Chapter 4](https://iris-go.com/docs/routing-controllers/) for the macro types) are read through `ctx.Params()`. Accessors return the value directly with a type-specific default on a missing key or failed conversion; there are no `(value, error)` tuples: ```go app.Get("/users/{id:int64}/posts/{slug}", func(ctx iris.Context) { id := ctx.Params().GetInt64("id") // int64, -1 on failure slug := ctx.Params().GetString("slug") // string, "" on failure // ... }) ``` | Accessor | Failure default | | ---------------------- | ---------------------------------------- | | `GetString` | `""` | | `GetInt`, `GetInt64` | `-1` | | `GetUint`, `GetUint64` | `0` | | `GetFloat64` | `-1` | | `GetBool` | `false` | | `GetUUID` | zero `uuid.UUID` (equal to `uuid.Nil()`) | | `GetDuration` | `0` | | `GetTime` | zero `time.Time` | | `GetIP` | zero `netip.Addr` (`IsValid()` false) | When the default is ambiguous (is `-1` a real value or a miss?), use `Lookup`, which reports presence, or `Exists`: ```go if raw, ok := ctx.Params().Lookup("id"); ok { _ = raw // the stored value, typed by the route macro. } ``` A request whose typed parameter does not match the macro (say `/users/abc` against `{id:int64}`, or a malformed `{id:uuid}`) never reaches your handler; applications built with `iris.NewBuilder` render a 400 `INVALID_ARGUMENT` ("invalid path parameter") response automatically. The context has a generic method that writes the error response for you (404 when the parameter is missing, 400 when it is not of type `T`), so the handler only handles the happy path: ```go app.Get("/bookmarks/{id:int64}", func(ctx iris.Context) { id, ok := ctx.BindParam[int64]("id") if !ok { return // error response already sent. } // ... }) ``` To bind all path parameters at once, `ctx.ReadParams(&dst)` decodes into a struct using the `param` field tag; `ctx.BindParams[S]()` does the same and writes the error response itself, returning `(S, bool)`. `T` is the type the macro stored, not the type you would like: `{id:int64}` stores an `int64`, and `{id:uuid}` stores a parsed `uuid.UUID`, so a uuid parameter is read as `ctx.BindParam[uuid.UUID]("id")` or `ctx.Params().GetUUID("id")`. Asking for `string` there answers 400, because the assertion fails. `ctx.Params().GetString("id")` still works and returns the canonical lowercase text, whatever spelling the client sent. #### Query parameters `ctx.Query()` returns a read-only, sorted view of the URL query with the same typed accessors and the same failure defaults as `Params()`: ```go // GET /search?q=iris&page=2&active=true&tags=go&tags=web q := ctx.Query().GetString("q") // "iris" page := ctx.Query().GetInt("page") // 2, -1 if missing or invalid active := ctx.Query().GetBool("active") // true, false if missing price := ctx.Query().GetFloat64("max") // -1 if missing exists := ctx.Query().Exists("q") // true val, ok := ctx.Query().Lookup("q") // "iris", true ``` A few getters parse the text into a richer type and return its zero value on a miss. `GetDuration` reads `?ttl=1h30m` as 90 minutes, `GetTime` reads an RFC 3339 timestamp such as `?since=2026-10-10T09:00:00Z`, and `GetIP` turns `?addr=10.0.0.7` into a `netip.Addr` whose `IsValid()` is false when the value is not an address: ```go ttl := ctx.Query().GetDuration("ttl") // time.Duration, 0 if missing or invalid since := ctx.Query().GetTime("since") // time.Time, zero if missing or invalid addr := ctx.Query().GetIP("addr") // netip.Addr ref := ctx.Query().GetUUID("ref") // uuid.UUID, uuid.Nil() if missing or invalid ``` Repeated keys are available in two forms: ```go tags := ctx.Query().GetSlice("tags") // ["go", "web"]: the original // []string from the query map. cols := ctx.Query().GetStringSlice("columns") // splits the joined value by // the configured separator. ``` `GetStringSlice` splits on `ContextOptions.URLParamSeparator` (default `","`), so `?columns=a,b,c` and `?columns=a&columns=b&columns=c` both produce three elements; `GetSlice` returns the raw multi-value list as the client sent it. There is no `URLParam` method family in v14. Use the `Query()` accessors or `ReadQuery`. Struct binding uses the `url` tag, and `ReadURL` combines path parameters with the query in one call: ```go type SearchFilter struct { UserID int `param:"id"` // from the path. Keyword string `url:"q"` // from the query. Tags []string `url:"tags"` Page int `url:"page"` } app.Get("/users/{id:int}/search", func(ctx iris.Context) { var filter SearchFilter if err := ctx.ReadURL(&filter); err != nil { errors.InvalidArgument.Err(ctx, err) return } ctx.JSON(filter) }) ``` `ctx.ReadQuery(&dst)` does the same for the query alone. #### Strict query readers The accessors above are lenient by design: `GetInt("page")` answers `-1` for a missing key *and* for `?page=banana`, and a filter built on them silently returns the wrong rows instead of telling the client it sent nonsense. Three readers make that a 400 instead: ```go // GET /reports?from=2026-01-01&limit=50&detailed=yes from, ok := ctx.QueryDate("from", iris.QueryRequired()) if !ok { return // 400 already written: missing, or not a date. } limit, ok := ctx.QueryInt("limit", iris.QueryDefault("25")) if !ok { return } detailed, ok := ctx.QueryBool("detailed") if !ok { return } ``` The second result is the `ok` of the `Bind*` family: `false` means the response was already written and the handler returns. A present but unparsable value is always a 400 naming the key and the shape expected, never a silent zero. An absent value is the zero value unless an option says otherwise. | Option | Effect | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `iris.QueryRequired()` | An absent or blank value is a 400 naming the key | | `iris.QueryDefault(raw)` | The raw string to parse when the key is absent, stated in the client's own wire form (`"2020-01-02"`, `"25"`, `"true"`). It wins over `QueryRequired` | | `iris.QueryZeroValues(v...)` | The placeholders older clients send for "unset" (`"0"`, `"zero"`, `"0000-00-00"`, `"null"`), each read exactly like an absent value | `QueryDate` parses `types.SimpleDate` (`2006-01-02`, or `2006-1-2`) and `QueryBool` accepts what `strconv.ParseBool` accepts plus `yes`, `no`, `on` and `off`, case-insensitively. Nothing is special-cased below the options: `?from=null` is a 400 unless `QueryZeroValues("null")` declared it, because what a raw value means is the endpoint's decision and not the parser's. An unparsable `QueryDefault` is a programmer error and panics on the first request rather than rendering as the client's fault. An endpoint that answers to more than one spelling of the same parameter states them once, separated by a space, and each is tried in the order given until one carries a value: ```go date, ok := ctx.QueryDate("date day from") // ?day=2026-01-02 answers here if !ok { return } ``` A URL query key cannot contain a space, so a key without one is a single key and reads exactly as it always did. `QueryInt` and `QueryBool` take the same. When no key carried a value the 400 names all of them; when one carried an unparsable value it names only that one, since the client did not send the others. ### Binding JSON Requests For JSON APIs the recommended entry point is the generic context method `ctx.BindJSON[T]()`. It decodes the body, runs the payload's own `Validate() error` method when the type implements it (the `iris.Validatable` interface), and on any failure writes a structured 4xx response and returns `false`. The handler simply returns. You never call `Validate` by hand. The `rest/errors/validation` package provides small field builders for those `Validate` methods: ```go package main import ( "github.com/kataras/iris/v14" "github.com/kataras/iris/v14/rest/errors/validation" ) type CreateUserInput struct { Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` } // Implementing Validate makes ctx.BindJSON run it after decoding. func (in CreateUserInput) Validate() error { return validation.Join( validation.String("name", in.Name).NotEmpty().Length(2, 100), validation.String("email", in.Email).Email(), validation.Number("age", in.Age).Positive(), ) } func createUser(ctx iris.Context) { input, ok := ctx.BindJSON[CreateUserInput]() if !ok { return // 400 already written: malformed JSON or failed Validate. } // input is decoded AND valid here. ctx.Created(input.Name, nil) // see Chapter 6 for responses. } func main() { app := iris.New() app.Post("/users", createUser) app.Listen(":8080") } ``` A failed `Validate` renders each field error individually: ```json { "http_error_code": {"canonical_name": "INVALID_ARGUMENT", "status": 400}, "message": "validation failure", "detail": "fields were invalid", "validation": [ {"field": "name", "value": "", "reason": "must not be empty, must be between 2 and 100 characters"}, {"field": "age", "value": 0, "reason": "must be positive"} ] } ``` The builders cover strings (`NotEmpty`, `Length`, `Email`, `Fullname`), numbers (`Positive`, `Negative`, `NonZero`, `InRange`) and slices, and compose with plain functions through `Func` for custom rules. `validation.Join` drops entries that passed, and returns `nil` when everything did. #### Optional bodies and variants `ctx.BindJSONOptional[T]()` behaves like `BindJSON` except that an empty request body is a success: it returns the zero value of `T` with `true` and skips validation. Use it when the body is a nice-to-have, such as an optional filter object. Two more variants accept a body that may be either a single object or an array of objects: `ctx.BindJSONOneOrMany[T]()` and `ctx.BindJSONList[T]()` (which normalizes a single object into a one-element slice). Every element is validated. #### Partial updates and the column list A `PATCH` sends the body and the list of fields it means to change. The list travels in the `columns` query key, and reading it apart from the body invites the bug the whole family exists to prevent: a typo matches no column, the update changes nothing, and the client is told it succeeded. `ctx.BindUpdate[T]()` reads both and validates them against each other: ```go func updateUser(ctx iris.Context) { id, ok := ctx.BindParam[int64]("id") if !ok { return } input, columns, ok := ctx.BindUpdate[UpdateUserInput]() if !ok { return // 400 already written: the body, or an unknown column. } updated, err := svc.UpdateUser(ctx, id, input, columns...) ctx.NoContent(updated, err) // 204, or 304 when nothing changed. } ``` The body half is `BindJSON` exactly: the same decoder, the same `RequestValidator` hook, the same `Validate` call. The columns arrive either as a repeated key (`?columns=name&columns=email`) or as one comma-separated value (`?columns=name,email`); both spellings are accepted, values are trimmed, empties dropped and duplicates removed, keeping the client's order. No columns at all means a full update and yields a nil slice. Every column is checked against the JSON member names of `T`, the `json` tag name or the Go field name where there is no tag, and an unknown one answers 400 naming the column and the names it could have been. The comparison is exact where the body decoder is case-insensitive, because a column name reaches an `UPDATE` statement: the endpoint must receive the spelling it declared rather than a normalized guess. The names that come back are still the client's, so a store that builds SQL from them maps them to its own columns and never interpolates them. ### The Context Read Family When the payload is not JSON, or when you want to handle the error yourself instead of letting the helpers respond, the context offers a `Read*` method per format. Each decodes into a pointer, using a struct tag that matches the source: | Method | Source | Struct tag | | --------------------------- | ------------------------------- | -------------- | | `ctx.ReadJSON(&v, opts...)` | JSON body | `json` | | `ctx.ReadXML(&v)` | XML body | `xml` | | `ctx.ReadYAML(&v)` | YAML body | `yaml` | | `ctx.ReadMsgPack(&v)` | MessagePack body | — | | `ctx.ReadProtobuf(msg)` | Protobuf body (`proto.Message`) | — | | `ctx.ReadForm(&v)` | URL-encoded or multipart form | `form` | | `ctx.ReadQuery(&v)` | URL query | `url` | | `ctx.ReadParams(&v)` | path parameters | `param` | | `ctx.ReadHeader(&v)` | request headers | `header` | | `ctx.ReadURL(&v)` | path parameters + query | `param`, `url` | | `ctx.ReadBody(&v)` | dispatched by content type | depends | XML and YAML are, like JSON, human-readable text formats with different syntax. MessagePack and Protobuf are binary encodings: they trade readability for smaller, faster payloads and are common in service-to-service traffic rather than browser-facing APIs. `ctx.ReadJSON` accepts optional `iris.JSONReader` options: ```go var data map[string]any err := ctx.ReadJSON(&data, iris.JSONReader{ DisallowUnknownFields: true, // reject keys with no struct field. }) ``` Decoding runs on the standard library's `encoding/json/v2` (Go 1.27). Two of its stricter rules apply to every reader: a body with duplicate object keys or invalid UTF-8 is rejected (the bind helpers render that as a 400). One rule is deliberately relaxed: member names still match struct fields case-insensitively, as deployed clients expect from the old `encoding/json`; set `JSONReader.CaseSensitive` to restore v2's strict matching for a call. The `UnmarshalOptions` field takes raw `encoding/json/v2` options, appended after the defaults so yours win: `jsontext.AllowDuplicateNames(true)`, for example, accepts duplicate members again. For large arrays, `ctx.ReadJSONStream` decodes one object at a time instead of buffering the whole body: ```go var users []User err := ctx.ReadJSONStream(func(decode iris.DecodeFunc) error { var u User if err := decode(&u); err != nil { return err } users = append(users, u) return nil }) ``` #### ReadBody: content-type dispatch `ctx.ReadBody(&v)` picks the decoder for you. For `GET` requests it reads from the URL query (falling back to the form). For other methods it switches on the request's media type: XML, YAML, url-encoded or multipart forms, JSON, Protobuf and MessagePack are dispatched to the matching `Read*` method; an unrecognized type falls back to the query (when one exists) and finally to JSON. \[Diagram: ctx.ReadBody short-circuits for raw string and byte targets, reads the query or the form for a GET, and otherwise switches on the media type, falling back to the query and then to JSON.] ```go var raw []byte if err := ctx.ReadBody(&raw); err != nil { // raw body, no decoding. errors.InvalidArgument.Err(ctx, err) return } ``` The lowest level is `ctx.Body()`, which returns the body as `[]byte`. Like any HTTP body, it can be consumed once: a later `ReadJSON` on the same request finds it empty. Empty-input behavior: `ReadForm`, `ReadQuery` and `ReadBody` return a `nil` error on empty input by default, leaving the struct at its zero value. Set `ContextOptions.FireEmptyFormError` to make them return `iris.ErrEmptyForm` instead. `ReadForm` errors on unknown client fields; ignore that class with `err != nil && !iris.IsErrPath(err)`. ### Pagination Input Returning an entire table from one endpoint does not scale: the response grows without bound and most clients display a screenful at a time. Pagination splits the list into fixed-size pages the client requests one by one; in database terms, the page number and size translate into how many rows the query fetches and how many it skips first. List endpoints usually accept `?page=&size=&details=` plus an optional JSON filter body. `ctx.BindPagination` reads both at once: ```go type BookFilter struct { Author string `json:"author"` } func listBooks(ctx iris.Context) { opts, filter, ok := ctx.BindPagination[BookFilter]() if !ok { return // error response already sent. } // opts.GetLimit(), opts.GetOffset() feed the database query. books, total, err := svc.Books(ctx, opts, filter) ctx.OK(iris.NewPage(opts, &filter, books, total), err) } ``` `iris.PageOptions` carries `Page`, `Size` and `Details`; `GetLimit` returns `Size` bounded by `pagination.MaxPageSize` (falling back to `pagination.DefaultPageSize`, 100) and `GetOffset` derives the offset from the page number. Set the two bounds once at startup, either directly or through `iris.Patches().Context().SetMaxPageSize(n)` and `SetDefaultPageSize(n)`. An empty body yields the zero filter. The filter is optional by design. A filter body on a read raises the question of which method carries it. GET has no defined body semantics, and POST says the request changes something, which it does not. HTTP QUERY (RFC 10008) is the method made for this: safe and idempotent like GET, with a body like POST, and its response is cacheable. Iris registers it with `app.Query`, so one path can answer both shapes: ```go app.Get("/books", listBooks) // ?page=&size= only. app.Query("/books", listBooks) // ?page=&size= plus a filter body. ``` Send the QUERY filter as JSON. On Go 1.27, net/http parses a url-encoded body only for POST, PUT and PATCH, so a form-encoded QUERY body never reaches `ctx.Form*`. The page types themselves (`PageOptions`, `Page[T]`, `NewPage`, `ListFilter`, `CountResponse[N]`) live in `core/pkg/pagination`, a package that imports only the standard library. `iris` and `context` alias them, so `iris.Page[T]` and `pagination.Page[T]` are the same type, and the `client` package decodes a list into that type too. `ctx.Paginated` collapses the whole flow (read options and filter, run a collector, respond with a `iris.Page[T]`) into one call: ```go import ( "context" "github.com/kataras/iris/v14" ) func listBooks(ctx iris.Context) { ctx.Paginated(func(reqCtx context.Context, opts iris.PageOptions, filter BookFilter) ([]Book, int64, error) { return svc.Books(reqCtx, opts, filter) }) } ``` The response shape (`current_page`, `page_size`, `total_pages`, `total_items`, `has_next_page`, `filter`, `items`) is covered in [Chapter 6](https://iris-go.com/docs/response-handling/). #### The shared filter, and who chooses the sort Most list filters carry the same four or five members before they carry anything of their own. `iris.ListFilter` is that half, ready to embed: ```go type BookFilter struct { iris.ListFilter Author string `json:"author,omitzero" url:"author"` } ``` It fields `Term`, `Names`, `Substring`, `Sort` and `SortDescending`, and carries both tag sets on purpose: the `json` names for a `QUERY` or `POST` list body and the `url` names for a `GET` list route read through `ReadQuery`. `f.SortColumn(allowed...)` returns the requested column or an error wrapping `iris.ErrSortNotAllowed`, and `f.SortOrder()` returns `"ASC"` or `"DESC"`. A sort column reaches an `ORDER BY` clause, so which columns a list offers belongs to the endpoint and never to the request. Two options say so declaratively, and both answer 400 before the collector runs: ```go func listBooks(ctx iris.Context) { ctx.Paginated(func(reqCtx context.Context, opts iris.PageOptions, filter BookFilter) ([]Book, int64, error) { return svc.Books(reqCtx, opts, filter) }, iris.AllowSort("title", "published_at")) } ``` `iris.AllowSort(columns...)` limits the column a client may name, and the rejection names both the column and the allowed ones. `iris.DenySort(message)` refuses any sort at all, for a list whose order the query itself fixes. Both work on `ctx.BindPagination` too, and both read the decoded filter through its `SortRequest` method (the `pagination.SortRequester` interface), which `ListFilter` provides, so a filter without one is simply unaffected. Sorting is often a permission rather than a shape. A list a client may read but not order should answer 403, not 400, and any error code carries the rejection: ```go list, filter, ok := ctx.BindPagination[BookFilter]( errors.PermissionDenied.DenySort("sorting is not available on this plan")) if !ok { return } ``` `errors.ErrorCodeName.DenySort`, from `rest/errors`, is `iris.DenySort` answered with that code and is otherwise identical. A collector may take its filter by pointer, which is the shape a repository method already has, so it can be passed as a bare method value: ```go ctx.Paginated(svc.ListBooksPaginated) // (context.Context, iris.PageOptions, *BookFilter) ([]Book, int64, error) ``` Such a collector never receives nil. A request with no body decodes to the filter's zero value, so the repository reads an empty filter and the sort policy still has a real `SortRequest` to read. ### Forms and File Uploads #### Form values HTML form submissions (`application/x-www-form-urlencoded` or `multipart/form-data`) bind through the `form` tag: ```go type ContactForm struct { Name string `form:"name"` Email string `form:"email"` Message string `form:"message"` } func contact(ctx iris.Context) { var form ContactForm if err := ctx.ReadForm(&form); err != nil && !iris.IsErrPath(err) { errors.InvalidArgument.Err(ctx, err) return } // ... } ``` Individual values are available through `ctx.Form()` (query + body values) and `ctx.PostForm()` (body values only), which return the same read-only entries view as `ctx.Query()`: `GetString`, `GetInt`, `GetSlice` and friends all apply. Multipart form parsing buffers up to `ContextOptions.PostMaxMemory` in memory (default 32 MB, spillover goes to temporary files). Raise or lower it through `iris.Options{Context: iris.ContextOptions{...}}`. #### File uploads An uploaded file's name is client input like any other. A malicious client can submit a name such as `../../etc/crontab`, hoping the server joins it to the upload directory and writes outside it, an attack known as path traversal. Sanitizing the name before it touches the filesystem is therefore not optional, and the helper below does it for you. `ctx.UploadFormFiles` is the batteries-included path: it saves every uploaded file into a destination directory, sanitizes file names against an allowlist pattern and blocks path-traversal attempts (unsafe names are silently skipped). The optional callback filters or renames files before saving: ```go import ( "mime/multipart" "github.com/kataras/iris/v14" "github.com/kataras/iris/v14/rest/errors" ) func upload(ctx iris.Context) { files, total, err := ctx.UploadFormFiles("./uploads", func(ctx iris.Context, file *multipart.FileHeader) bool { return file.Size <= 8*iris.MB // reject anything larger. }) if err != nil { errors.InvalidArgument.Err(ctx, err) return } ctx.JSON(iris.Map{"uploaded": len(files), "bytes": total}) } ``` For manual control, `ctx.FormFile(key)` returns the first uploaded file for a form key and `ctx.FormFiles(key, ...)` all of them; pair either with `ctx.SaveFormFile(header, dest)` to write to disk. When building the destination path yourself, sanitize the client-supplied name first. `context.SafeFilename` implements the same checks `UploadFormFiles` uses: it strips every directory component, holds the result to an allowlist of characters, rejects the reserved Windows device names (`NUL`, `COM1`, `CON.txt` and the rest, which open a device rather than a file), and resolves the destination directory before checking that the final path is inside it. The upload endpoints are a natural place for the body limits described below. ### Cookies A cookie is a small named value the server asks the browser to store; the browser then attaches it to every later request to the same site. That echo is what gives stateless HTTP a memory: it is how a login survives from one request to the next. Two readers cover the request side: ```go // Just the value; "" when the cookie is missing. session := ctx.CookieValue("session_id") // The full *http.Cookie; http.ErrNoCookie when missing. cookie, err := ctx.Cookie("session_id") if err == nil { _ = cookie.Value } ``` `CookieValue` URL-unescapes the value for you. Setting, upserting and removing cookies happens on the response and is covered in [Chapter 6](https://iris-go.com/docs/response-handling/). ### Body Size Limits Unbounded request bodies are a denial-of-service vector: a client can stream gigabytes into `ReadJSON` or hide them behind a compressed body. Iris offers an application-wide cap and a per-route middleware. **Application-wide**: `ContextOptions.BodyMaxMemory` limits the total bytes read by `Body`, `ReadJSON`, `ReadJSONStream` and `ReadMultipartRelated`. That cap therefore also covers everything built on them, including `ReadXML`, `ReadYAML`, `ReadBody` and `ctx.BindJSON`: ```go app := iris.New(iris.Options{ Context: iris.ContextOptions{ BodyMaxMemory: 4 * iris.MB, }, }) ``` The default `0` means no limit. Reading past the cap fails with `context.ErrRequestEntityTooLarge` (package `github.com/kataras/iris/v14/context`), which the `rest/errors` package renders as a 413 `TOO_LARGE` response. **Per route or group**: the `bodylimit` middleware applies the same enforcement wherever it is registered, typically on upload routes: ```go import "github.com/kataras/iris/v14/middleware/bodylimit" media := app.Router("/media", bodylimit.New(20*iris.MB)) media.Post("/upload", upload) ``` It rejects requests up front when the declared `Content-Length` already exceeds the limit, and wraps the body reader to catch chunked or compressed bodies that only reveal their size while being read. `PostMaxMemory` is a different knob: it bounds how much of a multipart form is buffered in memory during parsing, not how large the body may be. Use both where appropriate. ### The RequestValidator Hook The per-type `Validate() error` method with `ctx.BindJSON` is the recommended validation path. For teams standardizing on a tag-based validation library instead, the context exposes one application-level hook: `ContextOptions.RequestValidator`, any value implementing ```go type Validator interface { Struct(any) error } ``` It defaults to `nil`, so `validate:"..."` struct tags do nothing on their own. When set, every decoding `Read*` method (`ReadJSON`, `ReadXML`, `ReadYAML`, `ReadMsgPack`, `ReadForm`, `ReadQuery`, `ReadParams`, `ReadURL`, `ReadHeader`, `ReadBody`) calls it after a successful decode and returns its error. Wiring a third-party library is a three-line adapter: ```go import ( "github.com/go-playground/validator/v10" // third-party. "github.com/kataras/iris/v14" ) type structValidator struct{ v *validator.Validate } func (s structValidator) Struct(ptr any) error { return s.v.Struct(ptr) } func main() { app := iris.New(iris.Options{ Context: iris.ContextOptions{ RequestValidator: structValidator{validator.New()}, }, }) // ... } ``` The two mechanisms compose: `ctx.BindJSON` surfaces `RequestValidator` failures through the same structured 400 responses it uses for a failed `Validate()` method, so handlers keep the single `if !ok { return }` guard either way. ### Summary - `ctx.Method()`, `ctx.Path()`, `ctx.Host()` and friends expose request metadata; `ctx.RemoteAddr()` trusts proxy headers only when `RemoteAddrHeaders` is configured. - `ctx.Header(name)` reads a request header (repeated values joined by `", "`); `ctx.ContentType()` / `ctx.MediaType()` describe the request body's declared type. - `ctx.Params()` and `ctx.Query()` share typed accessors with fixed failure defaults (`GetString` → `""`, `GetInt` → `-1`); `Lookup` and `Exists` distinguish missing from zero; `GetSlice` returns raw multi-values. - `ctx.BindJSON[T]()` is the recommended body reader: it decodes, runs the type's `Validate() error` automatically, and writes structured 4xx responses. `ctx.BindJSONOptional[T]()` accepts an empty body. - The `ctx.Read*` family covers JSON, XML, YAML, MessagePack, Protobuf, forms, query, params and headers; `ctx.ReadBody` dispatches by content type and fills `*string`/`*[]byte` with the raw body. - `ctx.BindPagination` and `ctx.Paginated` turn `?page=&size=` plus an optional filter body into a complete list endpoint. - `ctx.UploadFormFiles` saves multipart uploads with sanitized names; `PostMaxMemory` bounds form buffering, `BodyMaxMemory` and the `bodylimit` middleware cap body size (413 on overflow). - `ContextOptions.RequestValidator` is the app-wide hook for tag-based validation libraries; without it, `validate` tags are inert. ### Further Reading - [MDN: HTTP messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages). The anatomy of requests and responses: start line, headers, body. - [MDN: MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types): how media types like `application/json` and `multipart/form-data` work. - [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110): the authoritative definition of methods, headers and message bodies. - [RFC 8259: JSON](https://www.rfc-editor.org/rfc/rfc8259): the complete JSON grammar; shorter than most tutorials about it. - [OWASP: Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html): server-side validation strategy, allowlists and common pitfalls. - [OWASP: File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html): the risks behind upload endpoints and the checks that mitigate them. --- ## Chapter 6: Response Handling Iris gives you two layers for writing responses. The response helpers on the context are the recommended surface for JSON APIs: each takes the value and the error your service returned and decides what to send, including the error response when something went wrong. Underneath sit the `Context` writers (JSON, XML, YAML, text, files, streams) for the cases where you need full control over the bytes on the wire. This chapter covers both layers, plus status codes, response headers, redirects, streaming, Server-Sent Events, content negotiation and the handler that runs when a response write itself fails. ### Background An HTTP response mirrors the request's shape. Its first line (the status line) carries a three-digit status code and a short reason phrase ("200 OK", "404 Not Found"). Headers follow, describing what the body is (`Content-Type`), how large it is (`Content-Length`) and how it may be cached (`Cache-Control`). The body (HTML, JSON, file bytes) comes last, and some responses legitimately have none. Status codes are grouped by their first digit, and the class alone tells a client how to react: - **2xx**: success. 200 carries a body, 201 announces a created resource, 204 succeeds with nothing to return. - **3xx**: redirection. The resource is elsewhere, or (304) the copy the client already holds is still fresh. - **4xx**: the client made a mistake. Malformed input (400), missing credentials (401), no permission (403), no such resource (404). - **5xx**: the server failed. The request itself may have been perfectly valid. Treat the code you send as an API contract, because software you do not control acts on it. Browsers follow 3xx redirects on their own. Proxies and CDNs cache responses based on the code and headers: a 200 may be stored and replayed, a 500 will not be. HTTP clients and load balancers retry on some codes and give up on others. Monitoring counts 5xx responses to decide whether to page someone. Send a 200 with an error message hidden in the body and every one of those layers draws the wrong conclusion. Content negotiation is the name for "one URL, several formats". The client's `Accept` request header lists the representations it can handle (`application/json`, `text/xml`, ...) with optional preference weights; the server compares that list against what it can produce and picks the best match, or answers 406 Not Acceptable when there is none. The same mechanism extends to compression (`Accept-Encoding`) and language (`Accept-Language`). What this chapter's helpers remove from your code is the ceremony around those rules. Picking the right status for success and failure, shaping a consistent error body, remembering that headers must precede the first body byte. The response helpers encode those decisions once, so a typical handler ends with a single line instead of a block of status bookkeeping. ### The Response Helpers In a typical handler the response is the last line: call the service, then hand the result and the error to one helper. Every helper follows the same contract: ```go ok := ctx.Helper(value, err) // ok == false: an error response was written ``` If `err` is non-nil, the helper routes it through the central error mapping of `rest/errors` (see [Chapter 9](https://iris-go.com/docs/error-handling/)) and a structured error response is written. Your handler never branches on the error. Otherwise the helper renders the success response. The returned `bool` is `true` on success; you only check it when more work follows the write. ```go package main import ( "github.com/kataras/iris/v14" ) type ArticlesAPI struct { svc *ArticleService // your application's service layer. } func NewArticlesAPI(svc *ArticleService) *ArticlesAPI { return &ArticlesAPI{svc: svc} } func (api *ArticlesAPI) Routing(r iris.Router) { r.Get("/", api.list) r.Post("/", api.create) r.Get("/{id:int64}", api.get) r.Put("/{id:int64}", api.update) r.Delete("/{id:int64}", api.delete) } func (api *ArticlesAPI) get(ctx iris.Context) { id, ok := ctx.BindParam[int64]("id") if !ok { return // error response already written. } article, err := api.svc.ByID(ctx, id) ctx.OK(article, err) } func (api *ArticlesAPI) create(ctx iris.Context) { input, ok := ctx.BindJSON[ArticleInput]() if !ok { return // 400 already written: malformed JSON or failed Validate. } id, err := api.svc.Create(ctx, input) ctx.Created(id, err) // 201 with {"id": ...}. } func (api *ArticlesAPI) update(ctx iris.Context) { id, ok := ctx.BindParam[int64]("id") if !ok { return } input, ok := ctx.BindJSON[ArticleInput]() if !ok { return } updated, err := api.svc.Update(ctx, id, input) ctx.NoContent(updated, err) // 204, or 304 if nothing changed. } func (api *ArticlesAPI) delete(ctx iris.Context) { id, ok := ctx.BindParam[int64]("id") if !ok { return } deleted, err := api.svc.Delete(ctx, id) ctx.NoContent(deleted, err) } ``` The full set of response helpers: | Helper | Success status | Renders | | ----------------------------------- | ---------------------- | ------------------------------------------------------------------------ | | `ctx.OK(resp, err)` | 200 (status untouched) | `resp` as JSON; a nil `resp` sends nothing | | `ctx.Created(resp, err)` | 201 | id scalars wrapped as `{"id": ...}`, other values as-is | | `ctx.NoContent(modified, err)` | 204 or 304 | empty body: 204 when `modified` is true, 304 otherwise | | `ctx.Count(rowsAffected, err)` | 200 | `{"count": N}`; zero is a valid answer, a negative count renders as zero | | `ctx.List(items, err, tolerate...)` | 200 | `items` as a JSON array; empty and nil both render `[]` | | `ctx.JSONOrError(resp)` | current | `resp` as JSON, with encode failures handled for you | | `ctx.Paginated(collect)` | 200 | the standard page envelope | Two of the family are generic: `ctx.Count` infers the count's numeric type from its argument, and `ctx.Paginated` infers filter, item and count types from the collector you pass. Both follow the same contract as the others: the error routes through the central mapping, and `false` means a response was written. `ctx.JSONOrError` is the low-level member of the family: it has no error argument, it simply calls `ctx.JSON` and converts an encoding failure into a proper error response instead of a broken body. Reach for it when the status code or the value needs preparing first. #### What Created renders `ctx.Created` recognizes identifier values and wraps them in a JSON object, so a create handler can return the new id directly from the service: ```go type IDType interface { string | int | int64 | uint64 | uuid.UUID } type IDPayload[T IDType] struct { ID T `json:"id"` } ``` A `string`, `int`, `int64`, `uint64` or `uuid.UUID` result becomes `{"id": ...}` (a UUID is rendered as its string form). The `uuid.UUID` here is the standard library's `uuid` package type (Go 1.27), the same one route macros and the book's examples use. A `[]string` whose elements are UUIDs becomes a JSON array of `{"id": ...}` objects, useful for bulk inserts. Any other value, such as the full created entity, is sent as-is. A nil value sends the bare 201. #### What List renders A list endpoint that is not paginated has two shapes to get right, and hand-written versions get both wrong. A nil slice marshals as `null` rather than `[]`, which every client has to special-case; and a store that reports "not found" for an empty result set turns an ordinary empty list into a 404. `ctx.List` answers both: ```go func (api *ArticlesAPI) list(ctx iris.Context) { articles, err := api.svc.All(ctx) ctx.List(articles, err, sql.ErrNoRows) // 200 [] when there is nothing. } ``` A nil or empty slice renders `[]`, never `null`, so the client can always iterate the answer. An error matching one of the tolerated sentinels (`errors.Is`, so a wrapped one matches) renders `[]` as well: a repository's not-found for an empty set is not a failure of the list endpoint. Any other error goes through the central error mapping and `List` reports `false`, exactly like the rest of the family. Passing something that is neither a slice nor an array is a programmer error and panics naming the type, rather than rendering a JSON object where the client expects an array. Reach for `ctx.Paginated` instead as soon as the list can grow: `List` sends the whole set. #### Pagination in one call `ctx.Paginated` runs the complete listing flow: it reads `?page=` and `?size=` from the query and an optional JSON filter object from the request body, calls your collector with the parsed options, and renders the page envelope. ```go import ( stdContext "context" "github.com/kataras/iris/v14" ) func (api *ArticlesAPI) list(ctx iris.Context) { ctx.Paginated(func(reqCtx stdContext.Context, opts iris.PageOptions, filter ArticleFilter) ([]Article, int64, error) { return api.svc.List(reqCtx, opts, filter) }) } ``` The collector returns the requested window of items and the total row count; everything else (limit/offset math, the envelope, error handling) is done for you. The response shape: ```json { "current_page": 1, "page_size": 20, "total_pages": 3, "total_items": 42, "has_next_page": true, "filter": null, "items": [{"id": 1, "title": "First"}] } ``` Request reading (`ctx.BindJSON`, validation, `ctx.BindParam`) is covered in [Chapter 5](https://iris-go.com/docs/reading-requests/). ### Context Writers When a helper does not fit (a non-JSON format, a custom shape, a template), drop down to the `Context` writers. Each one sets the matching Content-Type and writes the body: | Method | Content-Type | Options value | | ---------------------- | ------------------------ | --------------- | | `ctx.JSON(v, ...)` | application/json | `iris.JSON` | | `ctx.JSONP(v, ...)` | text/javascript | `iris.JSONP` | | `ctx.XML(v, ...)` | text/xml | `iris.XML` | | `ctx.YAML(v)` | application/x-yaml | — | | `ctx.TextYAML(v)` | text/yaml | — | | `ctx.Markdown(b, ...)` | text/html | `iris.Markdown` | | `ctx.Text(s)` | text/plain | — | | `ctx.HTML(s)` | text/html | — | | `ctx.Binary(b)` | application/octet-stream | — | `JSON`, `JSONP`, `XML` and `Markdown` accept an optional options value as their last argument; the rest take none. Every one of them returns a single `error`, nil when the body was written. JSONP is a legacy format: the JSON is wrapped in a JavaScript function call so that browsers predating cross-site request rules could load it through a `