Skip to content

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 a ProjectionExpression from attribute names
  • Applying projections to GetItem and Query operations via DynamoClient
  • How ExpressionAttributeNames safely handles DynamoDB reserved words
  • Trade-off: projections bypass Entity schema decode (partial records, not typed models)

A domain model with several fields — projections let us fetch only what we need:

models.ts
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,
}) {}

entities.ts
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 } })

seed.ts
const db = yield* DynamoClient.make({
entities: { Employees },
})
const client = yield* DynamoClient
const 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",
})

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" }

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).

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.

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([])

ScenarioRecommendation
Dashboard showing name + status onlyUse projection — skip large fields
Full entity CRUD (get, put, update)Use Entity.get() — full schema decode
List views with a few columnsUse projection — reduce network transfer
Items with large string/binary fieldsUse projection to exclude them
Need typed model instancesUse 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.


The complete runnable example is at examples/projections.ts in the repository.

Start DynamoDB Local:

Terminal window
docker run -d -p 8000:8000 amazon/dynamodb-local

Run the example:

Terminal window
npx tsx examples/projections.ts
main.ts
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),
)

ConceptHow it’s used
Projection.projection()Builds ProjectionExpression + ExpressionAttributeNames from attribute names
ExpressionAttributeNamesAll attributes aliased with # prefix — safely handles reserved words
Projected GetItemPass ProjectionExpression to DynamoClient.getItem for selective retrieval
Projected QueryMerge projection names with query ExpressionAttributeNames via spread
Partial recordsProjected results bypass Entity schema decode — not typed model instances
Empty projectionProduces empty string — DynamoDB returns all attributes