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.

If you deploy with AWS CDK, the CDKTableV2 action derives your TableV2 keys and indexes from your Table.

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