Skip to main content
Version: v2

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)
})
info

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()
info

☝️ 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()