Ezorm is a TypeScript ORM built around decorated models, repository CRUD, explicit read queries, and an optional managed proxy runtime.
The practical path today is:
@ezorm/coredefines decorated models plus runtime metadata and validation.@ezorm/ormis the primary direct Node.js ORM for SQLite, PostgreSQL, MySQL, and MSSQL.ezormis the CLI for migrations and schema workflows from an explicitezorm.config.*file.- The maintained Nest and Next todo apps are the best end-to-end references in this repository.
- The maintained examples default to
sqlite::memory:, so restarting those processes clears data.
If you are new to ezorm, use this order:
- Install
@ezorm/coreand@ezorm/orm. - Define a decorated model.
- Create a client, run
pushSchema, and use a repository. - Add the CLI for checked-in migration workflows.
- Add a framework adapter or proxy runtime only after the direct ORM flow works.
Recommended first install:
npm install @ezorm/core @ezorm/ormInstall by intent:
| Goal | Package |
|---|---|
| Define model metadata with decorators | @ezorm/core |
| Use direct Node.js ORM repositories and queries | @ezorm/orm |
| Use the CLI for migrations and schema workflows | ezorm |
| Wrap the direct ORM with a Node runtime helper | @ezorm/runtime-node |
| Reuse direct ORM clients in Next.js Node runtimes | @ezorm/next |
| Wire ORM clients and repositories into Nest DI | @ezorm/nestjs |
| Use the pooled HTTP proxy client | @ezorm/runtime-proxy |
| Start and manage the packaged proxy process from Node.js | @ezorm/proxy-node |
To inspect the current CLI surface without installing anything:
npx ezorm --helpStart with @ezorm/core when you want model metadata and input validation from the same decorated class.
import {
Field,
Model,
PrimaryKey,
getModelMetadata,
validateModelInput
} from "@ezorm/core";
@Model({ table: "todos" })
class Todo {
@PrimaryKey()
@Field.string()
id!: string;
@Field.string()
title!: string;
@Field.boolean({ defaultValue: false })
completed!: boolean;
}
console.log(getModelMetadata(Todo));
console.log(
validateModelInput(Todo, {
id: "todo_1",
title: "Ship the README",
completed: false
})
);That gives you:
- runtime metadata for tables, fields, indices, and relations
- input validation from the same model definition
@ezorm/orm is the primary Node.js ORM surface. The fastest first run is SQLite in memory.
import { Field, Model, PrimaryKey } from '@ezorm/core';
import { createOrmClient } from '@ezorm/orm';
@Model({ table: 'todos' })
class Todo {
@PrimaryKey()
@Field.string()
id!: string;
@Field.string()
title!: string;
@Field.boolean({ defaultValue: false })
completed!: boolean;
}
const run = async () => {
const client = await createOrmClient({
databaseUrl: 'sqlite::memory:',
});
await client.pushSchema([Todo]);
const todos = client.repository(Todo);
await todos.create({
id: 'todo_1',
title: 'Ship the README',
completed: false,
});
console.log(await todos.findById('todo_1'));
console.log(
await todos.findMany({
orderBy: { field: 'title', direction: 'asc' },
}),
);
console.log(
await todos.update('todo_1', {
completed: true,
}),
);
await todos.delete('todo_1');
await client.close();
};
void run();The repository API is intentionally small in v1:
createfindByIdfindManyupdatedelete
findMany(...) supports exact-match scalar filters and simple ordering for single-table CRUD.
readCache is opt-in on the direct ORM path. Use it when you want cached repository reads for findById(...) and findMany(...).
import { createOrmClient } from "@ezorm/orm";
const client = await createOrmClient({
databaseUrl: "sqlite::memory:",
readCache: {
default: {
backend: "memory",
ttlSeconds: 30
}
}
});The same cache configuration is available through @ezorm/runtime-node:
import { createNodeRuntime } from "@ezorm/runtime-node";
const client = await createNodeRuntime({
connect: {
databaseUrl: "sqlite::memory:",
readCache: {
default: {
backend: "memory",
ttlSeconds: 30
}
}
}
});Current cache behavior:
readCacheis opt-in.- It applies only to
repository.findById(...)andrepository.findMany(...). - TTL is absolute from write time.
create,update, anddeleteclear that model's cached repository entries.
The ezorm CLI uses a project-level config file named one of:
ezorm.config.tsezorm.config.mtsezorm.config.ctsezorm.config.mjsezorm.config.jsezorm.config.cjs
The config must export:
databaseUrl- optional
models - optional
modelPaths - optional
migrationsDir
Example:
export default {
databaseUrl: "sqlite:///tmp/ezorm.db",
modelPaths: ["src/models"],
migrationsDir: "migrations"
};If models is omitted, the CLI scans modelPaths and imports files containing @Model or Model(...) before deriving schema metadata. Generated configs prefer src/models or models. Explicit models still override scan mode, and broad scan roots such as ["src"] or ["."] may import unrelated matching modules.
Use npx ezorm init to scaffold the config in the nearest package root, add a minimal Todo model when the project does not already have one, and patch tsconfig.json for decorator support in TypeScript projects. TypeScript and JavaScript scaffolds default modelPaths to ["src/models"] when src/ exists or ["models"] otherwise.
TypeScript config files can still import decorator-authored .ts model classes directly. JavaScript config files remain supported for ESM and CommonJS projects.
Current CLI commands:
ezorm init [--ts|--js]
ezorm migrate generate [name]
ezorm migrate apply
ezorm migrate status
ezorm migrate resolve --applied <filename>
ezorm migrate resolve --rolled-back <filename>
ezorm db pull
ezorm db push
Typical workflow:
npx ezorm init
npx ezorm migrate generate init
npx ezorm migrate apply
npx ezorm migrate status
npx ezorm db pull
npx ezorm db pushCommand behavior today:
initwritesezorm.config.*, adds an example Todo model when needed, and patches TypeScript decorator compiler flags for TS scaffolds.migrate generatewrites additive SQL migration files.migrate applyexecutes pending migration files and records them in_ezorm_migrations.migrate statusshows migration state.migrate resolveonly reconciles migration history. It does not execute SQL.db pullprints the live schema as JSON.db pushapplies additive schema drift directly without updating migration history, which makes it the development shortcut rather than the checked-in migration path.
Use repository CRUD for simple writes and single-table reads. Use client.query(...) plus explicit relation metadata for relation-aware reads.
import { BelongsTo, Field, HasMany, Model, PrimaryKey } from "@ezorm/core";
import { createOrmClient } from "@ezorm/orm";
@Model({ table: "users" })
class User {
@PrimaryKey()
@Field.string()
id!: string;
@Field.string()
email!: string;
@HasMany(() => Post, { localKey: "id", foreignKey: "userId" })
posts!: Post[];
}
@Model({ table: "posts" })
class Post {
@PrimaryKey()
@Field.string()
id!: string;
@Field.string()
userId!: string;
@Field.string()
title!: string;
@BelongsTo(() => User, { foreignKey: "userId", targetKey: "id" })
author!: User | undefined;
}
const client = await createOrmClient({
databaseUrl: "sqlite::memory:"
});
await client.pushSchema([User, Post]);
const posts = await client
.query(Post)
.join("author")
.where("author.email", "=", "alice@example.com")
.include("author")
.orderBy("title", "asc")
.all();
const users = await client.query(User).include("posts").all();
await posts[0].author;
await users[0].posts;
await client.load(Post, posts[0], "author");
await client.loadMany(User, users, "posts");
const projected = await client
.query(Post)
.join("author")
.select<{ title: string; authorEmail: string }>({
title: "title",
authorEmail: "author.email"
})
.orderBy("title", "asc")
.all();
console.log(projected);Current relation behavior:
BelongsTo,HasMany, andManyToManyare supported.- Relation metadata requires explicit key mappings.
client.query(Model)is read-only.include(...)prewarms lazy relation caches on query entities.await post.authorandawait user.postsread lazy relation properties from query results.load(...)andloadMany(...)are the explicit plain-object relation loaders.select(...)switches the query into flat projection mode and returns plain rows.
Relation-aware query(...), load(...), and loadMany(...) are available on the direct ORM path. They are not implemented on proxy-backed runtimes yet.
Choose the smallest layer that matches your deployment shape.
Use this first. It is the primary direct Node.js ORM surface for SQLite, PostgreSQL, MySQL, and MSSQL.
Use this when you want a thin Node runtime wrapper but the same direct ORM behavior surface.
import { createNodeRuntime } from "@ezorm/runtime-node";
const client = await createNodeRuntime({
connect: { databaseUrl: "sqlite::memory:" }
});Use this in Next.js server components, route handlers, and server actions when you want a cached direct ORM client.
import { getNextNodeClient } from "@ezorm/next/node";
const client = await getNextNodeClient({
cacheKey: "app",
connect: { databaseUrl: "sqlite::memory:" }
});Use this when you want an OrmClient and repositories wired through Nest dependency injection.
import { Module } from "@nestjs/common";
import { EzormModule } from "@ezorm/nestjs";
import { Todo } from "./todo.model";
@Module({
imports: [
EzormModule.forRoot({
connect: { databaseUrl: "sqlite::memory:" }
}),
EzormModule.forFeature([Todo])
]
})
export class AppModule {}Use @ezorm/runtime-proxy and @ezorm/proxy-node only when you specifically need the managed proxy flow.
@ezorm/proxy-nodestarts and manages the packaged proxy binary from Node.js.@ezorm/runtime-proxyis the HTTP client for that proxy.- The managed proxy supports pooled repository CRUD plus
pushSchemaandpullSchemafor SQLite, PostgreSQL, MySQL, and MSSQL. - Relation-aware
query(...),load(...), andloadMany(...)are not implemented on the pooled proxy runtime yet. - For Node-managed proxy usage, prefer
@ezorm/proxy-nodeinstead of documenting manual Cargo startup as the default workflow.
Use these examples when you want a complete application reference:
- NestJS todo backend: examples/apps/nest-todo-api
- Next.js todo frontend: examples/apps/next-todo-web
- Shared todo domain code: examples/packages/todo-domain
Current limits that matter when you are evaluating the workflow:
- The maintained todo examples default to
sqlite::memory:, so process restarts clear state. - Direct
@ezorm/ormand@ezorm/runtime-nodesupport SQLite, PostgreSQL, MySQL, and MSSQL. - Proxy-backed runtimes support pooled CRUD plus
pushSchemaandpullSchemafor SQLite, PostgreSQL, MySQL, and MSSQL. - Relation-aware
query(...),load(...), andloadMany(...)remain direct-ORM features today. - Primary key handling is intentionally simple in v1: application-supplied keys and single-column primary keys only.
Ezorm keeps a few design choices explicit:
- decorated model classes are the source for metadata, validation, indices, and relations
- repository CRUD stays small, while relation-aware reads move into explicit
query(...)flows - runtime shape is an architectural choice, with a clear split between direct ORM usage and the managed proxy path
- schema workflows stay explicit through
pushSchema,pullSchema, and CLI migrations driven by config
Use the committed package manifests as the source of truth for npm releases.
- Choose the smallest semver bump that matches the change scope.
- Update versions with
pnpm version:workspace <version>. - Refresh
pnpm-lock.yamlwithpnpm install --lockfile-only. - Commit the manifest and lockfile changes together, then merge to
main. - GitHub Actions publishes the npm packages automatically and pushes
v<version>after the publish step succeeds.
Ezorm is available under the MIT License. Copyright (c) 2026 ezorm contributors.