Skip to content

janmarkuslanger/graft

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GRAFT Logo


Code coverage Go Report Latest Release Build Status Download ZIP


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.

Features

  • 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 httptest helpers

Installation

go get github.com/janmarkuslanger/graft@latest

You can then import the packages you need:

import (
    "github.com/janmarkuslanger/graft/graft"
    "github.com/janmarkuslanger/graft/module"
    "github.com/janmarkuslanger/graft/router"
)

Quick Start

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
}

Working With Modules

  • module.Module[T] is generic over the dependency type you expect in handlers.
  • Routes holds the HTTP verb, relative path, and handler.
  • Middlewares lets 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 via Module.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.

Global Services

  • Register once: app.RegisterService("db", db)
  • Retrieve (typed helper): graft.MustServiceAs[Type](app, "db") or graft.ServiceAs[Type](app, "db")
  • Check existence: app.HasService("db")
  • Modules can opt in to receive the service bag by implementing SetServices(*graft.Services); UseModule injects 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 Toolbox

  • router.New() returns a wrapper around http.ServeMux.
  • AddHandler("GET /ping", handler, middleware...) registers method-aware routes.
  • Handle automatically registers both /path and /path/ variants.
  • Static(prefix, dir, middleware...) is a convenience for http.FileServer.
  • Middleware signature: func(ctx router.Context, next router.HandlerFunc).

Static assets in one line:

r := router.New()
r.Static("/assets", "./public", loggingMiddleware)

Running & Testing

  • graft.Run() listens on :8080 by default.
  • Use Go's standard tools: go test ./... runs the full suite; go test ./... -coverprofile=coverage.txt mirrors the CI setup and feeds Codecov.
  • The router and modules are intentionally httptest friendly—spin up a router, register modules, and exercise handlers directly.

Contributing

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.

About

Web framework written in go.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages