@xndrjs/domain
v0.3.0
Published
Validator-agnostic, function-first runtime modeling with domain and compose APIs.
Readme
@xndrjs/domain
Validator-agnostic semantic modeling with function-first kits:
- primitives for validated nominal values;
- shapes for immutable entity boundaries;
- proofs for explicit extra guarantees;
- capabilities for behavior attached to kits (not instances).
The package exports:
domain(primitive,shape,proof,capabilities.forShape,capabilities.forPrimitive);compose(object,array,optional) for validator composition;pipefor proof/capability assertion chains;DomainValidationErrorand public types (Validator,ShapeKit, ...).
Install
pnpm add @xndrjs/domainQuickstart (core-only)
The core needs a Validator<input, output>. You can write one directly or use an adapter package.
import type { Validator } from "@xndrjs/domain";
import { domain } from "@xndrjs/domain";
const nonEmptyString: Validator<string> = {
engine: "custom",
validate(input) {
if (typeof input !== "string" || input.length === 0) {
return {
success: false,
error: {
engine: "custom",
issues: [{ code: "invalid", path: [], message: "Expected non-empty string" }],
},
};
}
return { success: true, data: input };
},
};
const Email = domain.primitive("Email", nonEmptyString);
const email = Email.create("[email protected]");Recipes
Nested sub-shape composition from kit
Use compose.object + compose.array + compose.optional to build nested validators from existing kits.
import type { Validator } from "@xndrjs/domain";
import { compose, domain } from "@xndrjs/domain";
const nonEmptyString: Validator<string> = {
engine: "custom",
validate(input) {
if (typeof input !== "string" || input.length === 0) {
return {
success: false,
error: {
engine: "custom",
issues: [{ code: "invalid", path: [], message: "Expected non-empty string" }],
},
};
}
return { success: true, data: input };
},
};
const Address = domain.shape("Address", compose.object({ city: nonEmptyString }));
const User = domain.shape(
"User",
compose.object({
id: nonEmptyString,
address: Address,
tags: compose.array(nonEmptyString),
nickname: compose.optional(nonEmptyString),
})
);Capabilities + patch re-validation
Capabilities attach only custom methods to a schema kit. Use UserShape.create / UserShape.is on the shape kit; use User.verify on the capability kit. The factory receives a context (patch, create, safeCreate, is) wired from the schema kit at attach time.
import type { Validator } from "@xndrjs/domain";
import { compose, domain } from "@xndrjs/domain";
const nonEmptyString: Validator<string> = {
engine: "custom",
validate(input) {
if (typeof input !== "string" || input.length === 0) {
return {
success: false,
error: {
engine: "custom",
issues: [{ code: "invalid", path: [], message: "Expected non-empty string" }],
},
};
}
return { success: true, data: input };
},
};
const booleanValidator: Validator<boolean> = {
engine: "custom",
validate(input) {
if (typeof input !== "boolean") {
return {
success: false,
error: {
engine: "custom",
issues: [{ code: "invalid", path: [], message: "Expected boolean" }],
},
};
}
return { success: true, data: input };
},
};
const UserShape = domain.shape(
"User",
compose.object({ displayName: nonEmptyString, isVerified: booleanValidator })
);
const User = domain.capabilities
.forShape<{ displayName: string; isVerified: boolean }>()
.methods(({ patch }) => ({
verify(user) {
return patch(user, { isVerified: true });
},
}))
.attach(UserShape);
const user = UserShape.create({ displayName: "Ada", isVerified: false });
const verified = User.verify(user);Proof + refineType + pipe
Chain proof assertions with pipe to get progressive guarantees.
import type { Validator } from "@xndrjs/domain";
import { domain, pipe } from "@xndrjs/domain";
const verifiedValidator: Validator<{ isVerified: boolean }> = {
engine: "custom",
validate(input) {
if (
typeof input !== "object" ||
input === null ||
typeof (input as { isVerified?: unknown }).isVerified !== "boolean"
) {
return {
success: false,
error: {
engine: "custom",
issues: [{ code: "invalid", path: [], message: "Expected { isVerified: boolean }" }],
},
};
}
return { success: true, data: { isVerified: (input as { isVerified: boolean }).isVerified } };
},
};
const Verified = domain
.proof("Verified", verifiedValidator)
.refineType((row): row is typeof row & { isVerified: true } => row.isVerified === true);
const user = { isVerified: true };
const proven = pipe(user, Verified.assert);Pitfalls and design decisions
- Keep
domainas source of truth; adapters validate boundary payloads. - Core shape kits do not expose schema-specific extension helpers (like adapter
.extendAPIs). ischecks rely on prototype/marker semantics; JSON roundtrip removes prototype and must re-enter throughcreate.- Prefer
proof.test/assertto express explicit guarantee steps rather than implicit casts.
See also
- Zod adapter:
../domain-zod - Valibot adapter:
../domain-valibot
License
MIT
