DynamoDB storage adapter for Keyv
Features
- Built on @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb with full TypeScript support
- TTL support via DynamoDB TTL on the
expiresAtattribute. Keys set without a TTL never expire - Namespace support for key isolation across multiple Keyv instances
- Automatic table creation with
PAY_PER_REQUESTbilling mode setMany,getMany,deleteMany, andhasManybatch operations- Async
iteratorsupport with namespace-aware filtering createKeyvhelper for quick setup
Note: DynamoDB doesn't guarantee data will be deleted immediately upon expiration. See the DynamoDB TTL documentation for details.
Table of Contents
- Install
- Quick Start with createKeyv
- Usage
- Usage with Namespaces
- Usage with NestJS
- Migrating to v6
- Options
- Properties
- Methods
- constructor(options?)
- .get(key)
- .getMany(keys)
- .set(key, value, expires?)
- .setMany(entries)
- .delete(key)
- .deleteMany(keys)
- .clear()
- .has(key)
- .hasMany(keys)
- .iterator()
- .disconnect()
- .formatKey(key)
- .createKeyPrefix(key, namespace?)
- .removeKeyPrefix(key, namespace?)
- .ensureTable(tableName)
- .createTable(tableName)
- License
Install
npm install --save keyv @keyv/dynamo
Quick Start with createKeyv
import { createKeyv } from '@keyv/dynamo';
const keyv = createKeyv({ endpoint: 'http://localhost:8000' });
// set a value
await keyv.set('foo', 'bar');
// get a value
const value = await keyv.get('foo');
// set with TTL (milliseconds)
await keyv.set('foo', 'bar', 6000);
// delete a value
await keyv.delete('foo');
You can also pass options:
import { createKeyv } from '@keyv/dynamo';
const keyv = createKeyv({
endpoint: 'http://localhost:8000',
tableName: 'cacheTable',
namespace: 'my-app',
});
Usage
import Keyv from 'keyv';
import KeyvDynamo from '@keyv/dynamo';
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
const keyv = new Keyv(store);
// set a value
await keyv.set('foo', 'bar');
// set a value with TTL (in milliseconds)
await keyv.set('foo', 'bar', 6000);
// get a value
const value = await keyv.get('foo');
// delete a value
await keyv.delete('foo');
// clear all values
await keyv.clear();
Usage with Namespaces
import Keyv from 'keyv';
import KeyvDynamo from '@keyv/dynamo';
// Keyv passes its namespace to the adapter, which stores `foo` as `namespace1:foo`.
// Give each Keyv instance its own adapter.
const store1 = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
const keyv1 = new Keyv(store1, { namespace: 'namespace1' });
const store2 = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
const keyv2 = new Keyv(store2, { namespace: 'namespace2' });
// keys are isolated by namespace
await keyv1.set('foo', 'bar1');
await keyv2.set('foo', 'bar2');
const value1 = await keyv1.get('foo'); // 'bar1'
const value2 = await keyv2.get('foo'); // 'bar2'
Usage with NestJS
Note:
cache-manager7 depends on Keyv v5. It reads expiry data withstore.get(key, { raw: true }), which a v6Keyvignores, sottl()and the refresh logic inwrap()stop working with a v6 store. Check which Keyv version yourcache-managerneeds withnpm view cache-manager dependencies.keyv. See Libraries That Embed Keyv v5.
Since DynamoDB has a 400KB limit per item, compressing data can help in some cases.
With a payload less than or equal to 400KB
import { Keyv } from 'keyv'
import { KeyvDynamo } from '@keyv/dynamo'
import { CacheModule } from '@nestjs/cache-manager'
import { Module } from '@nestjs/common'
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => {
return {
stores: [
new Keyv({
store: new KeyvDynamo({
tableName: 'TableName',
}),
}),
],
}
},
}),
],
})
export class InfrastructureModule {}
With a payload greater than 400KB
import { Keyv } from 'keyv'
import KeyvBrotli from '@keyv/compress-brotli'
import { KeyvDynamo } from '@keyv/dynamo'
import { CacheModule } from '@nestjs/cache-manager'
import { Module } from '@nestjs/common'
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => {
return {
stores: [
new Keyv({
store: new KeyvDynamo({
tableName: 'TableName',
}),
compression: new KeyvBrotli(),
}),
],
}
},
}),
],
})
export class InfrastructureModule {}
Migrating to v6
- Keys written without a TTL no longer expire. In v5, every key written without a TTL got an expiry six hours after the write. In v6, such a key has no expiry and is kept until it is deleted. To keep a default expiry, set Keyv's
ttloption:
const keyv = new Keyv(store, { ttl: 6 * 60 * 60 * 1000 });
sixHoursInMillisecondswas removed. Remove any code that reads or assignsstore.sixHoursInMilliseconds.- Keys written by v5 keep the expiry they were written with until they are written again.
- The
ttlSupportandoptsproperties were removed. Use thetableNameandendpointproperties instead. - The adapter's
settakes an absoluteexpires. v5'sset(key, value, ttl)took a relativettl. This only affects code that calls the adapter directly. See .set(key, value, expires?). - Keys written by v5 need matching namespace settings. Keyv v6 has no default namespace.
- v5's default setup,
new Keyv(new KeyvDynamo(options)), storedfooaskeyv:foo. v6 joins the namespace and key with::, so read those keys withnew Keyv(new KeyvDynamo({ ...options, namespaceSeparator: ':' }), { namespace: 'keyv' }). If you set your own namespace in v5, pass that one instead. - Keys written through v5's
createKeyv()were stored without a prefix, even when you passed anamespace. The same is true when v5 ran withuseKeyPrefix: false. Read those keys without a namespace. Without a namespace,clear()deletes every item in the table.
- v5's default setup,
See the v5 to v6 migration guide for more.
Options
Options extend DynamoDBClientConfig so all AWS SDK options (endpoint, region, credentials, etc.) are supported.
| Option | Type | Default | Description |
|---|---|---|---|
tableName |
string |
'keyv' |
The DynamoDB table name. Created automatically if it doesn't exist. |
namespace |
string |
undefined |
Key prefix for namespace isolation |
namespaceSeparator |
string |
'::' |
Separator placed between the namespace and key |
endpoint |
string |
— | The DynamoDB endpoint URL (e.g., 'http://localhost:8000' for local development) |
uri |
string |
— | Alias for endpoint (for consistency with other Keyv adapters). endpoint takes precedence when both are set. |
region |
string |
— | The AWS region (e.g., 'us-east-1') |
import KeyvDynamo from '@keyv/dynamo';
// Using an endpoint string
const store = new KeyvDynamo('http://localhost:8000');
// Using an options object
const store2 = new KeyvDynamo({ endpoint: 'http://localhost:8000', tableName: 'cacheTable' });
Properties
.client
The underlying DynamoDBDocument client instance. Can be used to access the DynamoDB client directly.
| Type | Default |
|---|---|
DynamoDBDocument |
Created from the options |
.namespace
Key prefix for namespace isolation. When set, all keys are prefixed with namespace:.
| Type | Default |
|---|---|
string | undefined |
undefined |
.namespaceSeparator
The separator between the namespace and key.
| Type | Default |
|---|---|
string |
'::' |
.tableName
The DynamoDB table name in use. Read-only.
| Type | Default |
|---|---|
string |
'keyv' |
.endpoint
The configured DynamoDB endpoint URL, if one was provided. Read-only.
| Type | Default |
|---|---|
string | undefined |
undefined |
Methods
constructor(options?)
Creates a new KeyvDynamo instance. Automatically creates the DynamoDB table if it doesn't exist.
options— AKeyvDynamoOptionsobject or an endpoint string. Defaults to{ tableName: 'keyv' }.
import KeyvDynamo from '@keyv/dynamo';
// Using an endpoint string
const store = new KeyvDynamo('http://localhost:8000');
// Using an options object
const store2 = new KeyvDynamo({ endpoint: 'http://localhost:8000', tableName: 'cacheTable' });
.get(key)
Retrieves a value from DynamoDB. Returns the stored value or undefined if the key does not exist.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('foo', 'bar');
const result = await store.get('foo'); // 'bar'
.getMany(keys)
Retrieves multiple values from DynamoDB. Returns an array of stored data corresponding to each key.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('key1', 'value1');
await store.set('key2', 'value2');
const results = await store.getMany(['key1', 'key2']);
.set(key, value, expires?)
Stores a value in DynamoDB. With expires, the item records it in expiresAt (seconds), which DynamoDB TTL uses to delete the item, and in expiresAtMs (milliseconds), which reads check so an expired item is never returned. Without expires, the item has no expiry and is kept until it is deleted. Returns true on success, false on failure.
When you call the adapter directly, the third argument is an absolute
expirestimestamp (Unix ms since epoch), not a relative duration. Through Keyv (keyv.set(key, value, ttl)) you still pass a relative TTL — Keyv converts it toexpiresfor you.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('foo', 'bar'); // never expires
await store.set('foo', 'bar', Date.now() + 60000); // expires in ~60 seconds
.setMany(entries)
Stores multiple values in DynamoDB using BatchWriteItem in chunks of 25. Each entry is a KeyvStorageEntry<Value> object ({ key: string, value: Value, expires?: number }), where expires is an absolute Unix ms timestamp as in .set(), and Value is inferred from the entries provided. Returns a boolean[] with per-entry success tracking — any items reported as UnprocessedItems by DynamoDB are marked as false.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
const results = await store.setMany([
{ key: 'key1', value: 'value1' },
{ key: 'key2', value: 'value2', expires: Date.now() + 60000 },
]); // [true, true]
.delete(key)
Deletes a key from DynamoDB. Returns true if the key was deleted, false otherwise.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('foo', 'bar');
const deleted = await store.delete('foo'); // true
.deleteMany(keys)
Deletes multiple keys from DynamoDB. Returns a boolean[] indicating whether each key was deleted.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('key1', 'value1');
await store.set('key2', 'value2');
const results = await store.deleteMany(['key1', 'key2']); // [true, true]
.clear()
Clears data from DynamoDB. If a namespace is set, only keys with the namespace prefix are deleted. Otherwise, all keys are deleted. It scans the whole table, one page per 1 MB of data, so it reads every item in the table even when a namespace is set.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.clear();
.has(key)
Checks whether a key exists in DynamoDB.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('foo', 'bar');
const exists = await store.has('foo'); // true
const missing = await store.has('baz'); // false
.hasMany(keys)
Checks whether multiple keys exist in DynamoDB. Returns an array of booleans corresponding to each key.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('key1', 'value1');
await store.set('key2', 'value2');
const results = await store.hasMany(['key1', 'key2', 'key3']); // [true, true, false]
.iterator()
Returns an async iterator over all [key, value] pairs in the store. If a namespace is set, only keys with that namespace are yielded and the namespace prefix is removed from the returned keys. The namespace does not need to be passed in — it uses the namespace configured on the adapter. Expired entries are skipped and deleted.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.set('key1', 'value1');
await store.set('key2', 'value2');
for await (const [key, value] of store.iterator()) {
console.log(key, value);
}
.disconnect()
Disconnects from the DynamoDB client. This is a no-op for DynamoDB since it communicates over HTTP requests and does not maintain a persistent connection.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.disconnect();
.formatKey(key)
Formats a key by prepending the namespace if one is set. A key that already starts with the namespace prefix gets it again, so myapp:foo and foo stay separate keys.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
store.formatKey('foo'); // 'foo'
store.namespace = 'myapp';
store.formatKey('foo'); // 'myapp:foo'
store.formatKey('myapp:foo'); // 'myapp:myapp:foo'
.createKeyPrefix(key, namespace?)
Creates a prefixed key by prepending the namespace and separator. Returns the key as-is if no namespace is provided.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
store.createKeyPrefix('key', 'ns'); // 'ns::key'
store.createKeyPrefix('key'); // 'key'
.removeKeyPrefix(key, namespace?)
Removes the namespace prefix from the start of a key. Returns the key as-is if no namespace is provided or the key does not start with the prefix.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
store.removeKeyPrefix('ns::key', 'ns'); // 'key'
store.removeKeyPrefix('key'); // 'key'
.ensureTable(tableName)
Ensures the DynamoDB table exists and is active. If the table is in CREATING status, waits for it to become active. If it doesn't exist, creates it.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.ensureTable('my-table');
.createTable(tableName)
Creates a new DynamoDB table with TTL support enabled on the expiresAt attribute. Uses PAY_PER_REQUEST billing mode.
const store = new KeyvDynamo({ endpoint: 'http://localhost:8000' });
await store.createTable('my-table');