DynamoDB deserves a real ORM

A strongly typed data modeler and active-record style ORM for DynamoDB — relationship-aware, schema-enforced, and fully transactional.

TypeScript-first
ACID transactions
Single-table
Minimal deps

DynamoDB with real relationships

DynamoDB offers unmatched scalability and performance. But relational modeling has always meant partition key gymnastics and hand-rolled adjacency lists — dyna-record handles it for you.

dyna-record combines single-table design with the adjacency list pattern for true relational modeling. Your data is pre-joined across partitions, giving you single-query retrieval without expensive joins.

@HasOne@HasMany@BelongsTo@HasAndBelongsToMany
typescript
@Entity
class Teacher extends MyTable {
  @StringAttribute()
  public name: string;

  @HasMany(() => Course, { foreignKey: "teacherId" })
  public readonly courses: Course[];
}

// Fetch with related data
const teacher = await Teacher.findById(id, {
  include: [{ association: "courses" }]
});

A data modeler & ORM

Define your schema, enforce it at runtime, and model relationships — all with a familiar TypeScript interface.

Decorator-based models

Define entities with TypeScript decorators. String, number, boolean, date, and object attributes.

typescript
@Entity
class User extends MyTable {
  @IdAttribute // Customizable unique ID field
  @StringAttribute()
  public email: string;

  @StringAttribute({ alias: "Username" })
  public username: string;

  @NumberAttribute({ nullable: true })
  public age?: number;

  @BooleanAttribute()
  public isActive: boolean;
}

Foreign key constraints

Referential integrity enforced via DynamoDB ConditionChecks. Invalid references fail before write.

typescript
// Fails: User with ID '123' does not exist
await Order.create({ userId: "123" });
// TransactionWriteFailedError

// @IdAttribute enforces uniqueness
await User.create({ email: "alice@co.com" }); // OK
await User.create({ email: "alice@co.com" }); // Fails

Powerful queries

Filter with $beginsWith, $contains, $or, and IN arrays. All validated against your schema.

typescript
const orders = await Customer.query(customerId, {
  skCondition: { $beginsWith: "Order" },
  filter: {
    status: ["PENDING", "SHIPPED"],
    $or: [{ city: "NYC" }, { priority: true }]
  }
});

Compile + runtime safety

TypeScript catches errors at compile time. Schema validation catches them at runtime.

typescript
// Caught by TypeScript - invalid field
await User.create({ invalidField: "value" });

// Caught at runtime - missing required field
await User.create({});
// ValidationError via Zod schema
typescript
12345678910111213141516171819202122@Entity
class Order extends MyTable {
  @ForeignKeyAttribute(() => User)
  public readonly userId: ForeignKey<User>;

  @BelongsTo(() => User, { foreignKey: "userId" })
  public readonly user: User;

  @HasAndBelongsToMany(() => Product, {
    targetKey: "orders",
    through: () => ({ joinTable: OrderProduct, foreignKey: "orderId" })
  })
  public readonly products: Product[];
}

// Eager loading in one query
const order = await Order.findById(id, {
  include: [
    { association: "user" },
    { association: "products" }
  ]
});

Single-table design, without the complexity

dyna-record automatically manages partitions for each entity and handles denormalization across related partitions. Think of it as pre-joining your data.

All operations are ACID-compliant transactions. Updates propagate atomically to every denormalized copy, ensuring consistency.

Foreign key constraints enforced. Invalid references throw TransactionWriteFailedError — caught atomically.

Dive deeper into how dyna-record handles relational modeling on the blog →

Type-aware queries

Filter fields, SK conditions, and include options are all validated at compile time. Results narrow automatically based on your query.

Type-aware SK conditions

SK conditions are validated at compile time. Results automatically narrow to the matching entity type.

typescript
// TypeScript knows this returns Order[]
const orders = await Customer.query(customerId, {
  skCondition: { $beginsWith: "Order" }
});

// Exact match - returns Invoice[]
const invoices = await Customer.query(customerId, {
  skCondition: "Invoice"
});

