Core

Routing

Register routes, use path patterns, group them, and attach metadata.

Routing is Go's net/http.ServeMux. ezz registers METHOD /pattern entries on a mux it owns, so pattern syntax, precedence, and conflict rules are the standard library's.

Register a route

Each HTTP method has a matching function on the app. They all take a pattern and an http.Handler, and return a *RouteBuilder you can chain metadata onto.

app := ezz.New()

app.GET("/users", listUsers)
app.POST("/users", createUser)
app.PUT("/users/{id}", replaceUser)
app.PATCH("/users/{id}", updateUser)
app.DELETE("/users/{id}", deleteUser)
app.HEAD("/health", healthHead)
app.OPTIONS("/users", usersOptions)

// any other method
app.Handle("TRACE", "/debug", traceHandler)

The handler can be an ezz.H typed handler, an http.HandlerFunc, or anything else implementing http.Handler:

app.GET("/legacy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("plain net/http still works"))
})).Public()

Typed handlers additionally expose their request and response types, which is what the OpenAPI generator reads. A plain http.Handler still routes and still respects auth rules, it just has no schema to document.

Match path parameters

Patterns use {name} for a single segment and {name...} for a trailing wildcard.

app.GET("/users/{id}", handler)             // /users/42
app.GET("/files/{path...}", handler)        // /files/a/b/c.txt
app.GET("/orgs/{org}/repos/{repo}", handler)

Read them through the request type, which is the usual case:

type GetUserRequest struct {
    ID string `path:"id" validate:"required"`
}

Or read them from the context when you have no request struct:

app.GET("/users/{id}", ezz.HFunc(func(ctx *ezz.Context) (*User, error) {
    return lookup(ctx.Param("id"))
}))

app.Group(prefix) returns a *RouteGroup that shares a path prefix, and optionally middleware and an auth rule, with every route registered on it.

api := app.Group("/api/v1")

api.GET("", listRoot)          // /api/v1
api.GET("/users", listUsers)   // /api/v1/users

Groups nest, and a child copies the parent's middleware and auth rule at the moment it is created:

api := app.Group("/api/v1").Use(middleware.Gzip())
admin := api.Group("/admin").Auth(ezz.Role("admin"))

admin.GET("/stats", stats)   // /api/v1/admin/stats, gzipped, admin only

Trailing slashes on the prefix are trimmed, so Group("/api/") and Group("/api") behave the same.

A group snapshots its middleware and auth rule when a route is registered, and a child group snapshots the parent when the child is created. Call Use and Auth on a group before registering routes on it or creating children from it.

Override auth per route

Precedence runs from broad to narrow: the app default, then the group rule, then the route rule.

app := ezz.New().Auth(provider)      // default: any valid token

app.GET("/health", health).Public()               // no auth
app.GET("/admin", panel).Auth(ezz.Role("admin"))  // admin only
app.GET("/me", profile)                            // inherits the default

To make routes public by default and opt into auth instead, pass nil:

app := ezz.New().Auth(provider).DefaultAuth(nil)

app.GET("/open", handler)                          // public
app.GET("/closed", handler).Auth(ezz.Authenticated())

See Auth rules for the full set of rules.

Attach metadata for the docs

Every registration returns a *RouteBuilder:

app.GET("/users/{id}", getUser).
    Tags("Users").
    Description("Returns a single user by ID.").
    Summary("Get user").
    OperationID("getUser").
    Response(404, "User not found").
    Deprecated()
MethodEffect
Auth(rule)Sets the auth rule for this route
Public()Shorthand for Auth(ezz.Public())
Tags(...)OpenAPI tags, also used to group operations in the docs UI
Description(text)OpenAPI operation description
Summary(text)Stored on the route
OperationID(id)Stored on the route
Deprecated()Stored on the route
Response(status, description, example...)Stored on the route
Tags and description are the two fields the current OpenAPI generator emits. Summary, operation ID, deprecation, and extra responses are recorded on the route but not yet written into the spec; operation IDs are generated from the method and path instead. See OpenAPI.

Start serving

// the convenience path
log.Fatal(app.Listen(":8080"))

// or drive the server yourself, since App is an http.Handler
srv := &http.Server{
    Addr:              ":8080",
    Handler:           app,
    ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(srv.ListenAndServeTLS(certFile, keyFile))

Use the second form when you need timeouts, TLS configuration, or graceful shutdown. Listen calls http.ListenAndServe with no timeouts set.

Inspect registered routes

app.Routes() returns the *ezz.Route values, which is what the OpenAPI generator walks. It is also handy for a startup log or a route table in tests.

for _, r := range app.Routes() {
    log.Printf("%-6s %-30s public=%t", r.Method, r.Pattern, r.AuthRule == nil || r.AuthRule.IsPublic())
}

Next steps

  • Handlers for what happens inside a route.
  • Middleware for the wrapping order and the built-in set.
Copyright © 2026