Routing
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"))
}))
Group related routes
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.
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()
| Method | Effect |
|---|---|
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...) | OpenAPI response for that status |
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 or TLS configuration. Listen calls http.ListenAndServe with no timeouts set.
Shut down gracefully
ListenWithContext serves until the context is cancelled, then drains in-flight requests before returning. Pair it with signal.NotifyContext and the deferred cleanup in main actually runs:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
pool := mustConnect()
defer pool.Close()
app := buildApp(pool)
if err := app.ListenWithContext(ctx, ":8080"); err != nil {
log.Fatal(err)
}
}
In-flight requests get up to 30 seconds to finish; app.ShutdownTimeout(d) changes that. Call app.Shutdown(ctx) directly if you drive the lifecycle yourself — it is safe before Listen starts and safe to call twice, and it makes Listen return nil rather than http.ErrServerClosed, so an intentional stop is not mistaken for a crash.
app.Server() returns the underlying *http.Server once Listen has started it, for reading or tuning fields you would otherwise have no handle on.
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.