@ikaru5/heimdall-contract
v0.11.0
Published
Validation and Representer Objects in your Frontend
Maintainers
Readme
Heimdall Contract
Inheritable and nestable data Objects with intuitive validation schema in your JS-Project. Target is to create Contracts like Rubys Trailblazer-Reform for your JS-Frontend like React, Vue or any other JS project (also backend).
Goals
- RELIABLE: Heimdall Contracts got 100% test coverage.
- REUSABLE: Contracts can be reused in other contracts. Thanks to validation context and inheritance you can reuse contracts with different validation rules.
- EXTENSIBLE: You can extend the validation schema with your own validators.
- INTUITIVE: The validation schema is intuitive and easy to understand.
- INDEPENDENT: Heimdall Contracts are independent of any other library. It is not tied to any state management system or framework.
- TESTABLE: Contracts are easy to test. Just take a look at the tests. ;)
- DOCUMENTED: The documentation is written in a way that you can understand it without any prior knowledge.
- FLEXIBLE: You can use Heimdall Contracts in any JS project. It is not tied to any framework or state management system.
- LIGHTWEIGHT: Heimdall Contracts is lightweight. It has no dependencies and is only 3.1kb minified and gzipped.
- TRANSLATABLE: You can use your own translation library like i18next for error messages.
Why Heimdall and not Zod, Yup or Valibot?
Short answer: they solve a different problem.
Zod, Yup and Valibot are schema parsers: you throw data at a schema, get a validated result and are done — stateless and perfect for type-checking data at an API boundary.
A Heimdall Contract is a form object (inspired by Ruby's Trailblazer-Reform): a stateful, class-based data holder that accompanies the whole lifecycle of a form or process:
- Instantiate it empty and bind your inputs directly to its properties.
assign()nested data from your API or state management — including key mapping viaparseAs/renderAs.- Validate the same contract differently per situation using validation contexts (
on: "signup"). - Reuse and extend contracts through class inheritance and nesting.
- Render a clean object for sending with
toObject().
If you just need to parse and type-check data once (especially with TypeScript type inference), use Zod — the tools complement each other rather than compete. If you need a reusable, stateful data object living between your UI and your API, that is what Heimdall Contract is built for.
Example
First lets build a signup data contract!
class SignupContract extends ContractBase {
defineSchema() {
return (
{
...super.defineSchema(), // optional: used for inheritance ;)
...{
name: {dType: "String", presence: true},
agb: {dType: "Boolean", default: false, only: true},
email: {dType: "String", presence: true, isEmail: true},
username: {dType: "String", presence: true, min: 8},
password: {dType: "String", presence: true, min: 8},
passwordRepeat: {
dType: "String", presence: true,
validate: (value, contract) => {
return value === contract.password ? true : false // for custom error message return string instead of false.
// Note: Custom validation functions return final messages and are not automatically translated
}
},
address: {
street: {dType: "String", presence: true},
streetNumber: {dType: "Number", presence: true},
plz: {dType: "String", presence: true},
city: {dType: "String", presence: true},
}
}
}
)
}
}Now you can simply instantiate your contract like this:
const signUpContract = new SignupContract()Accessing and assigning single values is pretty straight forward:
signUpContract.name = "Kirill"
signUpContract.address.street = "Uhlandstr. 36"
console.log(
signUpContract.name
)
console.log(
signUpContract.address.street
)If the data is coming as JSON or as an nested Object from your API or State Management System for example, you can assign it like that:
signUpContract.assign({
name: "Kirill",
agb: true,
email: "[email protected]",
username: "kirill94",
password: "secret-password",
passwordRepeat: "secret-password",
address: {
street: "Uhlandstr.",
streetNumber: 36,
plz: "10437",
city: "Berlin"
}
})Inherit from your base class and define your contracts! Have Fun!
Documentation
- Changelog
- Installation & Getting Started
- General Usage
- TypeScript
- Configuration
- API
- Schema
- Errors
- Mixed type Arrays
- Validation
- Localization
Validation Context
It is possible to do validations only if a specific context is set. It is a quality of life feature and could also be implemented through validateIf.
class SubContextContract extends ContractBase {
defineSchema() {
return (
{
...super.defineSchema(),
...{
numberWithoutContext: {dType: "Number", min: 10},
number: {dType: "Boolean", only: true, on: "contextA"}
}
}
)
}
}
class ContextContract extends ContractBase {
defineSchema() {
return (
{
...super.defineSchema(),
...{
numberWithoutContext: {dType: "Number", min: 10},
number: {dType: "Number", min: 10, on: "contextA"},
string: {dType: "String", match: /^[a-zA-Z0-9\s]*$/, on: ["contextA", "contextB"]},
addressSimple: {
plz: {presence: true, dType: "String", on: "contextA"}
},
sub: {dType: "Contract", contract: SubContextContract, allowBlank: false, on: "contextB"},
addressesContracted: {dType: "Array", min: 2, arrayOf: SubContextContract, allowBlank: false, on: "contextB"}, // this context will skip only outer validations like "min: 2" in this example
subsContractedWithInner: {dType: "Array", min: 3, arrayOf: SubContextContract, allowBlank: false, on: "contextB", innerValidate: {on: "contextB"}} // use innerValidate to skip validations of nested contract
}
}
)
}
}As you can see, every attribute can have one or more contexts through on attribute.
The context will also be passed to the nested contracts.
const contextContract = new ContextContract()
contextContract.assign(data)
contextContract.isValid("contextA") // context will be set to contextA
contextContract.isValid(["contextA", "contextB"]) // use multiple contexts
contextContract.isValid() // context = undefined
contextContract.isValid("matchAnyContext") // magic context to match all contextsRoadmap
Heimdall Contract is feature complete for its core mission. On the way to 1.0:
- [ ] battle testing the strict schema linting and type inference in real projects
- [ ] freezing the public API (validation semantics and the errors object shape are already documented as intended)
Ideas beyond 1.0:
- typed
assign()input
All notable changes are tracked in the Changelog.
Contributing
- Fork it (https://github.com/ikaru5/heimdall-contract/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors and Contact
If you have ideas on how to develop heimdall or what features it is missing, I would love to hear about it!
- @ikaru5 Kirill Kulikov - creator, maintainer
Copyright
Copyright (c) 2025 Kirill Kulikov [email protected]
heimdall-contract is released under the MIT License.
