Validation

Nimbus provides a VineJS-inspired fluent validation API for validating requests, forms, and models. Define rules as chainable methods, validate structs directly, and use database-backed rules for uniqueness checks.

Fluent schema rules

Define validation schemas using the fluent builder API:

import "github.com/CodeSyncr/nimbus/validation"

func (r *CreateBlogRequest) Rules() validation.Schema {
    return validation.Schema{
        "title":   validation.String().Required().Min(1).Max(255),
        "content": validation.String().Required().Min(1).Max(5000),
        "email":   validation.String().Required().Email(),
        "age":     validation.Number().Required().Min(18).Max(120),
    }
}

String rules

MethodDescription
Required()Field must not be empty
Min(n)Minimum length
Max(n)Maximum length
Email()Must be a valid email
URL()Must be a valid URL
Regex(pattern)Must match regex
In(values...)Must be one of the allowed values
Alpha()Letters only
AlphaNum()Letters and numbers only
Trim()Trim whitespace before validation
Confirmed()Must match field_confirmation

Number rules

MethodDescription
Required()Field must not be zero
Min(n)Minimum value
Max(n)Maximum value
Positive()Must be greater than 0
Between(min, max)Must be within range

Database rules

Use Unique() and Exists() for database-backed validation. These query the database using GORM.

func (r *CreateUserRequest) Rules() validation.Schema {
    return validation.Schema{
        // Check email is unique in users table
        "email": validation.String().Required().Email().Unique(validation.UniqueOpts{
            Table:  "users",
            Column: "email",
        }),

        // Unique with filter (exclude current user during updates)
        "email": validation.String().Required().Email().Unique(validation.UniqueOpts{
            Table:  "users",
            Column: "email",
            Filter: func(db *gorm.DB) *gorm.DB {
                return db.Where("id != ?", currentUserID)
            },
        }),

        // Check role exists in roles table
        "role_id": validation.Number().Required().Exists(validation.ExistsOpts{
            Table:  "roles",
            Column: "id",
        }),
    }
}

Model-level validation

Models can implement SchemaProvider to validate themselves. This is the recommended pattern:

type Todo struct {
    database.Model
    Title string
    Done  bool
}

func (t *Todo) Rules() validation.Schema {
    return validation.Schema{
        "title": validation.String().Required().Min(1).Max(255),
    }
}

func (t *Todo) Validate() error {
    return validation.ValidateStruct(t)
}

// In a controller:
todo := &models.Todo{Title: strings.TrimSpace(ctx.Request.FormValue("title"))}
if err := todo.Validate(); err != nil {
    return ctx.View("form", map[string]any{"error": err.Error()})
}

Request validation in controllers

func (c *BlogController) Store(ctx *http.Context) error {
    var req validators.CreateBlogRequest

    ve, err := validation.BindAndValidateSchema(ctx, &req)
    if ve != nil {
        return ve  // returns 422 JSON with validation errors
    }
    if err != nil {
        return err
    }

    blog := &models.Blog{Title: req.Title, Content: req.Content}
    if err := database.Get().Create(blog).Error; err != nil {
        return err
    }
    return ctx.JSON(http.StatusCreated, blog)
}

Generating validators

Scaffold a validator with the CLI:

nimbus make:validator Blog

This generates a struct with Rules() and Validate() methods using the fluent API.

Conditional validation

Apply rules conditionally based on another field's value or a custom predicate:

When (field-based)

func (r *CreateUserRequest) Rules() validation.Schema {
    return validation.Schema{
        "role":       validation.String().Required().In("personal", "business", "creator"),
        // Only required when role is "business"
        "company_id": validation.When("role", "business", validation.String().Required()),
        // Only required when role is "creator", with a fallback otherwise
        "bio": validation.When("role", "creator", validation.String().Min(10)).
            Otherwise(validation.String().Max(500)),
    }
}

WhenFn (predicate-based)

func (r *OrderRequest) Rules() validation.Schema {
    return validation.Schema{
        "shipping_address": validation.WhenFn(
            func(data map[string]any) bool {
                return data["delivery_type"] == "physical"
            },
            validation.String().Required().Min(10),
        ),
    }
}

Conditional rules implement the Rule interface and compose naturally with other rules in the schema.