Simple Key-Value Store
In this guide, we’ll build a simple Key-Value Store to store Pokemons
using their pokemonId
as the key.
Create an Entity
1. Define the Table
We first have to instanciate a Table that matches our deployed configuration:
import { Table } from 'dynamodb-toolbox/table'
// 👇 Peer dependencies
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
const dynamoDBClient = new DynamoDBClient()
const PokeTable = new Table({
// 👇 DynamoDB config.
name: 'poke-table',
partitionKey: { name: 'pokemonId', type: 'string' },
// 👇 Inject the client
documentClient:
DynamoDBDocumentClient.from(dynamoDBClient)
})
DynamoDB-Toolbox does NOT hold the responsibility of actually deploying your table. This should be done by other means, like the AWS CLI, Terraform or Cloudformation.
2. Design a Schema
Let's define a schema for our Pokemons.
You can read more on the schema
syntax in the dedicated section:
// Use the shorthands: s.item(), s.string()...
import { schema, s } from 'dynamodb-toolbox/schema'
// ...or direct/deep imports
import { item } from 'dynamodb-toolbox/schema/item'
import { string } from 'dynamodb-toolbox/schema/string'
...
const pokemonSchema = item({
// 👇 Key attributes
pokemonId: string().key(),
// 👇 Defaulted
appearedAt: string().default(now),
// 👇 Always required (but defaulted as well)
updatedAt: string()
.required('always')
.putDefault(now) // Same as `.default(now)`
.updateDefault(now),
// 👇 Optional field
customName: string().optional(),
// 👇 Finite range of options
species: string().enum('pikachu', 'charizard', ...),
// 👇 Other types
level: number(),
isLegendary: boolean().optional(),
pokeTypes: set(pokeTypeSchema),
evolutions: list(evolutionSchema).default([]),
resistances: record(pokeTypeSchema, number()).partial(),
// 👇 Union of types
captureState: anyOf(
map({
status: string().enum('captured'),
capturedAt: string()
}),
map({ status: string().enum('wild') })
)
.discriminate('status')
.default({ status: 'wild' }),
// 👇 Any type (skips validation but can be casted)
metadata: s.any().optional()
})
3. Create the Entity
Now that we have our schema, we can define our Entity
!
Because we have a single Entity
in this table, we can deactivate the internal entity
attribute (mostly useful for Single Table Design) as well as the internal timestamp
attributes (equivalent to appearedAt
and updatedAt
):
import { Entity } from 'dynamodb-toolbox/entity'
const PokemonEntity = new Entity({
name: 'pokemon',
table: PokeTable,
schema: pokemonSchema,
// Deactivate the internal attributes
entityAttribute: false,
timestamps: false
})
Perform Operations
Insert an Item
In order to improve tree-shaking, Entities
only expose a single .build(...)
method that acts as a gateway to perform Actions (if you don't mind larger bundle sizes, you can use the EntityRepository
action instead):
Let's use the PutItemCommand
action to write our first item:
import { PutItemCommand } from 'dynamodb-toolbox/entity/actions/put'
const command = PokemonEntity.build(PutItemCommand)
// 👇 Validated AND type-safe!
.item({
pokemonId: 'pikachu-1',
species: 'pikachu',
level: 42,
isLegendary: false,
pokeTypes: new Set('electric'),
resistances: { rock: 3 }
})
// 👇 Inspects the DynamoDB command
console.log(command.params())
// 👇 Sends the command
await command.send()
Assuming that the poke-table
exists and that you have correct permissions, this command writes the following item to DynamoDB:
{
"pokemonId": "pikachu-1",
"species": "pikachu",
"level": 42,
"isLegendary": false,
"pokeTypes": ["electric"], // (as a Set)
"resistances": { "rock": 3 },
// Defaulted attr. are automatically filled 🙌
"appearedAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z",
"evolutions": [],
"captureState": { "status": "wild" }
}
Get an Item
Let's use the GetItemCommand
action to retrieve our freshly written pokemon:
import { GetItemCommand } from 'dynamodb-toolbox/entity/actions/get'
const command = PokemonEntity.build(GetItemCommand)
// 👇 Only (non-defaulted) key attr. are required!
.key({ pokemonId: 'pikachu-1' })
// 👇 Validated AND type-safe!
const { Item: pikachu } = await command.send()
☝️ If the fetched data is invalid, DynamoDB-Toobox throws an error and the code is interrupted.
You can refine most commands by using the .options(...)
method:
const command = PokemonEntity.build(GetItemCommand)
.key({ pokemonId: 'pikachu-1' })
// 👇 Read consistently
.options({ consistent: true })
const { Item: pikachu } = await command.send()
Update an Item
We can update our pokemon with the UpdateItemCommand
and UpdateAttributesCommand
. For instance, let's evolve our pokemon:
import {
UpdateItemCommand,
$add,
$append
} from 'dynamodb-toolbox/entity/actions/update'
const command = PokemonEntity.build(UpdateItemCommand)
// 👇 Validated AND type-safe!
.item({
// 👇 Only (non-defaulted) key & always required attr. are required!
pokemonId: 'pikachu-1',
species: 'raichu',
// 👇 Native capabilities of DynamoDB
level: $add(1),
evolutions: $append({
from: 'pikachu',
to: 'raichu',
at: new Date().toISOString()
})
})
await command.send()
Delete an Item
Finally, we can clear our pokemon with the DeleteItemCommand
:
import { DeleteItemCommand } from 'dynamodb-toolbox/entity/actions/delete'
const command = PokemonEntity.build(DeleteItemCommand)
// 👇 Same input as GetItemCommand
.key({ pokemonId: 'pikachu-1' })
await command.send()