Route Groups
Route groups organize routes under a common path prefix, attach shared middleware, and expose group-level controls such as deprecation, disabling, OpenAPI tagging, and security requirements. Every group is bound to a single *Okapi instance and registers its routes through it.
Features at a Glance
- Path prefixing — every route registered on a group is joined with its prefix
- Nesting — sub-groups inherit the parent’s prefix, middlewares, and disabled state
- Middleware — chainable middleware applied before any route in the group (including nested ones)
- Standard
net/httpinterop — registerhttp.Handler/http.HandlerFuncand usefunc(http.Handler) http.Handlermiddleware - Disable / Enable — flip a group on or off at runtime; disabled groups return
404and are hidden from the OpenAPI spec - Deprecation — mark every route in the group as deprecated in the docs
- Tagging — apply OpenAPI tags, plus rich tag info with descriptions and external docs
- Security — declare Bearer, Basic, or fully custom security requirements at the group level
- Bulk registration — register controller-style
[]RouteDefinitionin one call
Creating a Group
There are two ways to create a group:
o := okapi.Default()
// Most common: create from the Okapi instance
api := o.Group("/api", LoggerMiddleware)
// Alternative: create explicitly with NewGroup (useful when wiring controllers)
v1 := okapi.NewGroup("/v1", o, AuthMiddleware)
Both forms accept zero or more middlewares applied to every route in the group. The prefix must be non-empty.
g.Okapi() returns the parent *Okapi instance, which is handy when a controller receives a *Group and needs access to the underlying app.
Nesting Subgroups
Calling Group on an existing group creates a nested subgroup. The child inherits its parent’s prefix, middleware chain, and disabled state.
o := okapi.Default()
api := o.Group("/api", LoggerMiddleware)
v1 := api.Group("/v1").Deprecated() // Marked as deprecated in OpenAPI
v2 := api.Group("/v2") // Active version
v3 := api.Group("/v3", featureFlagMW).Disable() // Disabled, returns 404
v1.Get("/books", getBooks)
v2.Get("/books", v2GetBooks)
v3.Get("/books", v3GetBooks) // Not reachable while v3 is disabled
admin := api.Group("/admin", adminAuthMiddleware)
admin.Get("/dashboard", getDashboard)
Registering Routes
A group exposes the same HTTP verbs as the top-level Okapi instance:
api := o.Group("/api")
api.Get("/books", listBooks)
api.Post("/books", createBook)
api.Put("/books/:id", updateBook)
api.Patch("/books/:id", patchBook)
api.Delete("/books/:id", deleteBook)
api.Options("/books", optionsBooks)
api.Head("/books", headBooks)
Each method accepts the same RouteOption values as the top-level router (e.g. DocSummary, DocResponse, UseMiddleware).
Standard net/http Handlers
For interop with the standard library, groups expose HandleStd and HandleHTTP. Both wrap the handler with the group’s middleware chain.
api := o.Group("/api")
// Standard http.HandlerFunc
api.HandleStd("GET", "/standard", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello"))
})
// Standard http.Handler (e.g. a file server)
api.HandleHTTP("GET", "/assets/*", http.FileServer(http.Dir("static")))
Middleware
Adding Okapi Middleware
Use appends one or more middlewares to the group’s chain. They run before any route-level middleware and are inherited by subgroups.
api := o.Group("/api")
api.Use(func(c *okapi.Context) error {
slog.Info("api request", "path", c.Request().URL.Path)
return c.Next()
})
Wrapping Standard HTTP Middleware
UseMiddleware adapts middleware written as func(http.Handler) http.Handler — the common pattern used by gorilla/handlers, chi, and similar libraries.
api.UseMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slog.Info("standard middleware")
next.ServeHTTP(w, r)
})
})
Enabling and Disabling Groups
Groups (and individual routes) can be toggled on or off at runtime without commenting out code.
| Type | HTTP Response | Swagger Docs | Affects Child Routes |
|---|---|---|---|
| Disabled Route | 404 Not Found | Hidden | N/A |
| Disabled Group | 404 Not Found | Hidden | Yes — all nested |
Typical uses:
- Temporarily removing endpoints during maintenance
- Gating routes behind feature flags
- Deprecating old API versions
- Building toggleable test or staging routes
app := okapi.Default()
api := app.Group("/api")
v1 := api.Group("/v1").Disable() // Hidden from docs, returns 404 for all v1 routes
v1.Get("/", func(c *okapi.Context) error {
return c.OK(okapi.M{"version": "v1"})
})
v2 := api.Group("/v2")
v2.Get("/", func(c *okapi.Context) error {
return c.OK(okapi.M{"version": "v2"})
})
if err := app.Start(); err != nil {
panic(err)
}
Call .Enable() to turn a group back on, or remove the .Disable() call entirely.
Deprecating a Group
Deprecated() marks every route in the group as deprecated in the OpenAPI specification. Routes still work — clients are merely informed.
v1 := api.Group("/v1").Deprecated()
v1.Get("/books", getBooks) // Documented as deprecated
OpenAPI Tagging
Tags group operations in Swagger / Redoc UI. Okapi falls back to the group prefix when no tag is set.
Simple Tags
api := o.Group("/api").WithTags([]string{"api"})
Rich Tag Info
WithTagInfo registers tag descriptions (and optional external documentation links) at the root of the OpenAPI spec, so Swagger UI renders them above the operations.
api := o.Group("/api").WithTagInfo(
okapi.GroupTag{
Name: "books",
Description: "Operations on the books catalog",
ExternalDocs: &okapi.ExternalDocs{
URL: "https://example.com/docs/books",
Description: "Full books API reference",
},
},
okapi.GroupTag{
Name: "shared",
Description: "Endpoints shared across catalogs",
},
)
api.Get("/books", listBooks) // Tagged: "books", "shared"
Empty tag names are silently ignored, and duplicate tag names are deduplicated across routes.
Group-Level Security
Okapi exposes three helpers for declaring security requirements on every route in a group. They register the requirement in the OpenAPI spec; pair them with your authentication middleware to actually enforce auth.
Bearer Authentication
secure := o.Group("/secure").WithBearerAuth()
secure.Use(authMiddleware) // Your enforcement logic
secure.Get("/me", profile)
Basic Authentication
internal := o.Group("/internal").WithBasicAuth()
internal.Use(basicAuthMiddleware)
Custom Security Requirements
WithSecurity accepts a raw OpenAPI security requirement object for fine-grained schemes (OAuth2, API keys, scopes, multiple schemes, etc.).
admin := o.Group("/admin").WithSecurity([]map[string][]string{
{"oauth2": {"admin:read", "admin:write"}},
})
Bulk Registration with Register
Register accepts one or more RouteDefinition values, making it easy to define routes inside a controller and attach them to a group later.
type BookController struct{}
func (c *BookController) Routes() []okapi.RouteDefinition {
return []okapi.RouteDefinition{
{
Method: http.MethodGet,
Path: "/books",
OperationId: "ListBooks",
Handler: c.list,
Options: []okapi.RouteOption{okapi.DocSummary("List books")},
},
{
Method: http.MethodPost,
Path: "/books",
Handler: c.create,
Middlewares: []okapi.Middleware{rateLimitMW},
Options: []okapi.RouteOption{okapi.DocSummary("Create a book")},
},
}
}
func main() {
app := okapi.Default()
api := app.Group("/api").WithTags([]string{"books"})
bc := &BookController{}
api.Register(bc.Routes()...) // All routes inherit /api + middleware + tags
}
Routes registered through Register inherit the group’s prefix, tags, tag info, and disabled state.
Method Chaining
Group configuration methods return *Group, so they can be chained fluently:
v1 := o.
Group("/v1", LoggerMiddleware).
WithTags([]string{"v1"}).
WithBearerAuth().
Deprecated()