@withboundary/contract exposes two entrypoints that do the same job:
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.
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: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 sharedcontractand reuse. - You want to wire a single
logger(especiallycreateBoundaryLogger) 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-drivenT inference:
What about the dashboard?
The SDK readscontractName 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