Example: Projections
An employee directory that demonstrates projection expressions — telling DynamoDB to return only specific attributes from an item. Projections reduce both network transfer and read capacity costs when you only need a subset of fields.
What you’ll learn:
Projection.projection()— build aProjectionExpressionfrom attribute names- Applying projections to
GetItemandQueryoperations viaDynamoClient - How
ExpressionAttributeNamessafely handles DynamoDB reserved words - Trade-off: projections bypass Entity schema decode (partial records, not typed models)
Step 1: Model
Section titled “Step 1: Model”A domain model with several fields — projections let us fetch only what we need:
class Employee extends Schema.Class<Employee>("Employee")({ employeeId: Schema.String, name: Schema.NonEmptyString, email: Schema.String, department: Schema.String, salary: Schema.Number, title: Schema.String,}) {}Step 2: Schema, Entity, and Table
Section titled “Step 2: Schema, Entity, and Table”const AppSchema = DynamoSchema.make({ name: "proj-demo", version: 1 })
const Employees = Entity.make({ model: Employee, entityType: "Employee", primaryKey: { pk: { field: "pk", composite: ["employeeId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byDepartment: { name: "gsi1", pk: { field: "gsi1pk", composite: ["department"] }, sk: { field: "gsi1sk", composite: ["employeeId"] }, }, }, timestamps: true,})
const MainTable = Table.make({ schema: AppSchema, entities: { Employees } })Step 3: Seed Data
Section titled “Step 3: Seed Data”const db = yield* DynamoClient.make({ entities: { Employees },})
const client = yield* DynamoClientconst tableConfig = yield* MainTable.Tag
yield* db.tables["proj-demo-table"]!.create()
yield* db.entities.Employees.put({ employeeId: "e-1", name: "Alice", email: "alice@company.com", department: "engineering", salary: 120000, title: "Senior Engineer",})yield* db.entities.Employees.put({ employeeId: "e-2", name: "Bob", email: "bob@company.com", department: "engineering", salary: 95000, title: "Engineer",})yield* db.entities.Employees.put({ employeeId: "e-3", name: "Charlie", email: "charlie@company.com", department: "marketing", salary: 85000, title: "Marketing Lead",})Step 4: Operations
Section titled “Step 4: Operations”Building a Projection
Section titled “Building a Projection”Projection.projection() takes an array of attribute names and returns an object with expression (the ProjectionExpression string) and names (the ExpressionAttributeNames map). All attribute names are aliased with # prefixes to safely handle DynamoDB reserved words:
const nameAndDept = Projection.projection(["name", "department"])// nameAndDept.expression → "#name, #department"// nameAndDept.names → { "#name": "name", "#department": "department" }Projected GetItem
Section titled “Projected GetItem”Apply the projection to a GetItem call via DynamoClient. DynamoDB returns only the requested attributes, reducing both network transfer and read capacity:
const nameAndDept = Projection.projection(["name", "department"])
const projResult = yield* client.getItem({ TableName: tableConfig.name, Key: toAttributeMap({ pk: "$proj-demo#v1#employee#e-1", sk: "$proj-demo#v1#employee", }), ProjectionExpression: nameAndDept.expression, ExpressionAttributeNames: nameAndDept.names,})
if (projResult.Item) { const _item = fromAttributeMap(projResult.Item)}Note that projected results are partial records, not typed model instances. The Entity schema decode is bypassed because the item lacks required fields. Use projections when you need raw efficiency (dashboards, lists, summaries).
Projected Query
Section titled “Projected Query”Projections work with queries too. Here we fetch only name and title for all engineering employees:
const listProjection = Projection.projection(["name", "title"])
const queryResult = yield* client.query({ TableName: tableConfig.name, IndexName: "gsi1", KeyConditionExpression: "#pk = :pk", ExpressionAttributeNames: { "#pk": "gsi1pk", ...listProjection.names, }, ExpressionAttributeValues: toAttributeMap({ ":pk": "$proj-demo#v1#employee#engineering", }), ProjectionExpression: listProjection.expression,})
for (const rawItem of queryResult.Items ?? []) { const item = fromAttributeMap(rawItem)}When combining projections with queries, merge the projection’s names into the query’s ExpressionAttributeNames using the spread operator.
Edge Case: Empty Projection
Section titled “Edge Case: Empty Projection”An empty projection produces an empty expression and empty names map. DynamoDB ignores an empty ProjectionExpression, so all attributes are returned:
const empty = Projection.projection([])When to Use Projections
Section titled “When to Use Projections”| Scenario | Recommendation |
|---|---|
| Dashboard showing name + status only | Use projection — skip large fields |
| Full entity CRUD (get, put, update) | Use Entity.get() — full schema decode |
| List views with a few columns | Use projection — reduce network transfer |
| Items with large string/binary fields | Use projection to exclude them |
| Need typed model instances | Use Entity.get() or Entity.query — schema-decoded |
Projections trade type safety for efficiency. Use them at the edges (API responses, UI data) where you control the shape, and use full Entity operations in domain logic where type safety matters.
Running the Example
Section titled “Running the Example”The complete runnable example is at examples/projections.ts in the repository.
Start DynamoDB Local:
docker run -d -p 8000:8000 amazon/dynamodb-localRun the example:
npx tsx examples/projections.tsLayer Setup
Section titled “Layer Setup”const AppLayer = Layer.mergeAll( DynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }), MainTable.layer({ name: "proj-demo-table" }),)
const main = program.pipe(Effect.provide(AppLayer))
Effect.runPromise(main).then( () => console.log("\nDone."), (err) => console.error("\nFailed:", err),)Key Takeaways
Section titled “Key Takeaways”| Concept | How it’s used |
|---|---|
| Projection.projection() | Builds ProjectionExpression + ExpressionAttributeNames from attribute names |
| ExpressionAttributeNames | All attributes aliased with # prefix — safely handles reserved words |
| Projected GetItem | Pass ProjectionExpression to DynamoClient.getItem for selective retrieval |
| Projected Query | Merge projection names with query ExpressionAttributeNames via spread |
| Partial records | Projected results bypass Entity schema decode — not typed model instances |
| Empty projection | Produces empty string — DynamoDB returns all attributes |
What’s Next?
Section titled “What’s Next?”- Example: Conditional Writes & Filters — Conditional puts, updates, deletes, and filter operators
- Example: Scan Operations — Full-table scans with filters, limits, and entity-type isolation
- Queries Guide — Index queries, pagination, sort key conditions, and collections
- Example: Human Resources — Full single-table design with 3 entities, collections, and transactions