Skip to main content
@withboundary/contract exposes two entrypoints that do the same job:
Both run the same engine. Pick based on how you use the contract in your codebase.

defineContract — the default

Use when:
  • The contract is used in more than one place (multiple endpoints, multiple environments, shared across a service).
  • You want a named, typed object to pass around.
  • You want to override options per call without redefining.
Every existing guide on this site uses defineContract.

enforce — the shortcut

Use when:
  • The contract is one-off (experimental notebook, a one-shot script, a migration job).
  • You don’t need to reuse the definition.
  • You prefer a flat call with all options in one place.
enforce is implemented on top of defineContract — there’s no behavioral difference. It’s just a convenience when the extra line would be noise.

Side-by-side

Same behavior, different ergonomics:
Both accept identical options (ContractOptions<T> & { name: string }). enforce’s signature re-arranges the required fields (schema + run + options), while defineContract bundles everything into one config object and returns a factory.

When to switch from enforce to defineContract

  • You start calling enforce(schema, ..., { name: "x", rules }) in two or more places. DRY it up — create a shared contract and reuse.
  • You want to wire a single logger (especially createBoundaryLogger) across many calls without repeating the option.
  • Your test suite starts needing to share the contract config with production code.

TypeScript inference

Both preserve schema-driven T inference:

What about the dashboard?

The SDK reads contractName off each event. Whether you used enforce or defineContract, the dashboard groups events by name. Keep names stable — renaming them splits your dashboard history.

See also

defineContract

Full option reference

enforce

Signature + example