Pydantic-inspired • Lightweight • Backend & Frontend friendly
A blazing fast, lightweight and Reactive Class-Based Object Modeling framework.
Build clean, safe, and delightful domain entities for TypeScript and JavaScript.
@bufferpunk/modelcore is built around a Base class that validates plain objects using a static schema definition. It is useful when working with NoSQL data, API payloads, and nested objects that need runtime and compile-time guarantees and constraints.
When a model extends Base and defines a static schema, instance creation and updates will:
- enforce required fields
- apply defaults (primitive values or factory functions)
- coerce values to the configured type when possible
- validate nested objects and arrays recursively
- validate allowed values with
enum - run custom
beforeChecksandafterCheckshooks if present - run custom
validatehook for final validation if present - enforce immutability at class or field level
This package now also provides a typed factory pattern (createFrom) and improved TypeScript mappings so editors receive useful type information and inferred instance types when schemas are declared with as const.
- Zod / Joi / Yup: These are schema validation libraries that focus on validating plain objects. They do not provide a class-based model with runtime immutability or automatic coercion. You would need to manually validate and then assign values to a class instance.
- TypeORM / Sequelize: These are ORM libraries that provide class-based models but are tightly coupled to databases and do not focus on runtime validation or immutability outside of the context of database operations.
- MobX / Vue Reactivity: These libraries provide reactivity and state management but do not enforce validation rules or immutability at the runtime level. They track changes but do not govern them.
ModelCore fills the gap by providing a class-based modeling system that enforces validation and immutability rules at runtime and compile-time, making it suitable for both frontend and backend applications where data integrity is crucial.
Here is a simple comparison with other libraries
| Feature | ModelCore | Zod / Joi / Yup | TypeORM / Sequelize | MobX / Vue Reactivity |
|---|---|---|---|---|
| Class-based models | ✅ | ❌ | ✅ | ❌ |
| Runtime validation | ✅ | ✅ | Limited (database-focused) | ❌ |
| Reactivity | ✅ | ❌ | ❌ | ✅ |
| Immutability enforcement | ✅ | ❌ | Limited (database-focused) | ❌ |
| Automatic coercion | ✅ | Limited (Zod has some coercion) | ❌ | ❌ |
| Nested object validation | ✅ | ✅ | Limited (database-focused) | ❌ |
| TypeScript support | ✅ | ✅ | ✅ | ✅ |
| Frontend & Backend friendly | ✅ | ✅ | Backend-focused | Frontend-focused |
npm install @bufferpunk/modelcoreimport Base from "@bufferpunk/modelcore"; // in ESM environments
// or const Base = require("@bufferpunk/modelcore").default; // in CommonJS environments
class User extends Base {
static version = 1;
static schema = {
name: {
type: String,
min: 2,
max: 80,
beforeChecks: (value) => typeof value === "string" ? value.trim() : value,
afterChecks: (value) => value.replace(/\s+/g, " ")
},
role: {
type: String,
enum: ["admin", "editor", "viewer"],
default: "viewer",
beforeChecks: (value) => typeof value === "string" ? value.toLowerCase() : value
},
confirmed: { type: Boolean, optional: true, default: false }
};
}
const user = new User({ name: " John Doe ", role: "EDITOR" });
console.log(user);import Base, { SchemaDefinition } from '@bufferpunk/modelcore';
class User extends Base {
static version = 1;
static schema = {
name: {
type: String,
min: 2,
max: 80,
beforeChecks: (value: any) => typeof value === 'string' ? value.trim() : value,
afterChecks: (value: any) => value.replace(/\s+/g, ' ')
},
language: {
type: String,
enum: ['english', 'spanish', 'portuguese'],
default: 'english',
beforeChecks: (value: any) => typeof value === 'string' ? value.toLowerCase().trim() : value,
afterChecks: (value: any) => value.charAt(0).toUpperCase() + value.slice(1)
}
} as const satisfies SchemaDefinition;
}
const user = User.createFrom({ name: ' Ana Silva ' });
console.log(user);You can use custom classes as field types. The system will validate that the value is an instance of the class and run its constructor logic.
import Base, { SchemaDefinition } from "@bufferpunk/modelcore";
class Email {
constructor(public value: string) {
if (typeof value !== "string" || !/^\S+@\S+\.\S+$/.test(value)) {
throw new Error("Invalid email format");
}
}
}
const userSchema = {
id: Number,
email: Email,
name: String,
tags: [String]
} as const satisfies SchemaDefinition;
class User extends Base {}
User.schema = userSchema;
// typed factory; the instance type is inferred from the schema
const u = User.createFrom({ id: 1, email: new Email("a@b.com"), name: "A", tags: ["x"] });
u.email = new Email("b@c.com"); // typescript will enforce that this is an Email instanceEach field in a schema can include:
type(required): constructor such asString,Number,Boolean,Date,Array,Objector custom classes and typesoptional: allows missing valuedefault: fallback value when input isnullorundefined(function values are executed)enum: list of allowed valuesmin,max: length constraints for values with alengthpropertyimmutable: prevent this field from being changed after creationbeforeChecks(value): transforms/sanitizes raw input before required/type checksafterChecks(value): transforms value after type/length/enum checks and before validationvalidate(value): custom final validation logicvalues: required forArraytypes to validate each array itemkeys: required forObjecttypes to validate nested properties
The type property is the only required configuration for a field. All other properties are optional and can be used as needed to enforce constraints and transformations.
For each field, validation runs in this order:
beforeChecks- required/optional and default handling
- type validation/coercion
min/maxenumafterChecks- custom
validate - immutability check
Mark classes or individual fields as immutable to prevent modifications.
class ImmutableUser extends Base {
static immutable = true;
static schema: SchemaDefinition = {
id: { type: String, immutable: true },
name: { type: String }
};
}Use regular property access (recommended) to modify instance properties or the update() method (best if you want to update the whole object).
The constructor automatically includes the version if defined on the class.
const user = new User({ name: 'John', role: 'user' });
user.name = 'Jane'; // (easiest and recommended for simple property changes)
user.update({ name: 'Jane', role: 'admin' });Prefer defining schemas with as const and using the createFrom factory to get type inference for instance shapes without duplicating declarations.
const userSchema = {
id: Number,
email: String,
name: String
} as const satisfies SchemaDefinition;
class User extends Base {}
User.schema = userSchema
const instance = User.createFrom({ id: 1, email: 'a@b.com', name: 'A' })Use keys for objects and values for arrays. See the examples for full patterns.
base.ts/base.js/base.d.ts: base validator implementationexamples/*: runnable examples demonstrating inheritance, factory usage, and custom typestest/*: test suite covering all behaviors
See CHANGELOG.md for breaking changes and migration steps.
Run tests with Node's test runner:
npm run build
npm testThe repository includes a GitHub Actions workflow to run build and test on Node LTS.
Use the included micro-benchmark to compare construction, factory creation, updates, and mutation paths:
npm run benchYou can adjust iteration count with BENCH_ITERATIONS:
BENCH_ITERATIONS=100000 npm run benchExample result on this repository, run with BENCH_ITERATIONS=100000:
construct + validate 1512.23 ms 66128 ops/sec
createFrom factory 1452.82 ms 68832 ops/sec
update validated fields 2831.84 ms 35313 ops/sec
array mutations 2747.17 ms 36401 ops/sec
The benchmark is intentionally small and repeatable. It is useful for comparing changes between commits, not for replacing a full profiler or load test.
See the manifesto for the project's goals and positioning: manifesto.md
This package is intentionally small and framework-agnostic. It gives you runtime schema safety, immutability constraints, and field-level validation without requiring an ORM or heavyweight validation framework.
- Keep schemas as the single source of truth. Prefer
createFromfor type inference and one-source-of-truth behavior. - This library is intentionally small and framework-agnostic: no runtime dependencies and minimal conceptual overhead.
- For TypeScript ergonomics, prefer
as constand named type aliases when you need concise editor hovers. - See the manifesto for the project's goals and positioning: manifesto.md
MIT