Iris has no plugin system, and it does not need one: its extension model is
plain Go. Every advanced capability in this chapter is an ordinary,
verifiable API: generic controllers that generate REST routes from your
types, typed contexts that replace iris.Context in your handler
signatures, global patches that swap framework internals such as the JSON
encoder, custom path parameter types, raw router wrappers, a
multi-application registry, and interop with net/http. This final
chapter walks through each extension point and closes with the built-in
controllers that ship with the framework.
Background
What an extension point is
An extension point is a place where a framework lets you replace or
extend its own machinery rather than merely use it: swap the JSON
encoder behind ctx.JSON, teach the router a new path parameter type,
run code before routing even begins. Reaching for one is justified in
two situations: you keep repeating the same workaround in many
handlers because a framework default does not fit your case, or you are
integrating tooling that predates or lives outside Iris. Be clear about
the flip side: most applications never need this chapter. The first
twenty-two chapters cover everyday application work; if you are reading
the book front to back, it is enough to remember what exists here and
return when a concrete need appears.
What generics buy the MVC controllers
Go generics (type parameters, added to the language in Go 1.18) let one piece of code be written against placeholder types and specialized by the compiler for the concrete types you supply. The first section uses them as a code generator: from five typed method signatures the framework derives routes, request decoding, response encoding and error handling. The alternative in older frameworks is reflection (inspecting types while the program runs), which costs performance and turns a wrong method shape into a runtime surprise. With generics the same wiring is checked when you compile: a controller whose signatures do not match the contract fails the build, not a production request.
What process-global means
A process is one running instance of your compiled program, the
unit the operating system starts and stops. State that is
process-global is shared by everything inside that binary: every Iris
application in it, every library it imports, every test in the same
go test run. Two features below are process-global (iris.Patches()
and the application registry behind the apps package), and such state
demands caution for the reason globals always do: whoever writes last
wins, and every reader everywhere is affected. The rules that keep it
safe appear in their sections; the short version is mutate once, at
startup, from main, never from a library.
Generic MVC Controllers
The routing controllers of Chapter 4 give you
full control: a Routing(r iris.Router) method and any routes you like. The
controller/mvc package sits one level higher. You implement five typed
methods on a struct and the framework generates the REST routes, decodes
requests, encodes responses, and wires the error pipeline of
Chapter 9, all without reflection at serve time.
The Controller Interface
// github.com/kataras/iris/v14/controller/mvc
type Controller[Context handler.ContextType, Request any, Response any,
ID iris.IDType, Filter any, Total iris.CountType] interface {
Create(Context, Request) (Response, error)
Get(Context, ID) (Response, error)
Update(Context, ID, Request, ...string) (*Response, bool, error)
Delete(Context, ID) (bool, error)
List(ctx Context, filter Filter, limit, offset int) (
[]Response, Total, error)
}The six type parameters, honestly:
| Parameter | Constraint | Meaning |
|---|---|---|
Context | handler.ContextType | iris.Context or a standard context.Context; your methods may accept either |
Request | any | The input payload, decoded from the request |
Response | any | The output model; also names the generated path parameter |
ID | iris.IDType | string | int | int64 | uint64 | uuid.UUID |
Filter | any | The List filter payload |
Total | iris.CountType | Any integer or float type for the list total |
At the call site you write only the controller type; the compiler infers the
rest from your method signatures. iris.CountType is defined in
core/pkg/pagination as constraints.Number, and it and the other numeric
bounds the framework’s generics use come from core/pkg/constraints, a
six-interface package that mirrors the subset of
golang.org/x/exp/constraints Iris needed, so the module carries no
dependency on the experimental repository.
A Complete Controller
package main
import (
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/controller/mvc"
)
type Store struct {
ID string `json:"id"`
Name string `json:"name"`
Location string `json:"location"`
}
type StoreFilter struct {
Location string `json:"location"`
}
type StoreService struct{ /* database handle */ }
func NewStoreService() *StoreService { return &StoreService{} }
type StoreController struct {
Service *StoreService // injected by the DI container.
}
func (c *StoreController) Create(ctx iris.Context, in Store) (Store, error) {
in.ID = "generated"
return in, nil
}
func (c *StoreController) Get(ctx iris.Context, id string) (Store, error) {
return Store{ID: id, Name: "Bookstore", Location: "New York"}, nil
}
func (c *StoreController) Update(ctx iris.Context, id string, in Store,
columns ...string) (*Store, bool, error) {
return &in, true, nil
}
func (c *StoreController) Delete(ctx iris.Context, id string) (bool, error) {
return true, nil
}
func (c *StoreController) List(ctx iris.Context, filter StoreFilter,
limit, offset int) ([]Store, int, error) {
stores := []Store{{ID: "1", Name: "Bookstore", Location: "New York"}}
return stores, len(stores), nil
}
func main() {
app := iris.New()
app.RegisterDependency(NewStoreService)
app.Router("/stores", mvc.UseController[*StoreController]())
app.Listen(":8080")
}UseController resolves the controller through the dependency injection
container of Chapter 8, or constructs it and
binds its exported fields, then registers these routes:
| Method, path | Controller method | Success response |
|---|---|---|
POST /stores | Create | 201, JSON body |
GET /stores/{store_id} | Get | 200, JSON body |
PUT /stores/{store_id}?columns=a&columns=b | Update | 200 if *Response non-nil, else 204 (true) or 304 (false) |
DELETE /stores/{store_id} | Delete | 204 (true) or 304 (false) |
POST /stores/list, QUERY /stores/list | List | 200, paginated JSON |
The path parameter name derives from the response model (Store becomes
store_id); its macro type derives from the ID type parameter: int,
int64 and uint64 map to the matching macro, uuid.UUID maps to
{...:uuid}, and a string ID stays a plain {...:string}. Declare the
controller with uuid.UUID when you want the uuid macro’s validation.
(iris.IDType permits only those five, so there is no uint case to
reach.) Errors returned by any method
flow through the rest/errors pipeline automatically: the controller
installs errors.Handler on its child router for you.
Resource Options
UseController takes a variadic ...mvc.Option, and the five options
below cover what a generated resource usually needs to say about itself.
Existing UseController() calls compile unchanged.
app.Router("/stores", mvc.UseController[*StoreController](
mvc.Guard(auth.RequireAdmin),
mvc.Without(mvc.Delete),
mvc.IDParam("id"),
mvc.Pagination(iris.AllowSort("name", "created_at")),
))| Option | What it does |
|---|---|
mvc.Guard(handlers...) | Registers middleware on the write routes only: Create, Update, Delete |
mvc.Without(verbs...) | Drops generated routes. The verbs are mvc.Create, mvc.Get, mvc.Update, mvc.Delete and mvc.List |
mvc.IDParam(name) | Overrides the derived parameter name; the macro still comes from the ID type |
mvc.BeforeWrite(fn) | Runs on Create and Update, after the body is decoded and before the controller sees it |
mvc.Pagination(opts...) | The list route’s sort policy: iris.AllowSort, iris.DenySort, or errors.<Code>.DenySort to answer with a chosen code |
Guard exists because “everyone may read, only a signed-in caller may
write” is the common shape and a router-level Use cannot express it
without splitting the resource into two routers. A dropped route under
Without is not registered at all, so it answers 404, or 405 when the
application enables FireMethodNotAllowed and another method of the same
path survives.
BeforeWrite is how a value the client must not choose gets stamped on
every body in one place, which is the other half of the request scope from
Chapter 10:
mvc.BeforeWrite(func(ctx iris.Context, v *Store) error {
tenantID, ok := auth.ScopeID[string](ctx)
if !ok {
return errors.Unauthenticated
}
v.TenantID = tenantID
return nil
})The model type is a type parameter, so the hook receives *T directly and
no reflection happens at serve time. Three of the options fail loudly
rather than quietly: a BeforeWrite declared for a type the resource does
not write panics when the routes are registered, naming both types,
because what it was silently skipping is typically “stamp the tenant id on
this body” and the symptom would otherwise be a cross-tenant write.
Without panics on an unknown verb and IDParam on an empty name, both
at option-construction time.
Routes Straight Over a Repository
When a resource’s HTTP half adds nothing to its store, there is no
controller type left to write. mvc.UseRepository registers the same six
routes directly over the repository:
app.Router("/invoices", mvc.UseRepository(invoiceRepository))The repository is the five methods a service already has, over the standard context: a repository never sees Iris.
type Repository[T any, ID iris.IDType, Filter any] interface {
Create(ctx context.Context, v T) (ID, error)
Get(ctx context.Context, id ID) (T, error)
Update(ctx context.Context, id ID, v T, onlyColumns ...string) (
updated bool, err error)
Delete(ctx context.Context, id ID) (deleted bool, err error)
List(ctx context.Context, filter Filter, limit, offset int) (
[]T, int64, error)
}POST / renders 201 {"id": ...}, GET /{model_id} the model, PUT and
PATCH /{model_id} a 204 or a 304 through ctx.NoContent,
DELETE /{model_id} the same, and POST or QUERY /list the page the List route
already renders. The parameter name and its macro derive from the model
and its ID type exactly as they do for UseController, so a uuid.UUID
id registers {invoice_id:uuid} and validates before any handler runs,
and every option from the table above applies here too.
One deliberate difference: the update routes read their body and columns
through ctx.BindUpdate (Chapter 5), so a
column that is not a member of the model answers 400 naming it and the
members it could have been. The controller routes cannot do that, because
their request type is decoded through a pluggable decoder. The names that
reach the repository are still the client’s, so a repository that builds
SQL from them maps them to its own columns and never interpolates them.
Reach for a controller as soon as a route needs anything the repository does not do: a mail on create, a computed response, a second store to keep in sync.
Custom Routes and the Generated Parameter
A controller may additionally implement Routing(r iris.Router) to add
routes beside the generated ones. mvc.GetPathParam returns the generated
parameter’s name, macro type and route path:
func (c *StoreController) Routing(r iris.Router) {
param := mvc.GetPathParam(c) // {Name: "store_id", Type: "uuid", ...}
r.Get("/featured", func(ctx iris.Context) {
ctx.Writef("featured stores (id param: %s)", param.Name)
})
}Routing runs before the five generated routes are registered, so an
r.Use here guards those too:
func (c *StoreController) Routing(r iris.Router) {
r.Use(auth.RequireAdmin) // protects Create, Get, Update, Delete, List
}The ordering is what makes that work. A route captures the router’s middleware at the moment it is created, so middleware added after a route exists never reaches it.
One Controller, Every Request
A controller is a singleton. It is resolved once, its exported fields are bound once, and its five methods are captured into handlers at registration. There is no per-request instance, so every field is shared across all concurrent requests:
type StoreController struct {
Service *StoreService // fine: written once, read everywhere
current *Store // wrong: one request's value, visible to all
}Keep fields for dependencies. Anything that belongs to a single request belongs on the context or in a method parameter.
Nested Controllers
UseController returns a *DeployedController. Its
With(relPathWithoutParam, controllers...) method mounts children under the
parent’s id segment:
app.Router("/stores", mvc.UseController[*StoreController]().
With("/books", mvc.UseController[*BookController]()))
// BookController serves under /stores/{store_id}/booksA child reads the parent’s path parameter through an auto-injected
mvc.PathParam[T, V] field, where T is the parent response type and V
its ID type:
type BookController struct {
Service *BookService
StoreParam *mvc.PathParam[Store, string]
}
func (c *BookController) Create(ctx iris.Context, book Book) (Book, error) {
book.StoreID = c.StoreParam.Get(ctx) // or GetString(ctx)
return book, nil
}DeployedController.WithViews adds HTML endpoints beside the JSON ones
for controllers that also implement the view methods (IndexView,
CreateView, DetailsView, UpdateView, DeleteView, ListView)
against a registered view engine. It is a generic method: the four type
arguments name the data types those methods return, and a controller
that does not implement mvc.ViewController for them panics at
registration time rather than at first request:
app.Router("/books", mvc.UseController[*BookController]().
WithViews[IndexData, CreateData, ModelData, ListData]())The generated routes are GET /, /create, /{id}/details,
/{id}/update, /{id}/delete and /list, and the template names
derive from the controller’s model name plus
mvc.DefaultTemplateExtension (books/index.html and so on).
The handler Subpackage
The machinery behind UseController is public in controller/mvc/handler
and usable route-by-route. handler.Handler wraps a typed function into an
iris.Handler with the same decode/encode/error pipeline; options tune it:
import "github.com/kataras/iris/v14/controller/mvc/handler"
// POST /orders: decode Order from the body, respond 201.
app.Post("/orders", handler.Handler(createOrder,
handler.WithStatusCode(iris.StatusCreated)))
// GET /orders/{order_id:int64}: decode the named path parameter
// into the function's second argument.
app.Get("/orders/{order_id:int64}", handler.Handler(getOrder,
handler.WithPathParameter[int64]("order_id")))
func createOrder(ctx iris.Context, in Order) (Order, error) { /* ... */ }
func getOrder(ctx iris.Context, id int64) (Order, error) { /* ... */ }For single-value types (string, int, int64, uint64, uuid.UUID) the
default decoder already reads the last path parameter;
WithPathParameter[T](name) makes the binding explicit by name.
handler.IndexHandler wraps a func(Context) (Response, error) action with
no request payload.
WebSocket and gRPC Controllers
Both protocols in this section depart from HTTP’s request-response
rhythm. A WebSocket is a long-lived, two-way connection: after an
initial HTTP handshake, client and server can each push messages at
any moment, the transport behind chats, live dashboards and games.
gRPC is a remote procedure call protocol: instead of paths and JSON,
a client calls typed methods defined in a shared .proto schema, with
binary messages carried over HTTP/2, a common choice for
backend-to-backend APIs.
Two more controller forms ship in the same package. mvc.Websocket
registers WebSocket controllers backed by the third-party
github.com/kataras/neffos library; struct pointers or constructor
functions, resolved through DI:
app.Router("/websocket", mvc.Websocket(newChatController))A WebSocket controller may implement Namespace() string and the standard
neffos events (OnNamespaceConnect, OnNamespaceConnected,
OnNamespaceDisconnect, OnRoomJoin, OnRoomJoined, OnRoomLeave,
OnRoomLeft); any other exported method with the
func(*neffos.NSConn, neffos.Message) error shape becomes a custom event.
mvc.GRPC serves a gRPC server (third-party google.golang.org/grpc)
through the Iris router, POST-only, at /{ServiceName}/{Method} paths:
grpcServer := grpc.NewServer()
pb.RegisterGreeterServer(grpcServer, ctrl)
app.Router("/", mvc.GRPC(ctrl).
Server(grpcServer).
ServiceName("helloworld.Greeter").
Strict(false))With Strict(true) only gRPC clients are served and plain HTTP requests
receive 404; set it to true for streaming.
When Generic Controllers Pay Off
Reach for mvc.UseController when you have several resources that genuinely
fit the Create/Get/Update/Delete/List shape and you want identical REST
semantics, error handling and pagination across all of them: the five
signatures are a contract, and uniformity is the reward. When a resource is
not CRUD-shaped, or its routes need individual middleware and status
conventions, a plain routing controller from
Chapter 4 is the better tool. The two styles
compose freely: a generic controller’s Routing method and With children
are ordinary routers.
Custom Typed Contexts
Handlers that keep reading the same values out of ctx.Values() and casting
them accumulate noise. iris.NewContextWrapper lets you define your own
context type (embedding iris.Context) and write handlers against it
directly:
package main
import (
"github.com/kataras/iris/v14"
)
// appContext extends iris.Context with application-specific helpers.
type appContext struct {
iris.Context
}
// SetContext makes *appContext usable by the wrapper's default pool.
func (c *appContext) SetContext(ctx iris.Context) {
c.Context = ctx
}
func (c *appContext) TenantID() string {
return c.Values().GetString("tenant_id")
}
func main() {
app := iris.New()
// The pointer type must implement SetContext(iris.Context).
w := iris.NewContextWrapper[appContext]()
app.Get("/dashboard", w.Handler(func(ctx *appContext) {
ctx.JSON(iris.Map{"tenant": ctx.TenantID()})
}))
app.Listen(":8080")
}w.Handler converts a func(*appContext) into a standard iris.Handler:
per request it acquires an *appContext, calls SetContext with the live
Iris context, runs your function, and releases the value. Because the custom
type embeds iris.Context, every context method remains available, and you
can override methods or add helpers as needed.
The wrapper offers more converters for the places Iris expects specific
function shapes: Handlers (a slice at once), HandlerReturnError,
HandlerReturnDuration and Filter.
A sync.Pool, from Go’s standard library, is a recycling bin for
short-lived objects: instead of allocating a fresh value for every
request and leaving it to the garbage collector, code takes an instance
from the pool and puts it back when done, reducing allocation pressure
under load.
The default pool allocates a zero value per request. To recycle instances,
build the wrapper from the context subpackage with a sync.Pool-backed
pool:
import "github.com/kataras/iris/v14/context"
w := context.NewContextWrapperWithPool(
context.NewContextPool[appContext]())With a recycling pool, SetContext is your reset hook: it runs on every
acquire, so reset any custom fields there: instances are reused across
requests.
Global Patches
iris.Patches() returns a process-wide singleton for overriding framework
defaults. These are the patch points:
| Patch | Effect |
|---|---|
Patches().Context().Writers().JSON(fn) | Replaces the writer behind ctx.JSON |
Patches().Context().Writers().JSONP/XML/Markdown/YAML(fn) | Same for the other body writers |
Patches().Context().GetDomain(fn) | Changes how Context.Domain() derives the domain from host:port |
Patches().Context().SetDefaultCookieExpiration(d) | Changes the lifetime Context.AddCookie gives a cookie with no CookieExpires option (365 days by default) |
Patches().Context().ResolveHTTPFS(fn) | Changes filesystem resolution for the file server |
Patches().Context().ResolveFS(fn) | Changes filesystem resolution for the view engine |
Patches().Context().SetMaxPageSize(n) | Sets pagination.MaxPageSize, the upper bound of PageOptions.GetLimit |
Patches().Context().SetDefaultPageSize(n) | Sets pagination.DefaultPageSize, used when a list request sends no size |
The classic use of the JSON patch point was swapping in a faster
third-party encoder (jsoniter, easyjson). That trade no longer exists:
since Go 1.27 ctx.JSON streams through the standard library’s
encoding/json/v2, redesigned around the same streaming approach those
libraries were built for, so the stdlib encoder is already the fast
path. What the seam is still for is policy: a binary that must
pretty-print every response, or enforce one marshaling option set
everywhere, patches once instead of threading options through every
call:
package main
import (
"encoding/json/jsontext"
json "encoding/json/v2"
"github.com/kataras/iris/v14"
)
func main() {
// Process-global: applies to every ctx.JSON call
// of every Iris application in this binary.
iris.Patches().Context().Writers().JSON(
func(ctx iris.Context, v any, options *iris.JSON) error {
return json.MarshalWrite(ctx.ResponseWriter(), v,
jsontext.WithIndent(" ")) // house style: always indented.
})
app := iris.New()
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"encoder": "stdlib, patched"})
})
app.Listen(":8080")
}(For a per-call version of the same control, pass options in the
iris.JSON struct’s MarshalOptions field instead; see
Chapter 6. A type that implements
encoding/json/v2’s json.MarshalerTo interface streams itself
through plain ctx.JSON with no patch at all, which replaces the old
ctx.EasyJSON special case.)
Two caveats keep patches honest. First, they are process-global: a patch
affects every application in the binary, including test servers, so
libraries must never patch; only the main package should. Second, apply
them once, at startup, before serving; patches are package-level variables
underneath, not synchronized for concurrent replacement.
Custom Path Macros
Chapter 4 covers the built-in path parameter
types ({id:int}, {id:uuid}, {p:path} and friends). app.Macros()
exposes the registry behind them, so you can extend an existing type with a
new function or register an entirely new parameter type.
Add a function to an existing type with Get + RegisterFunc. The outer
function receives the compile-time arguments from the route pattern; the
returned function validates each request’s value:
app.Macros().Get("string").RegisterFunc("equals",
func(expected string) func(string) bool {
return func(paramValue string) bool {
return paramValue == expected
}
})
// Usage:
app.Get("/status/{state:string equals(active)}", handler)Register a new parameter type with Register. The evaluator converts the
raw segment into a typed value, or reports failure:
import "time"
app.Macros().Register("datetime", "", time.Time{}, false, false,
func(paramValue string) (any, bool) {
t, err := time.Parse("2006-01-02", paramValue)
if err != nil {
return nil, false
}
return t, true
})
app.Get("/events/{when:datetime}", func(ctx iris.Context) {
when := ctx.Params().GetTime("when") // the stored time.Time, as is
ctx.JSON(iris.Map{"weekday": when.Weekday().String()})
})The Register arguments after the alias are the sample value (its type
becomes the parameter’s Go type), whether the type is the master default,
and whether it is trailing (consumes the rest of the path, like path).
When a typed parameter fails to evaluate, builder-built applications respond
with 400 INVALID_ARGUMENT (“invalid path parameter”) by default.
SetErrorHandler (on the whole registry or a single macro) replaces that
behavior:
import "github.com/kataras/iris/v14/rest/errors"
app.Macros().SetErrorHandler(
func(ctx iris.Context, paramIndex int, err error) {
errors.InvalidArgument.Detailf(ctx, "bad path parameter",
"parameter %d: %v", paramIndex, err)
})Wrapping the Router
net/http is the HTTP server in Go’s standard library, the foundation
every Go web framework, Iris included, builds on. Its two base types
are http.ResponseWriter, which writes the response, and
*http.Request, which describes the request; a router wrapper works
directly at that level, before any iris.Context exists.
Middleware runs after Iris has acquired a context and, for Use, matched a
route. Router wrappers run earlier: raw net/http code executed before
routing, for every request. The wrapper signature is:
func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)Call next to continue into the router (or the next wrapper); skip it to
short-circuit. Because the wrapper sees the request before any matching
happens, it is the right place for cross-cutting URL rewrites:
package main
import (
"net/http"
"strings"
"github.com/kataras/iris/v14"
)
func main() {
app := iris.New()
// Serve the old /legacy/* URLs from the /v2 routes,
// without redirects and without touching the routes themselves.
app.WrapRouter(func(w http.ResponseWriter, r *http.Request,
next http.HandlerFunc) {
if p, ok := strings.CutPrefix(r.URL.Path, "/legacy/"); ok {
r.URL.Path = "/v2/" + p
}
next(w, r)
})
app.Get("/v2/users", func(ctx iris.Context) {
ctx.JSON(iris.Map{"version": 2})
})
app.Listen(":8080")
}Three registration methods exist on the root router, differing only in ordering:
WrapRouter(fn): wraps immediately; with multiple calls the last registered wrapper runs first (each new wrapper wraps the previous chain).AddRouterWrapper(fn): defers wrapping until the router builds; the first registered runs first. Use it for redirect-style wrappers that must observe the request before others rewrite it.PrependRouterWrapper(fn): likeAddRouterWrapperbut always first; reserved for path-normalization concerns.
Choose UseRouter (Iris middleware, has a Context, still before route
matching) when you need context features; choose a router wrapper when you
need to mutate the raw request or integrate net/http-level tooling.
At the extreme end, app.Downgrade(handlerFunc) replaces the entire Iris
router with your own http.HandlerFunc. The application still builds, hosts
and shuts down normally, but routing (and every wrapper) is bypassed; you
are responsible for everything.
Multi-Application Composition
Every iris.New call registers the application in a process-wide registry.
Give applications names through their options and any package can retrieve
them without import cycles:
app := iris.New(iris.Options{Name: "api"})
// Elsewhere, without importing the package that built it:
import "github.com/kataras/iris/v14/apps"
api := apps.Get("api") // nil if no app has that exact name.
all := apps.GetAll() // every registered *iris.Application.
apps.OnApplicationRegistered(func(app *iris.Application) {
// fires for each future registration.
})The registry itself lives in the context package:
context.GetApplications() returns the same set as read-only
context.Application interface values; that is what framework internals
use, while apps.GetAll casts to the concrete type for you.
The Switch Application
apps.Switch composes several applications behind one listener. It returns
a new, ordinary application whose job is to route each request to the first
matching case:
package main
import (
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/apps"
)
func newWebsite() *iris.Application {
app := iris.New(iris.Options{Name: "website"})
app.Get("/", func(ctx iris.Context) { ctx.Writef("website") })
return app
}
func newAPI() *iris.Application {
app := iris.New(iris.Options{Name: "api"})
app.Get("/status", func(ctx iris.Context) {
ctx.JSON(iris.Map{"ok": true})
})
return app
}
func main() {
website := newWebsite()
api := newAPI()
switcher := apps.Switch(apps.Hosts{
{Pattern: "api.example.com", Target: api},
{Pattern: "example.com", Target: website},
})
switcher.Listen(":80")
}What Switch actually does: it builds each case application on its own
Build (via OnBuild), then installs a UseRouter handler that evaluates
the case filters in registration order. On the first match it forwards the
request to that application’s ServeHTTP: the matched app serves with its
own context pool and configuration. If nothing matches, the request falls
through to the switcher’s own router, so you can register fallback routes or
a custom 404 on the switcher itself. It is deliberately not a load balancer;
it is a filter chain.
The Hosts provider matches the request host against each Pattern, which
may be a literal or a regular expression (the apps.AnyDomain constant
matches any domain). Target accepts an *iris.Application, the name of a
registered application, or a plain host string; the latter produces a
permanent redirect to that host. Providers combine with apps.Join, and the
apps.SetHost("canonical.example.com") option rewrites the request host
before the matched application sees it, useful for serving www. and the
root domain from one application without redirects.
Standard Library Interop
Iris converts in both directions: standard handlers into Iris handlers, and
the application itself into a standard http.Handler.
http.Handler is a one-method interface,
ServeHTTP(http.ResponseWriter, *http.Request), and it is the lingua
franca of Go’s HTTP ecosystem: metrics exporters, profilers, proxies
and entire frameworks all either implement it or accept it. That shared
contract is what makes both conversions below mechanical.
Calls in the other direction, out to somebody else’s service, have their
own package: github.com/kataras/iris/v14/client, whose client.New
takes a base URL, persistent request options, timeouts, rate limiters and
a retry policy, and whose request helpers marshal and unmarshal JSON, form
and multipart bodies, so a service does not hand-roll a fourth
*http.Client wrapper. c.BindJSON[Order](ctx, http.MethodGet, "/orders/42", nil)
names the response type at the call site and returns it, the way
ctx.BindJSON does on the server side; a status of 400 or above comes
back as an APIError that rest/errors.HandleAPIError forwards to your
own client. A paginated Iris list reads back through client.List[T],
one page, or client.Items[T], which walks every page as an
iter.Seq2[T, error]; both send QUERY and decode into the same Page[T]
the server built. Retries with backoff, a rate limit per client or per endpoint,
and redaction of API keys from error messages and debug dumps are options,
not code you write. The package is a thin layer over the author’s
standalone github.com/kataras/httpclient, with the framework’s JSON
policy as its default, so an application never imports that module
directly.
Mounting http.Handler Values
iris.FromStd accepts an http.Handler, an http.HandlerFunc-shaped
function, or a middleware-shaped func(w, r, next http.HandlerFunc) and
returns an iris.Handler:
package main
import (
// Third-party: the Prometheus client.
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/kataras/iris/v14"
)
func main() {
app := iris.New()
app.Get("/metrics", iris.FromStd(promhttp.Handler()))
app.Listen(":8080")
}A mounted handler reads route parameters with r.PathValue(name) and
the matched route template, such as /users/{id:uuid}, from
r.Pattern, in all three forms. r.Pattern stays empty where no route
has matched yet or none will: in the UseRouter slot and in the
built-in 404 and 405 handlers. Anything written against net/http
mounts this way; see Chapter 17 for
the full monitoring setup around that /metrics route.
The Application as an http.Handler
After Build, the application satisfies http.Handler, so it drops into
any standard server, test harness or third-party listener:
package main
import (
"log"
"net/http"
"github.com/kataras/iris/v14"
)
func main() {
app := iris.New()
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"served_by": "net/http"})
})
if err := app.Build(); err != nil {
log.Fatal(err)
}
srv := &http.Server{
Addr: ":8080",
Handler: app, // *iris.Application implements http.Handler.
}
log.Fatal(srv.ListenAndServe())
}app.Listen and app.Run call Build for you; call it yourself only when
handing the application to infrastructure you control, as above. This is
also how one Iris application embeds inside another: build the inner app and
forward requests to its ServeHTTP from a route of the outer one.
Built-in Controllers
The controller package tree ships ready-made controllers that implement
the same Routing(r iris.Router) interface as your own and register the
same two ways:
// Directly on a router:
app.Router("/static", fileserver.New("./assets", fileserver.DefaultOptions))
// Or through the builder's API step (see Chapter 3):
iris.NewBuilder().
// ...builder steps...
API("/static", fileserver.New("./assets", fileserver.DefaultOptions)).
Build().
Listen(":8080")File Server
fileserver.New(fileSystemOrDirectory, options) serves static files from a
directory path, an http.FileSystem, an embed.FS or an fs.FS. A
directory path follows symbolic links that point outside it; pass
root.FS() of an os.OpenRoot instead when that matters, and accept
that a link you placed there on purpose stops being served
(Chapter 18):
package main
import (
"embed"
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/controller/fileserver"
)
//go:embed assets
var embeddedAssets embed.FS
func main() {
app := iris.New()
app.Router("/static", fileserver.New(embeddedAssets,
fileserver.Options{
IndexName: "index.html",
ShowList: true,
AssetValidator: fileserver.BlockHidden,
}))
app.Listen(":8080")
}The options worth knowing: IndexName (directory index file), ShowList
and ShowHidden (directory listings, with DirList for a custom renderer),
SPA (serve the index instead of 404 for unknown files, as single-page
applications), Attachments (download mode with rate limiting), Cache
(in-memory caching and pre-compression), PushTargets (HTTP/2 push), and
AssetValidator, a per-request func(ctx iris.Context, name string) bool
gate. fileserver.BlockHidden is a ready validator that denies dotfiles
such as .git, .env and .htpasswd. fileserver.DefaultOptions is a
sensible starting value.
The package also exports Handler (the same serving logic as a plain
iris.Handler, no controller), Favicon(path) (a cached
GET|HEAD /favicon.ico controller) and WebDAV(handler) (wraps a
golang.org/x/net/webdav handler, third-party, registering all WebDAV
methods on /{p:path}).
API Graph
apigraph.New serves an interactive D3.js visualization of every registered
route: paths, methods, handlers, with editor links to each handler’s source
line. The graph is built after the application builds, so registration order
does not matter:
import "github.com/kataras/iris/v14/controller/apigraph"
app.Router("/apigraph", apigraph.New(apigraph.Options{Theme: "dark"}))Options.Theme is "light" (default) or "dark"; register custom themes
with apigraph.RegisterTheme. Mount it in development builds: it is a map
of your entire API surface.
Sitemap
A sitemap is an XML file that lists a site’s pages for search-engine crawlers, optionally hinting how often each page changes and how important it is relative to the rest.
sitemap.New(domain) generates /sitemap.xml from the registered static
routes at build time:
import "github.com/kataras/iris/v14/controller/sitemap"
app.Router("/", sitemap.New("https://www.example.com"))
app.Get("/", index)
app.Get("/about", about).
SetLastMod(time.Now()).
SetChangeFreq("monthly").
SetPriority(0.8)
app.Get("/internal", internal).ExcludeSitemap()
// GET /sitemap.xml now lists / and /about.The route methods SetLastMod, SetChangeFreq and SetPriority fill the
matching sitemap elements; ExcludeSitemap removes a route. Dynamic,
subdomain and offline routes are excluded automatically, and above 50,000
entries the output splits into an indexed set. With the i18n middleware in
place, WithLocalization emits translated alternate links per language.
Summary
mvc.UseController[*C]()generates the five REST routes from a typed controller, with DI-resolved construction, nested resources viaWithand parent parameters viamvc.PathParam[T, V]; thecontroller/mvc/handlerpackage exposes the same typed pipeline (Handler,WithStatusCode,WithPathParameter) route-by-route, andmvc.Websocket/mvc.GRPCcover the non-REST protocols.iris.NewContextWrapper[T]()turns a struct embeddingiris.Contextinto the argument type of your handlers; pool it for reuse withcontext.NewContextWrapperWithPool.iris.Patches()swaps framework internals (JSON and other body writers, domain resolution, cookie defaults, filesystem resolution) once, at startup, for the whole process.app.Macros()extends the path parameter language:RegisterFuncadds functions to existing types,Registeradds new types, andSetErrorHandlershapes the 400 response.WrapRouter,AddRouterWrapperandPrependRouterWrapperrun rawnet/httpcode before routing;Downgradereplaces the router entirely.- The
appspackage is a process-wide application registry, andapps.Switchwithapps.Hostsroutes one listener across many applications by host pattern. iris.FromStdmounts anynet/httphandler, and afterBuildthe application itself is anhttp.Handler.controller/fileserver,controller/apigraphandcontroller/sitemapare ready-made controllers for static assets, route visualization and sitemap generation.
Further Reading
- Go: Getting started with generics: the official hands-on introduction to type parameters.
- Go blog: An Introduction to Generics: why generics exist and how constraints work, from the Go team.
- Go: http.Handler: the one-method interface behind router wrappers and both interop directions.
- Go: sync.Pool: the recycling mechanism behind the pooled context wrapper.
- MDN: The WebSocket API:
what the protocol behind
mvc.Websocketprovides. - google.golang.org/grpc:
the gRPC library that
mvc.GRPCserves through the Iris router.
You have followed Iris from a first iris.New() to generic controllers,
typed contexts and framework patches; there is no larger machinery left
hidden.