Getting Started
Define domain models, register entities on a table, obtain a typed client, and run CRUD operations — all in a single file.
Step 1: Domain Models
Section titled “Step 1: Domain Models”Pure TypeScript classes with Effect Schema — no DynamoDB concepts:
const Role = { Admin: "admin", Member: "member" } as constconst RoleSchema = Schema.Literals(Object.values(Role))
const TaskStatus = { Todo: "todo", InProgress: "in-progress", Done: "done" } as constconst TaskStatusSchema = Schema.Literals(Object.values(TaskStatus))
class User extends Schema.Class<User>("User")({ userId: Schema.String, email: Schema.String, displayName: Schema.NonEmptyString, role: RoleSchema, createdBy: Schema.String,}) {}
const UserModel = DynamoModel.configure(User, { createdBy: { immutable: true },})
class Task extends Schema.Class<Task>("Task")({ taskId: Schema.String, userId: Schema.String, title: Schema.NonEmptyString, status: TaskStatusSchema, priority: Schema.Number,}) {}Models are pure domain objects — no DynamoDB attributes, no key definitions. DynamoModel.configure marks createdBy as immutable (can’t be changed after creation).
Step 2: Application Schema
Section titled “Step 2: Application Schema”const AppSchema = DynamoSchema.make({ name: "starter", version: 1 })DynamoSchema controls the key prefix format: $starter#v1#entity_type#attr_values. The version enables safe schema evolution.
Step 3: Entity, Table, and Index Definitions
Section titled “Step 3: Entity, Table, and Index Definitions”Entities define their primary key and GSI indexes. The table declares which entities it contains:
const Users = Entity.make({ model: UserModel, entityType: "User", primaryKey: { pk: { field: "pk", composite: ["userId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byEmail: { name: "gsi1", pk: { field: "gsi1pk", composite: ["email"] }, sk: { field: "gsi1sk", composite: [] }, }, }, unique: { email: ["email"] }, timestamps: true, versioned: true,})
const Tasks = Entity.make({ model: Task, entityType: "Task", primaryKey: { pk: { field: "pk", composite: ["taskId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byUser: { name: "gsi1", pk: { field: "gsi1pk", composite: ["userId"] }, sk: { field: "gsi1sk", composite: ["status"] }, }, }, timestamps: true,})
const MainTable = Table.make({ schema: AppSchema, entities: { Users, Tasks } })Key design decisions:
- Users have a
uniqueconstraint onemail— the ORM enforces uniqueness atomically using a sentinel item - Users have
versioned: truefor optimistic locking via aversionfield byUserindex on Tasks defines a GSI access pattern — query tasks by userId, sorted by status- Entities declare their own GSI indexes — each entity is self-contained with primary key + indexes
Entity.make returns a definition — it describes how the entity maps to DynamoDB but does not provide executable operations. To actually read and write data, you need to obtain a typed client via DynamoClient.make (next step).
Step 4: Typed Client and Use
Section titled “Step 4: Typed Client and Use”DynamoClient.make({ entities }) is the typed execution gateway. It resolves dependencies, auto-discovers collections from entity index collection properties, and returns a client with bound entities where every method is ready to call.
// #region programconst program = Effect.gen(function* () { // Get typed client — binds all entities and collections const db = yield* DynamoClient.make({ entities: { Users, Tasks }, })
// --- Create the table (derived from entity definitions) --- yield* Console.log("=== Setup ===\n")
yield* db.tables["starter-table"]!.create() yield* Console.log("Table created\n")
// --- Put: create items --- yield* Console.log("=== Put ===\n")
const alice = yield* db.entities.Users.put({ userId: "u-alice", email: "alice@example.com", displayName: "Alice", role: "admin", createdBy: "system", }) yield* Console.log(`Created user: ${alice.displayName} (${alice.email})`)
yield* db.entities.Tasks.put({ taskId: "t-1", userId: "u-alice", title: "Learn effect-dynamodb", status: "todo", priority: 1, })
yield* db.entities.Tasks.put({ taskId: "t-2", userId: "u-alice", title: "Build something cool", status: "in-progress", priority: 2, }) yield* Console.log("Created tasks: t-1, t-2\n")
// --- Get: read by primary key --- yield* Console.log("=== Get ===\n")
const user = yield* db.entities.Users.get({ userId: "u-alice" }) yield* Console.log(`Got user: ${user.displayName} (role: ${user.role})`)
const task = yield* db.entities.Tasks.get({ taskId: "t-1" }) yield* Console.log(`Got task: "${task.title}" (status: ${task.status})\n`)
// --- Update --- yield* Console.log("=== Update ===\n")
// Note: status is a GSI composite, so we must also provide userId // (all composites for the GSI) so the key can be recomposed const updated = yield* db.entities.Tasks.update({ taskId: "t-1" }).set({ status: "done", userId: "u-alice", }) yield* Console.log(`Updated task: "${updated.title}" -> ${updated.status}\n`)
// --- Query: tasks by user via GSI index accessor --- yield* Console.log("=== Query: Tasks by User (GSI) ===\n")
const aliceTasks = yield* db.entities.Tasks.byUser({ userId: "u-alice" }).collect() yield* Console.log(`Alice's tasks (${aliceTasks.length}):`) for (const t of aliceTasks) { yield* Console.log(` ${t.taskId}: "${t.title}" — ${t.status}`) } yield* Console.log("")
// --- Delete --- yield* Console.log("=== Delete ===\n")
yield* db.entities.Tasks.delete({ taskId: "t-1" }) yield* db.entities.Tasks.delete({ taskId: "t-2" }) yield* db.entities.Users.delete({ userId: "u-alice" }) yield* Console.log("Deleted all items\n")
// --- Cleanup --- yield* db.tables["starter-table"]!.delete() yield* Console.log("Table deleted.")})DynamoClient.make({ entities }) requires DynamoClient and TableConfig in the Effect context — you provide these as Layers when you run the program.
Step 5: Layer Setup and Running
Section titled “Step 5: Layer Setup and Running”Wire dependencies via Effect Layers — no global config or constructor options:
const AppLayer = Layer.mergeAll( DynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }), MainTable.layer({ name: "starter-table" }),)
const main = program.pipe(Effect.provide(AppLayer))
Effect.runPromise(main).then( () => console.log("\nDone."), (err) => console.error("Failed:", err),)Next Steps
Section titled “Next Steps”- CRUD Operations — Deep dive into put, get, update, delete with combinators
- Indexes & Queries — Design GSI access patterns with entity-level indexes
- Expressions — Conditions, filters, and projections