Type-checked filter building

Filter field names and value types are validated against your entity schema. Typos and type mismatches fail at compile time.

typescript
// Field names autocomplete in your IDE
const results = await Student.query(pk, {
  filter: {
    isActive: true,              // boolean field
    name: { $contains: "Alice" },// string operation
    status: ["ENROLLED", "GRADUATED"] // IN array
  }
});
// filter: { invalid: true } -> TS Error!

FindById with includes

Load relationships in a single query. The include option is type-checked against defined associations.

typescript
const course = await Course.findById(id, {
  include: [
    { association: "teacher" },     // HasOne
    { association: "assignments" }  // HasMany
  ]
});
// All loaded in one DynamoDB query

// { association: "typo" } -> TS Error!

Narrowed return types

Return types narrow based on your includes. Without includes, relationship fields are not accessible.

typescript
// Without includes
const course = await Course.findById(id);
course.teacher; // TS Error - not included!

// With includes - type is narrowed
const full = await Course.findById(id, {
  include: [{ association: "teacher" }]
});
full.teacher.name; // OK - Teacher is guaranteed

CRUD, simplified

Every operation is transactional, type-safe, and automatically manages timestamps and relationships.

Create

Insert with validation and foreign key checks

typescript
const student = await Student.create({
  username: "alice",
  email: "alice@school.edu"
});
// Returns Student with id, createdAt, updatedAt
Read

FindById or query with type safety

typescript
// By ID
const student = await Student.findById(id);

// Query with filter
const students = await Student.query("123", {
  filter: { isActive: true }
});
Update

Partial updates with automatic updatedAt

typescript
await Student.update(id, {
  email: "newemail@school.edu"
});
// Only specified fields updated
// updatedAt set automatically
Delete

Remove with relationship cleanup

typescript
await Student.delete(id);
// Removes the entity
// Cleans up denormalized records
// Transactional and atomic

Rich attribute types

Define your schema with decorators. Each type is validated at runtime and enforced at compile time.

@StringAttribute()string
@NumberAttribute()number
@BooleanAttribute()boolean
@DateAttribute()Date
@ObjectAttribute()typed object / Map
@IdAttributeunique ID field

Most attributes support nullable: true for optional fields. Nullable attributes are typed as T | undefined.

typescript
@Entity
class Student extends MyTable {
  @StringAttribute({ alias: "Username" })
  public username: string;

  @StringAttribute()
  public email: string;

  @NumberAttribute({ nullable: true })
  public someAttribute?: number;

  @BooleanAttribute()
  public isActive: boolean;

  @DateAttribute()
  public enrolledAt: Date;

  @IdAttribute
  @StringAttribute()
  public studentId: string; // Unique constraint
}

Get started in minutes

One base table class, one entity, and you're ready.

typescript
1234567891011121314151617181920212223242526272829303132333435363738394041import DynaRecord, {
  Table, Entity, PartitionKeyAttribute, SortKeyAttribute,
  StringAttribute, HasMany, BelongsTo, ForeignKeyAttribute,
  PartitionKey, SortKey, ForeignKey
} from "dyna-record";

@Table({ name: "my-table" })
abstract class MyTable extends DynaRecord {
  @PartitionKeyAttribute({ alias: "PK" })
  public readonly pk: PartitionKey;

  @SortKeyAttribute({ alias: "SK" })
  public readonly sk: SortKey;
}

@Entity
class User extends MyTable {
  @StringAttribute()
  public name: string;

  @HasMany(() => Order, { foreignKey: "userId" })
  public readonly orders: Order[];
}

@Entity
class Order extends MyTable {
  @ForeignKeyAttribute(() => User)
  public readonly userId: ForeignKey<User>;

  @BelongsTo(() => User, { foreignKey: "userId" })
  public readonly user: User;
}

// Create with enforced foreign key constraints
const user = await User.create({ name: "Alice" });
const order = await Order.create({ userId: user.id });

// Fetch with relationships in a single query
const alice = await User.findById(user.id, {
  include: [{ association: "orders" }]
});