DynamoDB deserves a real ORM
A strongly typed data modeler and active-record style ORM for DynamoDB — relationship-aware, schema-enforced, and fully transactional.
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.
@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.
@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.
// 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" }); // FailsPowerful queries
Filter with $beginsWith, $contains, $or, and IN arrays. All validated against your schema.
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.
// Caught by TypeScript - invalid field
await User.create({ invalidField: "value" });
// Caught at runtime - missing required field
await User.create({});
// ValidationError via Zod schema12345678910111213141516171819202122@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 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.
// 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.
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.
// 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 guaranteedCRUD, simplified
Every operation is transactional, type-safe, and automatically manages timestamps and relationships.
Insert with validation and foreign key checks
const student = await Student.create({
username: "alice",
email: "alice@school.edu"
});
// Returns Student with id, createdAt, updatedAtFindById or query with type safety
// By ID
const student = await Student.findById(id);
// Query with filter
const students = await Student.query("123", {
filter: { isActive: true }
});Partial updates with automatic updatedAt
await Student.update(id, {
email: "newemail@school.edu"
});
// Only specified fields updated
// updatedAt set automaticallyRemove with relationship cleanup
await Student.delete(id);
// Removes the entity
// Cleans up denormalized records
// Transactional and atomicRich 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 fieldMost attributes support nullable: true for optional fields. Nullable attributes are typed as T | undefined.
@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.
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" }]
});