Introduction
effect-dynamodb is an Effect TS ORM for DynamoDB providing Schema-driven entity modeling, single-table design as a first-class pattern, automatic key composition, type-safe pipeable queries, and DynamoDB client as an Effect Service.
Core Concepts
Section titled “Core Concepts”Four declarations drive everything:
Schema.Class → Entity → Table → DynamoClient.make()(domain) (model + (physical) (typed gateway) keys + indexes)- Model — A standard
Schema.Classdefining your domain fields. No DynamoDB concepts. - Entity — Binds model to a primary key + GSI indexes with system fields (timestamps, versioning, soft delete). Indexes with a
collectionproperty enable cross-entity queries. - Table — Physical DynamoDB table: schema namespace, registered entities.
- DynamoClient.make() — Typed execution gateway. Auto-discovers collections from entity indexes, returns namespaced client with
R = never.
Seven types are derived automatically from entity declarations — no manual type maintenance:
| Type | Description |
|---|---|
Entity.Model<E> | Pure domain object |
Entity.Record<E> | Domain + system metadata (version, timestamps) |
Entity.Input<E> | Creation input (no system fields) |
Entity.Update<E> | Mutable fields only (keys and immutable excluded) |
Entity.Key<E> | Primary key attributes |
Entity.Item<E> | Full unmarshalled DynamoDB item |
Entity.Marshalled<E> | DynamoDB AttributeValue format |
Key Design Principles
Section titled “Key Design Principles”- Domain models are portable. A
Userschema works with DynamoDB, SQL, or API responses. - Entities define all their indexes. Primary key + GSI access patterns on the entity. Cross-entity collections auto-discovered from matching
collectionproperties. - Convention over configuration. Declare which attributes compose keys, not how. The system handles format, delimiters, and serialization.
- Fluent query builder.
BoundQuerychains naturally —.filter().limit().collect()— with type-safe combinators. - Type safety from declarations. All types derived automatically. Change the Entity, types update everywhere.
Quick Example
Section titled “Quick Example”import { Schema, Effect, Console, Layer } from "effect"import { DynamoSchema, Table, Entity, DynamoClient } from "effect-dynamodb"
// 1. Modelclass Task extends Schema.Class<Task>("Task")({ taskId: Schema.String, projectId: Schema.String, title: Schema.NonEmptyString, status: Schema.Literals(["todo", "active", "done"]),}) {}
// 2. Entity — primary key + GSI indexesconst Tasks = Entity.make({ model: Task, entityType: "Task", primaryKey: { pk: { field: "pk", composite: ["taskId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byProject: { name: "gsi1", pk: { field: "gsi1pk", composite: ["projectId"] }, sk: { field: "gsi1sk", composite: ["status"] }, }, }, timestamps: true, versioned: true,})
// 3. Table — register entitiesconst AppSchema = DynamoSchema.make({ name: "myapp", version: 1 })const mainTable = Table.make({ schema: AppSchema, entities: { Tasks } })
// 4. Useconst program = Effect.gen(function* () { const db = yield* DynamoClient.make({ entities: { Tasks }, tables: { mainTable }, })
// Create table if it doesn't exist yield* db.tables.mainTable.describe().pipe( Effect.catchTag("ResourceNotFoundError", () => db.tables.mainTable.create() ), )
yield* db.entities.Tasks.put({ taskId: "t-1", projectId: "p-1", title: "Ship it", status: "active", })
const active = yield* db.entities.Tasks .byProject({ projectId: "p-1", status: "active" }) .collect()
yield* Console.log(active)})
Effect.runPromise(program.pipe( Effect.provide( Layer.mergeAll( DynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }), // Physical DynamoDB table name — required runtime configuration mainTable.layer({ name: "my-app-table" }), ) )))