modkit is a Go framework for building modular backend services. It brings NestJS-style module organization to Go—without reflection, decorators, or magic.
Modular toolkit for Go.
modkit is in early development. APIs may change before v0.1.0. Use it for prototypes, side projects, or evaluation, but expect potential breaking changes.
Go 1.25.7+. We pin the patch level to 1.25.7 in CI to align with vulnerability scanning and keep a consistent security posture.
Reflection in Go:
- Makes code harder to debug
- Obscures the call graph
- Can lead to runtime surprises
- Doesn't work well with static analysis tools
modkit uses explicit Build functions and string tokens. Everything is visible in code.
String tokens are simple, explicit, and work without reflection. The trade-off is manual type casting when you call Get():
svc, err := module.Get[UsersService](r, "users.service")
if err != nil {
return nil, err
}
// svc is already of type UsersServiceThis is intentional—it keeps the framework small and makes dependencies visible.
modkit only supports singleton scope (one instance per provider). This keeps the model simple and predictable. If you need request-scoped values, pass them through context.Context.
Modules provide:
- Boundaries: Clear separation between features
- Visibility: Control what's exposed to other modules
- Organization: Natural structure for larger codebases
- Testability: Replace entire modules in tests
modkit compares with google/wire, uber-go/fx, samber/do, manual DI, and NestJS. For a detailed comparison, see the Comparison Guide.
For small services, manual DI in main() is fine. modkit helps when:
- You have multiple feature modules
- You want visibility enforcement between modules
- You're building a larger service with a team
See Comparison Guide for a detailed analysis.
Yes. Modules must be passed as pointers to ensure stable identity when shared across imports:
// Correct
app, err := kernel.Bootstrap(&AppModule{})
if err != nil {
log.Fatal(err)
}
// Wrong - will not work correctly
app, err = kernel.Bootstrap(AppModule{}) // rejected
if err != nil {
// handle error
}No. modkit rejects circular imports with ModuleCycleError. Refactor to break the cycle, often by extracting shared dependencies into a separate module.
DuplicateModuleNameError. Module names must be unique across the graph.
Yes, as long as it's the same pointer instance. modkit deduplicates by pointer identity.
Providers are built lazily—on first Get() call, not at bootstrap. This means:
- Unused providers are never built
- Build order depends on resolution order
- Circular dependencies are detected at build time
No. DuplicateProviderTokenError. Each token must be unique within a module.
Create a test module that provides mock implementations:
type TestUsersModule struct{}
func (m *TestUsersModule) Definition() module.ModuleDef {
return module.ModuleDef{
Name: "users",
Providers: []module.ProviderDef{{
Token: "users.service",
Build: func(r module.Resolver) (any, error) {
return &MockUsersService{}, nil
},
}},
Exports: []module.Token{"users.service"},
}
}RouteRegistrar:
type RouteRegistrar interface {
RegisterRoutes(router Router)
}modkit includes a chi-based HTTP adapter, but controllers are just structs with a RegisterRoutes method. You can adapt to any router.
Use Group and Use:
func (c *Controller) RegisterRoutes(r mkhttp.Router) {
r.Group("/admin", func(r mkhttp.Router) {
r.Use(adminAuthMiddleware)
r.Handle(http.MethodGet, "/users", handler)
})
}chi v5.
Not yet. A gRPC adapter is planned for post-MVP.
mkhttp.Serve handles SIGINT/SIGTERM automatically. For custom shutdown logic:
server := &http.Server{Addr: ":8080", Handler: router}
go func() {
<-ctx.Done()
server.Shutdown(context.Background())
}()
server.ListenAndServe()See Error Types in the API reference.
Use a helper function or RFC 7807 Problem Details. See Error Handling Guide.
See CONTRIBUTING.md. Start with issues labeled good first issue.
Open an issue on GitHub using the bug report template.
Open an issue on GitHub using the feature request template.