Skip to main content
Whenever you deal with untrusted data, you need validation as far up as possible. Have the compiler prove these rules where values are known at compile time, and have it enforce them at runtime as well.

Manual validation in Go

Go handlers often repeat input validation manually:
Go structs define field names and types, and interfaces specify required methods, but neither can express detailed validation rules like value ranges or formats.

Constraints on types

In Forst, types can declare exactly what makes data valid. You attach constraints directly to types, and the compiler enforces them at the API boundary and, where possible, at compile time. This keeps validation clear and central—no more scattered checks.
When placeOrder runs, the compiler has already proven the shape at compile time where values are known; use ensure (or explicit checks) for dynamic input. Invalid SKUs and quantities never reach your catalog logic.
Forst also lets you define your own rules for types with type guards.

Built-in constraints

Constraints appear in two places: The same built-in names work in both. For Result narrowing (Ok, Err) and pointer checks (Nil, Present), see Ensure and narrowing.

Chaining

Constraints chain with dots on the base type. All chained rules must pass:

Reference

Use constraints on struct fields, function parameters, and nested shapes. Validation follows the data structure, including deeply nested records. Nested constraints on an order input, including tags on the payload:
Ensure-only or reserved (not typically chained on types):
  • Ok() / Err(): Result discriminants; use in ensure x is Ok() (Ensure and narrowing)
  • Valid(): reserved placeholder; prefer explicit guards
  • Value(…): literal refinement in assertion types (advanced)

Structural shapes

Forst shapes use field names (camelCase by convention) and compose like TypeScript interfaces:
Invalid nested data fails at the same boundary. You do not re-walk the tree in application code.

Caveats

Use ensure for runtime checks today

Constraints on types document the rules. The compiler does not emit automatic parameter checks before your function body yet. Use ensure explicitly inside handlers for runtime validation.

Compile time vs runtime

The compiler proves constraints only where values are known at compile time. Dynamic or untrusted input still needs ensure at the boundary.

Wire serialization

By default, shape struct fields lower to unexported Go identifiers with no json tags. For HTTP or JSON marshaling, pass -export-struct-fields on forst run / forst build, or set compiler.exportStructFields: true in ftconfig.json.

Built-in semantics still maturing

The typechecker does not fully validate every built-in constraint beyond presence wiring. Valid() is a reserved placeholder. Prefer explicit constraints or type guards. Generated TypeScript types reflect structure only. Client-side validation is your job unless you call the Forst server. See Generate client types § Caveats.
Constraint and emission behavior is still expanding. See the roadmap.

What gets emitted

The compiler lowers ensure checks in the function body to plain Go: String.Min(1) becomes a len(field) comparison, Int.Min(1) a numeric comparison, and HasPrefix / Contains call strings.* with an auto-added import.

Try it

From the Forst repository:
Examples live under examples/in/.

Ensure and narrowing

Assert invariants inside functions with ensure … is ….

Type guards

Reusable custom predicates on your domain types.