Skip to main content
Whenever you deal with untrusted data, you need validation as far up as possible. Attach rules to types so the compiler can prove them when values are known at compile time, and write ensure for data that arrives at runtime.

Constraints on types

In Forst, types can declare exactly what makes data valid. You attach constraints directly to types. The compiler checks those rules when values are known at compile time. Dynamic request data must be validated with ensure before you use it.
When placeOrder receives a literal that already satisfies the constraints, the compiler can prove that at compile time. For request bodies and other dynamic input, write ensure before business logic so invalid SKUs and quantities never reach your catalog.
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 Check a value.

Chaining

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

Reference

String.Min and Max limit length by Unicode character count. That matches JSON Schema minLength and maxLength. Use MinBytes and MaxBytes when you need a UTF-8 size limit, such as a header or payload cap. 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 order is Ok() (Check a value)
  • Value(…): literal refinement in assertion types (advanced)

Structural shapes

Forst shapes use field names (camelCase by convention) and compose like TypeScript interfaces:
Nested field constraints follow the same rules as top-level ones. Prove them at compile time when values are known, and check dynamic input with ensure. You do not re-walk the tree by hand in application code.

Caveats

Constraints stay small and composable

Built-in constraints are a fixed set of simple checks: length, bounds, prefixes, suffixes, and a few similar rules. You combine them with dots (all must pass) and or (one must pass). That keeps types readable and lets the compiler reuse the same rules for ensure, JSON Schema, and generated clients. Forst intentionally omits a regex constraint on String. Free regular expressions can express almost anything, which replaces clear, composable rules with opaque patterns. Build your formats from simple built-in constraints instead:
Name the refined type once, then enforce it at the boundary with that name. See Check a value for ensure username is Username. For a closed list of allowed values, use a literal union such as "todo" | "done". For richer domain rules, write a type guard. If you need a true regular expression, put it in an ordinary function. That validates at runtime. It does not become part of the type’s built-in constraint chain.

Write ensure to check values at runtime

A constraint on a type records the rule. Runtime checks happen when you write ensure. Put those checks in handlers that take untrusted input, before the rest of the function uses the value. See Check a value.

The compiler only proves values it can see

If the value is a literal in your source, the compiler can prove the constraint when you compile. Request bodies, query params, and other input from outside the program are unknown until the function runs. Write ensure at that boundary so invalid data never reaches your logic.

Export fields when you serve JSON

Generated Go structs keep their fields private and omit json tags. In-process Go can use those structs as they are. If you marshal the shape to JSON for HTTP, export the fields. Pass -export-struct-fields on forst run or forst build. You can also set compiler.exportStructFields to true in ftconfig.json.

Generated TypeScript types are structural

forst generate emits TypeScript types that match field names and shapes. Constraints such as String.Min(1) stay on the Forst side. If you need the same checks in the browser or in Node before you call the server, write them in TypeScript. Calling the Forst server runs the checks you wrote with ensure. See Generate client types § Caveats.

What generated Go looks like

Generated Go turns ensure checks in the function body into plain comparisons: String.Min(1) becomes a utf8.RuneCountInString comparison, String.MaxBytes(64) a len comparison, Int.Min(1) a numeric comparison, and HasPrefix / HasSuffix / Contains call strings.* with an auto-added import.

Examples

See the shape and constraint examples on GitHub:

Check a value

Assert invariants inside functions with ensure … is ….

Name a reusable rule

Reusable custom predicates on your domain types.