DynamoDB ORM or the raw SDK: when each one fits
Should you use an ORM with DynamoDB?
Use one when your items have a stable shape you want typed and validated, and you write many updates, conditions and queries. Stay on the raw AWS SDK for one table with a few access patterns, or for no dependency beyond the SDK. For single-table design with many entity types, ElectroDB or DynamoDB-Toolbox fits better than a model-per-table library.
The rest of this page shows what each option looks like and where it is the better choice. dynotable-orm is one of the four options, and this page says where it is not the right one. Competitor facts are from their own docs and npm pages, as of 2026-11-06.
What a DynamoDB ORM does
DynamoDB has no joins and no relations, so a DynamoDB "ORM" is an object mapper. It does five jobs that you otherwise write by hand:
- Types. The item type comes from a schema, so reads need no cast.
- Validation. Items are checked before they are written.
- Marshalling. JavaScript values become DynamoDB's typed attribute values (
{S: 'x'},{N: '1'}) and back. DynamoDB JSON explains the format. - Expressions. Updates, conditions and queries become expression strings with placeholder names and values.
- Keys and indexes. The library knows which attributes are keys, and which index a query can use.
What none of them changes: you still design your access patterns first, and a scan still reads the whole table.
The raw AWS SDK v3
Here is a conditional update in the raw SDK:
import {DynamoDBClient, UpdateItemCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'eu-west-1'});
const result = await client.send(
new UpdateItemCommand({
TableName: 'Task',
Key: {projectId: {S: 'p1'}, taskId: {S: 't1'}},
UpdateExpression: 'SET #status = :done ADD #points :two',
ConditionExpression: '#status = :open',
ExpressionAttributeNames: {'#status': 'status', '#points': 'points'},
ExpressionAttributeValues: {':done': {S: 'done'}, ':open': {S: 'open'}, ':two': {N: '2'}},
ReturnValues: 'ALL_NEW'
})
);
// A string or undefined: nothing ties it to the values a status can have.
const status = result.Attributes?.status?.S;
console.log(status);status is a DynamoDB reserved word, so it needs the #status placeholder. A typo in #points or
:two is found by DynamoDB at runtime, not by the compiler. @aws-sdk/lib-dynamodb's document client
removes the {S: ...} wrapping, but the expression strings and the untyped result stay.
The raw SDK fits when:
- you have one table and a few access patterns, and the code that touches DynamoDB is small;
- you want no dependency beyond the SDK, for example in a small Lambda function;
- you need a DynamoDB feature on the day it ships, with nothing in between;
- the team already reads and writes update and condition expressions fluently.
To type the results without a library, see how to generate TypeScript types from DynamoDB.
ElectroDB
ElectroDB (version 3.9.3 on npm, ISC license) describes itself as "type-safe single-table design,
entity modeling, and powerful relationships". You define entities, and "ElectroDB handles partition
keys, sort keys, GSIs, and entity isolation". A collection is "a grouping of Entities with the same
Partition Key", and ElectroDB collections "use a single DynamoDB query to retrieve results" for all of
its entities. It talks to DynamoDB through @aws-sdk/lib-dynamodb, a dependency of the package.
ElectroDB fits when you follow single-table design: many entity types in one table, key templates per entity, and one query that returns several entity types together.
DynamoDB-Toolbox
DynamoDB-Toolbox (version 2.11.1 on npm, MIT license) is "a light-weight and type-safe query builder for
DynamoDB and TypeScript". You define a table and its entities. Each item is tagged with its entity name
in an entity attribute, which the docs "strongly recommend keeping enabled if you use Single Table
Design". "Both pushed and fetched items are validated against your schemas", and schemas "can be
imported from or exported to Zod and JSON Schema". Its peer dependencies are @aws-sdk/client-dynamodb,
@aws-sdk/lib-dynamodb and @aws-sdk/util-dynamodb.
DynamoDB-Toolbox fits when you want single-table design with a command-style API: you build each SDK command from an entity, and items of several entity types in one table stay apart by their entity attribute.
dynotable-orm
dynotable-orm maps one model to one table, named after the model. A Zod 4 schema, or any Standard
Schema, defines the item, and the model's methods keep the dynamoose call
shapes: get, create, update with $SET/$ADD/$REMOVE/$DELETE, query and scan chains,
Condition, transaction([...]) and batches. Here is the same update as above:
import {model} from 'dynotable-orm';
import {extendZod} from 'dynotable-orm/zod';
import {z} from 'zod';
extendZod(z);
const Task = model(
'Task',
z.object({
projectId: z.string().hashKey(),
taskId: z.string().rangeKey(),
status: z.enum(['open', 'done']),
points: z.number()
})
);
const task = await Task.update(
{projectId: 'p1', taskId: 't1'},
{status: 'done', $ADD: {points: 2}},
{condition: Task.condition().where('status').eq('open')}
);
console.log(task.status); // 'open' | 'done'The placeholders, the marshalling and the reserved word are the library's job, and a wrong path or value does not compile.
dynotable-orm fits when:
- you have a table per model, or a few tables, rather than many entity types in one table;
- you have dynamoose code: it keeps dynamoose's call shapes, a whole-item write stores the same item as dynamoose 4.2.0, and a codemod moves the code over. See a dynamoose alternative;
- you already validate with Zod, or with another Standard Schema library, and want one schema for the types, the validation and the keys.
It does not fit when:
- you need single-table design across entity types. It has no entity attribute and no collections;
- you want the library to create or change tables. It never does:
createTableInput()gives you theCreateTableinput to send yourself; - you need a stable 1.0 API today. It is pre-release, and the API may change before 1.0;
- you run on a Node.js version older than 24.
Does performance decide it?
Rarely. A DynamoDB call spends most of its time on the network, and the CPU a library adds is small next to it for most schemas. On the 5-attribute schema of the benchmarks, measured 2026-11-06, ElectroDB and dynamodb-toolbox are slower than dynotable-orm on every operation, and hand-written SDK code is faster than dynotable-orm on create, update and query. These are CPU numbers on a stub client, with no network time. They say nothing about features.
How to choose
| Your situation | The better fit |
|---|---|
| One table, a few access patterns, minimal dependencies | the raw AWS SDK |
| Many entity types in one table, queries across them | ElectroDB or DynamoDB-Toolbox |
| Single-table design with schemas you share with Zod or JSON Schema | DynamoDB-Toolbox |
| A model per table, typed from a Zod or Standard Schema | dynotable-orm |
| Existing dynamoose code that should keep its tables | dynotable-orm, with the codemod |
Whichever you pick, learn the expressions underneath. They are what DynamoDB runs, and what you read in an error message. When not to use single-table design helps with the table layout decision itself.