Durgesh Rai

DynamoDB: Reading & Writing in Code

The SDK Document Client, conditional writes, and update expression placeholders, all in one place.

September 26, 20264 min read3 / 4

This page covers the AWS SDK side of writing to DynamoDB: what to actually write in a Lambda function. See DynamoDB: Data Modeling for single-table design and keys first.

Using the SDK: the Document Client, not the raw client

AWS SDK for JavaScript v3 talks to DynamoDB through two layers, and picking the wrong one means hand-writing work the other layer already does.

  • @aws-sdk/client-dynamodb — the low-level client. Every value sent or received is a raw DynamoDB attribute value, { S: "hello" } for a string, { N: "42" } for a number, not a plain JavaScript string or number. Marshalling a JS value into that shape before every write, and unmarshalling it back after every read, is on you.
  • @aws-sdk/lib-dynamodb — wraps the low-level client with a DynamoDBDocumentClient, built with DynamoDBDocumentClient.from(client). It marshals and unmarshals automatically, so a write takes a plain { id: "123", title: "..." } object and a read hands one back, no attribute-value wrapper in sight.

Default to the Document Client. The only reason to reach for the raw client is needing that attribute-value shape directly, which is rare application code.

Both packages are only installed locally as devDependencies. The Node.js 20.x Lambda runtime ships AWS SDK v3 built in. A deployed function already has it available at runtime without it being part of the uploaded bundle. Installing it locally exists purely so imports resolve and types work while writing the code.

Conditional writes

attribute_not_exists makes an insert idempotent for free: write only if this record doesn't already exist, and a duplicate insert attempt fails cleanly instead of creating a second copy. It only covers the insert case. An action with a side effect (charging a card) needs an explicit idempotency key and lookup table instead. Lambda Powertools ships a utility for that rather than hand-rolling it.

attribute_exists is the mirror image, for update and delete. An update or delete should only succeed against a record that's actually there. attribute_exists(<partition key>) as the condition expression makes that explicit instead of assumed. Without it, an update against a missing key silently creates a new, mostly-empty item instead of failing. A delete against a missing key just succeeds having done nothing. Neither is usually what the caller meant.

A plain read behaves differently: it doesn't throw on a miss. A GetCommand against a key that doesn't exist returns a response with no Item field at all, not an error. Checking for that absence explicitly (if (!data.Item)) before using the result is the caller's job. Skip that check, and an unguarded read against a missing key fails later, further down the code. The error it throws then is a confusing one about reading a property off undefined, not a clear one about the record being missing.

Update expressions need placeholders for reserved words

An UpdateCommand's UpdateExpression describes what changes, SET title = :title, body = :body, but writing attribute names directly into that string risks colliding with DynamoDB's own reserved words (status, name, size, and dozens more). The fix is a level of indirection: reference every attribute name and value through a placeholder instead of writing either literally into the expression.

JavaScript
UpdateExpression: "SET #title = :title, #body = :body", ExpressionAttributeNames: { "#title": "title", "#body": "body" }, ExpressionAttributeValues: { ":title": data.title, ":body": data.body },

ExpressionAttributeNames maps a #-prefixed placeholder to the real attribute name; ExpressionAttributeValues maps a :-prefixed placeholder to the real value. Neither placeholder can ever collide with a reserved word, since it's never treated as an attribute name literal. Using this pattern by default, even for attribute names that aren't currently reserved, avoids a later DynamoDB update turning a working expression into a broken one.

The Essentials

  1. Default to the Document Client, not the raw client, for plain JS objects instead of attribute-value wrappers.
  2. attribute_not_exists makes inserts idempotent for free; attribute_exists guards update and delete against acting on a record that isn't there.
  3. A plain read doesn't throw on a miss. Always check if (!data.Item) explicitly.
  4. Reference attribute names and values through placeholders in update expressions, always, to avoid reserved-word collisions.

Next: querying and pagination

DynamoDB: Querying, Pagination & Capacity covers Scan vs Query, pagination, Streams, capacity modes, and TTL.

Where this showed up