Graft is a tiny, opinionated HTTP toolkit that helps you structure Go services around modules. It keeps the standard library ergonomics you already know, adds a light router wrapper, and lets every module declare typed dependencies so the compiler has your back.
- Modular route definitions with strongly typed dependency injection
- Middleware chaining that still looks and feels like
net/http - Helpers for trailing slashes and serving static assets
- Batteries-included testability using Go's
httptesthelpers
go get github.com/janmarkuslanger/graft@latestYou can then import the packages you need:
import (
"github.com/janmarkuslanger/graft/graft"
"github.com/janmarkuslanger/graft/module"
"github.com/janmarkuslanger/graft/router"
)Create a module, register it on the app, run the server:
package main
import (
"net/http"
"github.com/janmarkuslanger/graft/graft"
"github.com/janmarkuslanger/graft/module"
"github.com/janmarkuslanger/graft/router"
)
type Deps struct {
Greeter func() string
}
func main() {
app := graft.New()
hello := module.Module[Deps]{
Name: "hello",
BasePath: "/hello",
Deps: Deps{
Greeter: func() string { return "hello from graft" },
},
Routes: []module.Route[Deps]{
{
Method: http.MethodGet,
Path: "/",
Handler: func(ctx router.Context, deps Deps) {
ctx.Writer.Write([]byte(deps.Greeter()))
},
},
},
}
app.UseModule(&hello)
app.Run() // serves on :8080
}module.Module[T]is generic over the dependency type you expect in handlers.Routesholds the HTTP verb, relative path, and handler.Middlewareslets you apply router middlewares to every route.- Handlers receive both the request context (
router.Context) and your typed dependency value. - Global middleware for the whole app:
app.UseMiddleware(...); module-specific middleware viaModule.Middlewares.
Example with dependencies and middleware:
type AuthDeps struct {
Users UserService
Sessions SessionService
}
auth := module.Module[AuthDeps]{
Name: "auth",
BasePath: "/auth",
Deps: AuthDeps{Users: users, Sessions: sessions},
Middlewares: []router.Middleware{
requestLogger,
},
Routes: []module.Route[AuthDeps]{
{
Method: http.MethodPost,
Path: "/login",
Handler: func(ctx router.Context, deps AuthDeps) {
token, err := deps.Sessions.Login(ctx.Request.Context())
if err != nil {
http.Error(ctx.Writer, err.Error(), http.StatusUnauthorized)
return
}
ctx.Writer.Write([]byte(token))
},
},
},
}Hooks let a module prepare dependencies or kick off background work:
auth := module.Module[AuthDeps]{
// ...
Hooks: module.Hooks[AuthDeps]{
OnUse: func(deps *AuthDeps) {
deps.Users = users.WithCache()
},
OnStart: func(deps *AuthDeps) {
log.Println("auth module ready", deps.Users.Count())
},
},
}OnUse runs when the module is registered with UseModule. OnStart runs right before the server starts, after all modules have been registered. Both receive a pointer to your dependency struct so you can update it in-place.
- Register once:
app.RegisterService("db", db) - Retrieve (typed helper):
graft.MustServiceAs[Type](app, "db")orgraft.ServiceAs[Type](app, "db") - Check existence:
app.HasService("db") - Modules can opt in to receive the service bag by implementing
SetServices(*graft.Services);UseModuleinjects it before hooks/routes run.
Example: module consuming a registered DB:
type DBModule struct {
services *graft.Services
}
func (m *DBModule) SetServices(s *graft.Services) { m.services = s }
func (m *DBModule) BuildRoutes(r router.Router) {
db := graft.MustGetService[DB](m.services, "db")
r.AddHandler("GET /users", func(ctx router.Context) {
users := db.All()
// ...
ctx.Writer.Write([]byte(fmt.Sprintf("%d users", len(users))))
})
}router.New()returns a wrapper aroundhttp.ServeMux.AddHandler("GET /ping", handler, middleware...)registers method-aware routes.Handleautomatically registers both/pathand/path/variants.Static(prefix, dir, middleware...)is a convenience forhttp.FileServer.- Middleware signature:
func(ctx router.Context, next router.HandlerFunc).
Static assets in one line:
r := router.New()
r.Static("/assets", "./public", loggingMiddleware)graft.Run()listens on:8080by default.- Use Go's standard tools:
go test ./...runs the full suite;go test ./... -coverprofile=coverage.txtmirrors the CI setup and feeds Codecov. - The router and modules are intentionally
httptestfriendly—spin up a router, register modules, and exercise handlers directly.
Bug reports, feature ideas, and pull requests are always welcome. Please format code (gofmt) and run the tests before submitting. If you plan a larger change, open an issue first so we can figure out the best direction together.
The project is MIT licensed—see LICENSE for the legal bits.