@bufferpunk/modelcore
v1.2.0
Published
A blazing fast, lightweight and Reactive Class-Based Object Modeling framework
Maintainers
Readme
ModelCore - Runtime Entity Integrity for JavaScript and TypeScript
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.
What It Does
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
- support typed unions with
Union(...)
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.
Comparison to Other Libraries
- 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 |
Installation
npm install @bufferpunk/modelcoreQuick Start (JavaScript)
import 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);Quick Start (TypeScript)
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 ' }); // creatFrom() is a factory function for better TypeScript type hints and inference
console.log(user);Custom Types / Classes
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: { type: Array, values: String } // all values of this array will be strings
} as const satisfies SchemaDefinition;
class User extends Base {}
User.schema = userSchema;
// typed factory; the types are inferred from the schema
const u = User.createFrom({ id: 1, email: new Email("[email protected]"), name: "A", tags: ["x"] });
u.email = new Email("[email protected]"); // typescript and ModelCore will enforce that this is an Email instanceUnion Types
Use Union(...) to define a field that accepts multiple constructor types while preserving TypeScript inference.
import Base, { SchemaDefinition, Union } from "@bufferpunk/modelcore";
class User extends Base {
static schema = {
identifier: Union(String, Number),
contact: {
type: Object,
keys: { // you can also use `properties`
email: String,
phone: { type: String, optional: true }
}
}
} as const satisfies SchemaDefinition;
}
const user = User.createFrom({ identifier: "123", contact: { email: "[email protected]" } });Union can also combine custom classes and primitives:
class Email extends String {
constructor(value: string) {
super(value);
}
}
class User extends Base {
static schema = {
contact: Union(String, Email)
} as const satisfies SchemaDefinition;
}Field Configuration
Each field in a schema can include:
type(required): constructor such asString,Number,Boolean,Date,Array,Objector custom classes and types. Nested object and array schemas can also use shorthand constructors directly insidekeysandvalues.optional: allows missing valuerequired: alias foroptional: false(can be used for clearer schema intent)default: 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/properties: 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.
Validation Order
For each field, validation runs in this order:
beforeChecks- required/optional and default handling
- type validation/coercion
min/maxenumafterChecks- custom
validate - immutability check
Immutability
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 }
};
}Updating Instances
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' });Factory and TypeScript ergonomics
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: '[email protected]', name: 'A' })Nested Objects and Arrays
Use keys for objects and values for arrays. See the examples for full patterns.
Included Files
base.ts/base.js/base.d.ts: base validator implementationexamples/*: runnable examples demonstrating inheritance, factory usage, and custom typestest/*: test suite covering all behaviors
Migration from @bufferpunk/schema
See CHANGELOG.md for breaking changes and migration steps.
Testing & CI
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.
Benchmarking
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:
│ (index) │ Benchmark │ Time (ms) │ Ops/sec | |---------|--------------------------------------------------|-----------|---------| │ 0 │ 'construct + validate' │ 1166 │ 85781 | │ 1 │ 'createFrom factory' │ 1080 │ 92620 | │ 2 │ 'construct + validate + update validated fields' │ 2661 │ 46729 | │ 3 │ 'construct + validate + array mutations' │ 2845 │ 48088 |
The benchmark is intentionally small and repeatable. It is useful for comparing changes between commits, not for replacing a full profiler or load test.
Why Runtime Entities Matter
See the manifesto for the project's goals and positioning: manifesto.md
Notes
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.
Contributing & Design Notes
- 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
License
MIT
