Skip to content

Getting Started

Define domain models, register entities on a table, obtain a typed client, and run CRUD operations — all in a single file.

Pure TypeScript classes with Effect Schema — no DynamoDB concepts:

models.ts
const Role = { Admin: "admin", Member: "member" } as const
const RoleSchema = Schema.Literals(Object.values(Role))
const TaskStatus = { Todo: "todo", InProgress: "in-progress", Done: "done" } as const
const 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).


setup.ts
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:

entities.ts
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 unique constraint on email — the ORM enforces uniqueness atomically using a sentinel item
  • Users have versioned: true for optimistic locking via a version field
  • byUser index 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).


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.

main.ts
// #region program
const 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.


Wire dependencies via Effect Layers — no global config or constructor options:

run.ts
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),
)