zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Start

Why zmdbSupported

zmdb exists because the four libraries it replaces each solve one part of "get data from a request into a database and back" and each pay for it in a different way. Using all four together means running four metadata systems over the same types.

The problem with the stack it replaces#

A typical TypeScript API today wires up something like this:

LayerLibraryWhat it reads
Request validationTypia or Zodyour TypeScript types
DTO / serializationclass-transformerdecorator metadata
Schema / queriesDrizzle or MikroORMa second schema declaration
OpenAPI@nestjs/swagger + CLI plugina third re-parse of your source

The same User shape is described four times. Nothing checks that the four descriptions agree, so the failure mode is not a crash — it is a validator that accepts a field the database rejects, or an OpenAPI document that documents a field you removed last week.

What zmdb does instead#

One TypeScript interface is the source of truth, and everything else is derived from it by the type system:

import type { HasDefault, PrimaryKey, Serial, Sql, Table, Unique } from '@zmdb/core/tags';

export interface User extends Table<'users'> {
  id: number & Sql<'integer'> & Serial & PrimaryKey;
  email: string & Sql<'text'> & Unique;
  createdAt: Date & Sql<'timestamp'> & HasDefault;
}

There is no column-map schema _object_ to write. The table name, column types, key and constraint intent are all on the type, carried by intersection tags that erase to nothing at runtime — @zmdb/core/tags has no runtime exports at all, so that import disappears from your build output. Schema objects that have no type-level shape, such as a standalone or expression index, stay in explicit migrations.

From that one declaration you get, with no second declaration and no runtime reflection:

If you delete email from the interface, every one of those changes in the same commit, and the ones that cannot change break the build.

Three design rules#

1. Compile time over runtime. Validators are generated from the checker's view of your types during tsc, not assembled from design:type metadata at boot. That is why there is no reflect-metadata, no metadata cache to invalidate, and no CLI plugin that re-parses your source to recover types the decorators could not see.

2. Rows are data. A read returns a plain object. There is no identity map holding it, no proxy deciding whether a property access is free or a query, and no flush() inferring your intent from a diff. See Why fetched rows are inert.

3. Explicit ownership. The runtime foundation has no external runtime dependencies. Database providers, HTTP, build tooling and optional integrations have their own declared dependencies and resource ownership. The default @zmdb/core install includes SQLite; select a supported driver instead of writing an adapter for the beginner path. See Runtime foundation and Database selection.

These rules exclude identity maps, automatic unit-of-work flushing and lazy relation proxies. Anti-patterns explains those boundaries. Use the generated package reference and client integration guide for current package and framework support.

When not to use zmdb#

---

Continue with Quick startBlog APIGenerated client. See Architecture for ownership and Anti-patterns for deliberate limits.