DynamoDB Entity Store
A lightly opinionated DynamoDB library for TypeScript & JavaScript applications
DynamoDB is a cloud-hosted NoSQL database from AWS (Amazon Web Services).
AWS provides a fairly basic SDK for using DynamoDB from TypeScript and JavaScript applications.
DynamoDB Entity Store is a TypeScript / JavaScript library that provides a slightly higher abstraction.
It is definitely not an "ORM library", and nor does it try to hide DynamoDB's fundamental behavior.
Therefore it requires that you still need a good understanding of how to work with DynamoDB in general - I strongly recommend
Alex DeBrie's book on the subject.
Entity Store provides the following:
- More simple write operations, handling repetitive aspects like configuration of table names, attributes for keys,
and setting metadata attributes like "Last Updated" time, and "TTL" (Time To Live).
- A cleaner interface for read operations (get, scans, and queries), including contextual result types, parsing, and automatic "entity type" filtering.
- Automatically load all available items for query and scan operations (auto-pagination), or choose page-by-page results.
- Simple setup when using "Single Table Design" ...
- ... but also allows non-standard and/or multi-table designs.
- A pretty-much-complete coverage of the entire DynamoDB API / SDK, including batch and transaction
operations, and options for diagnostic metadata (e.g. "consumed capacity").
- ... all without any runtime library dependencies, apart from the official AWS DynamoDB libraries (AWS SDK V3).
This library is named Entity Store since it's based on the idea that your DynamoDB tables store one or many collections of related records, and each
collection has the same persisted structure.
Each of these collections is an Entity, which also corresponds to a Domain Type within your application's code.
If you're deciding on what DynamoDB library to choose you may want to also consider the following alternatives:
- DynamoDB Toolbox - Jeremy's library was the biggest inspiration to
my own work. When I was first working on these ideas in 2022 DynamoDB Toolbox didn't support AWS SDK V3, but it does now.
- One Table
The rest of this README provides an overview of how to use the library. For more details see:
[!IMPORTANT]
As of November 2025, and Version 2, this library is ES Modules / ESM only.
If you require CJS support, or support back to Node 16, then you can use Version 1, which is in NPM, and the
source code is here on the v1 branch.
If you have questions or feedback, please use the project issues, or feel free to email me: mike@symphonia.io .
Example 1: Single Table Design, without indexes
Let's say we have the following Domain Type
export interface Sheep {
breed: string
name: string
ageInYears: number
}
And let's say we want to store these Sheep in DynamoDB like this:
PK | SK | breed | name | ageInYears |
|---|
SHEEP#BREED#merino | NAME#shaun | merino | shaun | 3 |
We are using a "Single Table Design" configuration here as follows:
- The table Partition Key is named
PK
- The table Sort Key is named
SK
Now we add / install Entity Store in the usual way from NPM , e.g.
% npm install @symphoniacloud/dynamodb-entity-store
Let's assume that our DynamoDB Table is named AnimalsTable.
We can create an entity store by using
the createStore
and createStandardSingleTableConfig functions:
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'))
entityStore is an object that implements
the AllEntitiesStore type.
We want to use the for(entity) method on this type, but it requires that we pass an Entity object. Here's the Entity implementation for Sheep:
export const SHEEP_ENTITY = createEntity(
// Type name
'sheep',
// Type predicate, used in standard parser
function(x: DynamoDBValues): x is Sheep {
const candidate = x as Sheep
return candidate.breed !== undefined &&
candidate.name !== undefined &&
candidate.ageInYears !== undefined
},
// Generate a value for PK attribute based on a subset of fields in Sheep
({ breed }: Pick<Sheep, 'breed'>) => `SHEEP#BREED#${breed}`,
// Generate a value for SK attribute based on a subset of fields in Sheep
({ name }: Pick<Sheep, 'name'>) => `NAME#${name}`
)
We only need to create this object once per type of entity in our application, and usually can be stateless, so you might want to define each of them as a global constant.
Each entity object is responsible for:
- Defining the name of the entity type
- Expressing how to convert a DynamoDB record to a well-typed object ("parsing")
- Creating partition key and sort key values
- Optional: express how to convert an object to a DynamoDB record ("formatting")
- Optional: Create Global Secondary Index (GSI) key values
A complete discussion of Entities is available in the manual.
We can now call .for(...) on our entity store. This returns an object that implements SingleEntityOperations - this is the object that you'll likely work with most when using this library.
For example, let's store some sheep by using SingleEntityOperations.put:
const sheepOperations = entityStore.for(SHEEP_ENTITY)
await sheepOperations.put({ breed: 'merino', name: 'shaun', ageInYears: 3 })
await sheepOperations.put({ breed: 'merino', name: 'bob', ageInYears: 4 })
await sheepOperations.put({ breed: 'suffolk', name: 'alison', ageInYears: 2 })
Now our DynamoDB table contains the following records:
PK | SK | breed | name | ageInYears | _et | _lastUpdated |
|---|
SHEEP#BREED#merino | NAME#shaun | merino | shaun | 3 | sheep | 2023-08-21T15:41:53.566Z |
SHEEP#BREED#merino | NAME#bob | merino | bob | 4 | sheep | 2023-08-21T15:41:53.956Z |
SHEEP#BREED#suffolk | NAME#alison | suffolk | alison | 2 | sheep | 2023-08-21T15:41:53.982Z |
What's going on here is that Entity Store is calling put on the underlying DynamoDB table for each sheep. Each DynamoDB record includes all the fields on Sheep (don't worry, that's overridable), along with the following extra fields:
PK - generated by calling SHEEP_ENTITY.pk(...) for each sheep
SK - generated by calling SHEEP_ENTITY.sk(...) for each sheep
_et - the value of SHEEP_ENTITY.type
_lastUpdated - the current date-time
Now let's retrieve Shaun so we can find out how old he is.
const shaun: Sheep = await sheepOperations.getOrThrow({ breed: 'merino', name: 'shaun' })
console.log(`shaun is ${shaun.ageInYears} years old`)
Output:
shaun is 3 years old
The underlying call to DynamoDB's get API needs to include both PK and SK fields. We use the same pk() and sk()
generator functions as we did for put to calculate these, but based on just the breed and name identifier values for
a sheep.
Note that unlike the AWS SDK's get operation here we get a well-typed result. This is possible because of the
type-predicate function that we included when creating SHEEP_ENTITY .
In this basic example we assume that the underlying DynamoDB record
attributes include all the properties on a sheep object, but it's possible to customize parsing and/or derive values
from the PK and SK fields if you want to optimize your DynamoDB table - I'll show that a little later.
We can get all of our merino-breed sheep by running a query against the table's Partition Key:
const merinos: Sheep[] = await sheepOperations.queryAllByPk({ breed: 'merino' })
for (const sheep of merinos) {
console.log(`${sheep.name} is ${sheep.ageInYears} years old`)
}
Output:
bob is 4 years old
shaun is 3 years old
Similar to getOrThrow we need to provide the value for the PK field, and again under the covers Entity Store is
calling pk() on SHEEP_ENTITY . Since this query only filters on the PK attribute we only need to provide breed
when we call queryAllByPk().
The result of the query operation is a well-typed list of items - again by using the parser / type
predicate function on SHEEP_ENTITY .
A lot of the power of using DynamoDB comes from using the Sort Key as part of a query.
Now let's find all sheep of a particular breed, but also where name is in a particular alphabetic range:
function rangeWhereNameBetween(nameRangeStart: string, nameRangeEnd: string) {
return rangeWhereSkBetween(`NAME#${nameRangeStart}`, `NAME#${nameRangeEnd}`)
}
const earlyAlphabetMerinos = await sheepOperations.queryAllByPkAndSk(
{ breed: 'merino' },
rangeWhereNameBetween('a', 'n')
)
for (const sheep of earlyAlphabetMerinos) {
console.log(sheep.name)
}
Output:
bob
To query with a Sort Key we must first specify the partition key - which here is the same as our first example - and
then must specify a sort key query range.
There are a few helpers in Entity Store for this - in our case we use the rangeWhereSkBetween helper which ends up generating the condition
expression PK = :pk and #sk BETWEEN :from AND :to .
Turning on logging
When you're working on setting up your entities and queries you'll often want to see what Entity Store is actually
doing. You can do this by turning on logging:
const entityStore = createStore(createStandardSingleTableConfig('AnimalsTable'), { logger: consoleLogger })
With this turned on we can see the output from our last query:
DynamoDB Entity Store DEBUG - Attempting to query or scan entity sheep [{"useAllPageOperation":false,"operationParams":{"TableName":"AnimalsTable","KeyConditionExpression":"PK = :pk and #sk BETWEEN :from AND :to","Exp