Architecture Finding
Type: tech-debt / abstraction (follow-up to epic #23685)
Affected area: pkg/api/handlers/ + pkg/api/route_group_*.go
Follow-up to #23685. Phase 1 (domain subpackages) is landed against origin/main @ 40886a18e7:
- 18 subpackages exist under
pkg/api/handlers/ (admin/, k8s/, proxy/, persistence/, ops/, dashboards/, compliance/, mcp/, missions/, stellar/, workloads/, gitops/, rewards/, benchmarks/, feedback/, github/, auth/, internal/httputil/).
- Root
pkg/api/handlers/ now contains 22 files, of which 7 are pure re-export shims (admin_aliases.go, dashboards_aliases.go, k8s_aliases.go, ops_aliases.go, persistence_aliases.go, proxy_aliases.go, transport_aliases.go — 13–69 LOC each) plus small residuals (auth_compat, demo_data, shared_types, shared_utils, k8s_errors, validation) and their tests.
- Route registration still goes through the root
handlers.* symbols: route_group_api_core.go alone has 16 handlers.Foo call sites, route_group_public.go has 9, route_group_governance.go has 4, route_group_feedback.go has 1.
This is exactly the state where Phase 2 from the epic pays off. Filing it as a discrete carve-out so it can be picked up independently.
Impact
- The
_aliases.go shims are a permanent tax — every new/renamed handler in a subpackage requires a matching root re-export or the route group breaks, undermining the boundary the split was meant to establish.
- Each
route_group_*.go still hand-wires the same 8 dependencies (store, hub, k8sClient, config fields, notificationService, persistenceStore, failureTracker, githubToken) into every constructor. Adding a new dep means editing ~5 route-group files and every constructor call.
- The subpackages have no shared registration contract, so tests can't drive route registration in isolation; today's tests either mount the whole
route_group_* or reach past it.
- New contributors still have to learn "call
handlers.NewFooHandler(store, hub, k8sClient, …) here" per handler instead of "register the subpackage".
Recommendation
Land Phase 2 in three PR-sized slices; PRs stay ≤500 LOC each and preserve behavior.
Slice 2a — introduce handlers.Deps and a Registrar interface (new file, no moves)
pkg/api/handlers/registrar.go:
package handlers
import "github.com/gofiber/fiber/v2"
// Deps is the shared dependency set that every handler subpackage needs.
// Add new fields here, not new constructor parameters.
type Deps struct {
Store store.Store
Hub *ws.Hub
K8sClient kubernetes.Interface
PersistenceStore persistence.Store
NotificationService notify.Service
FailureTracker *failure.Tracker
Config *config.Config
GitHubToken string
}
// Registrar is implemented by each handler subpackage. Route groups call
// Register once per subpackage instead of constructing individual handlers.
type Registrar interface {
Register(router fiber.Router, deps Deps)
}
No behavior change; all existing constructors continue to work.
Slice 2b — port ONE subpackage (proposal: admin/) to Registrar
- Add
admin.NewRegistrar() handlers.Registrar that internally builds AdminHandler/SettingsHandler/UserHandler/TeamHandler/RBACHandler and calls the same router.Get(…) lines currently duplicated across route_group_api_core.go and route_group_governance.go.
- Replace those call sites with
admin.NewRegistrar().Register(router, deps).
- Delete
admin_aliases.go if no other caller still uses the root re-exports (verify with go build ./...).
This slice is the template. Once it lands, the remaining 5 subpackages (k8s/, proxy/, ops/, dashboards/, persistence/) follow the same recipe and can each land as an independent PR.
Slice 2c — collapse route_group_*.go to a wire-up
Once every subpackage implements Registrar, route_group_api_core.go shrinks from ~234 LOC to a list:
for _, r := range []handlers.Registrar{
admin.NewRegistrar(),
k8s.NewRegistrar(),
proxy.NewRegistrar(),
ops.NewRegistrar(),
dashboards.NewRegistrar(),
persistence.NewRegistrar(),
} {
r.Register(api, deps)
}
All 7 _aliases.go shims can then be deleted; the root handlers package becomes the Deps + Registrar contract only (~80 LOC).
Out of scope (deliberately)
- Route path changes — pure structural refactor.
- Moving auth middleware — that stays in the route group so cross-cutting policy remains centralized.
github/, feedback/, compliance/ subpackages that already import cleanly with no root re-exports — they can opt in later but aren't blocking.
Refs epic #23685 (Phase 2 slice — landable as 2a + one 2b template PR to prove out the pattern).
Filed by architect agent (ACMM L6 — full mode)
🐝 Hive Agent: architect | Instance: hosted-kubestellar-console-4vkt | SHA: unknown
— hive: agent=architect backend=copilot model=claude-opus-4.7 copilot=1.0.88
Architecture Finding
Type: tech-debt / abstraction (follow-up to epic #23685)
Affected area:
pkg/api/handlers/+pkg/api/route_group_*.goFollow-up to #23685. Phase 1 (domain subpackages) is landed against
origin/main@40886a18e7:pkg/api/handlers/(admin/,k8s/,proxy/,persistence/,ops/,dashboards/,compliance/,mcp/,missions/,stellar/,workloads/,gitops/,rewards/,benchmarks/,feedback/,github/,auth/,internal/httputil/).pkg/api/handlers/now contains 22 files, of which 7 are pure re-export shims (admin_aliases.go,dashboards_aliases.go,k8s_aliases.go,ops_aliases.go,persistence_aliases.go,proxy_aliases.go,transport_aliases.go— 13–69 LOC each) plus small residuals (auth_compat,demo_data,shared_types,shared_utils,k8s_errors,validation) and their tests.handlers.*symbols:route_group_api_core.goalone has 16handlers.Foocall sites,route_group_public.gohas 9,route_group_governance.gohas 4,route_group_feedback.gohas 1.This is exactly the state where Phase 2 from the epic pays off. Filing it as a discrete carve-out so it can be picked up independently.
Impact
_aliases.goshims are a permanent tax — every new/renamed handler in a subpackage requires a matching root re-export or the route group breaks, undermining the boundary the split was meant to establish.route_group_*.gostill hand-wires the same 8 dependencies (store,hub,k8sClient,configfields,notificationService,persistenceStore,failureTracker,githubToken) into every constructor. Adding a new dep means editing ~5 route-group files and every constructor call.route_group_*or reach past it.handlers.NewFooHandler(store, hub, k8sClient, …)here" per handler instead of "register the subpackage".Recommendation
Land Phase 2 in three PR-sized slices; PRs stay ≤500 LOC each and preserve behavior.
Slice 2a — introduce
handlers.Depsand aRegistrarinterface (new file, no moves)pkg/api/handlers/registrar.go:No behavior change; all existing constructors continue to work.
Slice 2b — port ONE subpackage (proposal:
admin/) toRegistraradmin.NewRegistrar() handlers.Registrarthat internally buildsAdminHandler/SettingsHandler/UserHandler/TeamHandler/RBACHandlerand calls the samerouter.Get(…)lines currently duplicated acrossroute_group_api_core.goandroute_group_governance.go.admin.NewRegistrar().Register(router, deps).admin_aliases.goif no other caller still uses the root re-exports (verify withgo build ./...).This slice is the template. Once it lands, the remaining 5 subpackages (
k8s/,proxy/,ops/,dashboards/,persistence/) follow the same recipe and can each land as an independent PR.Slice 2c — collapse
route_group_*.goto a wire-upOnce every subpackage implements
Registrar,route_group_api_core.goshrinks from ~234 LOC to a list:All 7
_aliases.goshims can then be deleted; the roothandlerspackage becomes theDeps+Registrarcontract only (~80 LOC).Out of scope (deliberately)
github/,feedback/,compliance/subpackages that already import cleanly with no root re-exports — they can opt in later but aren't blocking.Refs epic #23685 (Phase 2 slice — landable as 2a + one 2b template PR to prove out the pattern).
Filed by architect agent (ACMM L6 — full mode)
🐝 Hive Agent:
architect| Instance:hosted-kubestellar-console-4vkt| SHA:unknown— hive: agent=architect backend=copilot model=claude-opus-4.7 copilot=1.0.88