Skip to content

Handlers ​

Basic Handlers ​

A handler receives a *Ctx and returns an error:

go
app.Get("/hello", func(c *kruda.Ctx) error {
    return c.Text("Hello, World!")
})

The HandlerFunc type:

go
type HandlerFunc func(c *Ctx) error

Typed Handlers with C[T] ​

Typed handlers use Go generics to auto-parse request data into a struct:

go
type CreateUserInput struct {
    Name  string `json:"name"  validate:"required"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"min=0,max=150"`
}

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

kruda.Post[CreateUserInput, User](app, "/users", func(c *kruda.C[CreateUserInput]) (*User, error) {
    return &User{ID: "1", Name: c.In.Name, Email: c.In.Email}, nil
})

The C[T] type extends Ctx with a typed In field:

go
type C[T any] struct {
    *Ctx
    In T // parsed input from request
}

Typed Handler Registration ​

Package-level generic functions register typed handlers:

go
kruda.Get[In, Out](app, path, handler, opts...)
kruda.Post[In, Out](app, path, handler, opts...)
kruda.Put[In, Out](app, path, handler, opts...)
kruda.Delete[In, Out](app, path, handler, opts...)
kruda.Patch[In, Out](app, path, handler, opts...)

Short variants (no error return, for prototyping):

go
kruda.GetX[In, Out](app, path, handler, opts...)
kruda.PostX[In, Out](app, path, handler, opts...)

Group-level typed handlers:

go
kruda.GroupGet[In, Out](g, path, handler, opts...)
kruda.GroupPost[In, Out](g, path, handler, opts...)

Struct Tags ​

Control how request data is bound to your struct:

go
type GetUserInput struct {
    ID     string `param:"id"`              // from route parameter :id
    Fields string `query:"fields"`          // from query string ?fields=name,email
}

type UpdateUserInput struct {
    ID   string `param:"id"`               // from route parameter
    Name string `json:"name"`              // from JSON body
    Age  int    `json:"age"`               // from JSON body
}

Supported tag types:

TagSourceExample
paramRoute parameters (:id)param:"id"
queryQuery string (?key=val)query:"fields"
jsonJSON request bodyjson:"name"
headerRequest headersheader:"X-Request-ID"

Validation ​

After binding, validate struct tags are checked. Use them like this:

go
type Input struct {
    Name  string `json:"name"  validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"min=0,max=150"`
}

Validation runs by default

Since v1.7.1 validate tags are enforced without any configuration. Opt out with kruda.New(kruda.WithoutValidation()), which parses input and checks nothing. kruda.WithValidator is for registering custom rules or messages, not for switching validation on.

Kruda implements 20 rules — (*kruda.Validator).RuleNames() lists them, plus any registered with Register. It also supports the two go-playground/validator modifiers that change how the rules around them apply: omitempty skips a field's rules when its value is the zero value, and dive applies every rule after it to each element of a slice, array or map rather than to the container. A dive failure names the element — tags[2], limits[free] — not just the field.

go
Bio  string   `validate:"omitempty,min=10"`  // optional, but ≥10 chars if present
IDs  []string `validate:"omitempty,dive,uuid"` // every element must be a UUID

The tag syntax resembles go-playground/validator, which has far more rules, so a tag carried over from it may name one that does not exist here. Those are skipped with a startup warning naming the rule; the field's other rules still apply. The generated OpenAPI schema advertises the constraints regardless.

When validation fails, Kruda returns a structured error response:

json
{
  "error": "Validation failed",
  "code": 422,
  "details": [
    { "field": "email", "message": "must be a valid email" }
  ]
}

Response Methods ​

Handlers use Ctx methods to send responses:

go
// JSON response (status set via Status())
c.Status(200).JSON(user)
c.JSON(user) // default status 200

// Text response
c.Status(200).Text("OK")
c.Text("OK")

// HTML response
c.HTML("<h1>Hello</h1>")

// No content (204)
c.NoContent()

// With headers (method chaining)
c.Status(200).SetHeader("X-Custom", "value").JSON(data)

// Redirect (default 302)
c.Redirect("/new-location")
c.Redirect("/new-location", 301)

See Context API for all response methods.

Route Options ​

Add metadata for OpenAPI generation:

go
kruda.Post[CreateUserInput, User](app, "/users", handler,
    kruda.WithDescription("Create a new user"),
    kruda.WithTags("users"),
)

Generated Binding (experimental) ​

For hot typed routes with many query fields, the generator pre-builds the binding loop and the min/max checks, so per-request reflection is gone from both paths. The output is ordinary Go that calls only the public API.

  1. Annotate the input and add a go:generate line:
go
//go:generate go run github.com/go-kruda/kruda/cmd/bindgen -input input.go -type SearchInput -output search_generated.go -func bindSearchInput

type SearchInput struct {
    Q    string `query:"q" default:"all" validate:"min=1,max=64"`
    Page int    `query:"page" default:"1" validate:"min=1,max=1000"`
}
  1. Run go generate ./... and pass the emitted plan to the route:
go
kruda.Get[SearchInput, SearchOutput](app, "/search", handler,
    kruda.WithGeneratedPlan(SearchInputPlan()),
)

Scope: query/param scalar fields (string, bool, ints, uints, floats) with optional default tags, plus built-in min/max validation. json/form body fields are rejected at generation time.

Everything fails closed. The plan carries the shape the code was generated for; if the struct drifts (added, removed, renamed, or retagged field), registration drops the stale binder silently and the route keeps the generic parser. Inputs whose rules the generator cannot compile — custom overrides, dive, omitempty, anything outside min/max — decline the compiled validator and keep generic validation. Regenerate after changing the input to stay on the fast path.

Released under the MIT License.