{
    "componentChunkName": "component---src-templates-mdx-content-page-js",
    "path": "/dynamodb-orm/",
    "result": {"data":{"mdx":{"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"path\": \"/dynamodb-orm/\",\n  \"publishedDate\": \"2026-10-23\",\n  \"updatedDate\": \"2026-10-23\",\n  \"metaTitle\": \"DynamoDB ORM Comparison: Node.js and TypeScript (2026)\",\n  \"metaDescription\": \"Compare ElectroDB, DynamoDB Toolbox, Dynamoose and OneTable with verified releases, TypeScript models and working DynamoDB Local examples.\",\n  \"title\": \"DynamoDB ORMs compared: ElectroDB, Toolbox, Dynamoose, OneTable\",\n  \"authorName\": \"Rafal Wilinski\",\n  \"authorLink\": \"https://x.com/rafalwilinski\",\n  \"authorImage\": \"/images/authors/rafal-wilinski.jpg\",\n  \"image\": \"/images/posts/dynamodb-orm/social.png\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"A DynamoDB ORM is usually a data mapper or query builder that adds schemas, validation and typed operations to DynamoDB. For a new Node.js or TypeScript application, start with \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"ElectroDB\"), \" for access patterns and composite keys, \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"DynamoDB Toolbox\"), \" for explicit, typed request building, or \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Dynamoose\"), \" for a model-oriented API. \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"OneTable is now archived and no longer maintained\"), \", so treat it as an existing-application or migration choice, not a default for new development.\"), mdx(\"p\", null, \"This comparison was checked on \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"October 23, 2026\"), \". Each library below implements the same Customer and Order model, including a GSI query and an update that changes the index key. All four examples ran against DynamoDB Local; the output is included.\"), mdx(\"h2\", null, \"Do you need a DynamoDB ORM?\"), mdx(\"p\", null, \"DynamoDB does not become a relational database when you add a modeling library. You still choose partition keys, sort keys and indexes around access patterns. A relationship between Customer and Order does not create a server-side join or a foreign-key constraint. Start with the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-single-table-design-guide/\"\n  }, \"single-table design guide\"), \" if those choices are still unsettled.\"), mdx(\"p\", null, \"The official SDK's \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"DynamoDBDocumentClient\"), \" already accepts ordinary JavaScript objects and converts them to DynamoDB's attribute-value representation. It does not define your application's Customer schema, derive an order's index keys or infer a domain-specific response type from a model. See AWS's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_dynamodb_code_examples.html\"\n  }, \"JavaScript examples\"), \" and our \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-nodejs/\"\n  }, \"Node.js guide\"), \" for that baseline.\"), mdx(\"p\", null, \"Use a library when several parts of your application repeat the same key templates, validation rules and update expressions. Keep the SDK alone when you have a few straightforward operations, an established validation layer, or need direct access to a new API before a wrapper exposes it. A small repository module around SDK commands can be enough.\"), mdx(\"p\", null, mdx(\"img\", {\n    parentName: \"p\",\n    \"src\": \"/images/posts/dynamodb-orm/library-layers.svg\",\n    \"alt\": \"ElectroDB and Toolbox use the document client, while Dynamoose and OneTable marshal through their own adapters; both paths call DynamoDB through SDK v3.\"\n  })), mdx(\"p\", null, \"The SDK layer differs: ElectroDB and Toolbox accept the v3 document client used here; Dynamoose and OneTable use their own conversion paths over the low-level v3 client. Schema validation runs in application code. Another writer using the SDK or console can still store items that violate that schema.\"), mdx(\"h2\", null, \"Current versions, licenses and maintenance\"), mdx(\"p\", null, \"These are the \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"npm \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"latest\"), \" versions\"), \", not prereleases or unreleased repository code. Publication dates are UTC. The package links expose npm's version and publication metadata; repository status was checked separately on the same day.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Library and npm package\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Current major / tested version\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Latest publication\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"License\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Maintenance evidence on October 23, 2026\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://registry.npmjs.org/electrodb\"\n  }, \"ElectroDB\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"3 / \", mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"3.9.3\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2026-08-09\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"ISC\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://github.com/tywalch/electrodb\"\n  }, \"Repository\"), \" is not archived; latest default-branch commit August 9, 2026\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://registry.npmjs.org/dynamodb-toolbox\"\n  }, \"DynamoDB Toolbox\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2 / \", mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"2.11.1\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2026-10-22\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"MIT\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://github.com/dynamodb-toolbox/dynamodb-toolbox\"\n  }, \"Repository\"), \" is not archived; latest default-branch commit October 22, 2026\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://registry.npmjs.org/dynamoose\"\n  }, \"Dynamoose\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"4 / \", mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"4.2.0\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2026-05-05\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Unlicense\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://github.com/dynamoose/dynamoose\"\n  }, \"Repository\"), \" is not archived; latest default-branch commit October 11, 2026\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://registry.npmjs.org/dynamodb-onetable\"\n  }, \"OneTable\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2 / \", mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"2.7.7\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"2025-02-25\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"MIT\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"a\", {\n    parentName: \"td\",\n    \"href\": \"https://github.com/sensedeep/dynamodb-onetable\"\n  }, \"Repository\"), \" is archived; README explicitly says it is no longer maintained\")))), mdx(\"p\", null, \"A release date is evidence of publication, not a support guarantee. OneTable's archive notice is more decisive than its release gap: its old feature set remains useful to understand, but a successful local run does not establish future SDK compatibility or security maintenance.\"), mdx(\"h2\", null, \"ElectroDB vs DynamoDB Toolbox vs Dynamoose vs OneTable\"), mdx(\"p\", null, \"All four support single-table layouts, SDK v3, batch operations and transactions. The practical difference is how much of the key design and model behavior they represent in the schema.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Capability\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"ElectroDB 3\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Toolbox 2\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Dynamoose 4\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"OneTable 2\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Single-table modeling\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Entities, Services and cross-entity collections\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Multiple Entities attached to a Table\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Multiple models attached to a Table\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Multiple models in one table schema\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"TypeScript inference\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Infers item and operation types from entity definitions\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Infers item, key and command types from schemas\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Typings plus an explicit \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"Item\"), \" subclass for domain fields\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Infers model types from a literal schema\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Runtime validation\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Attribute types, required fields, enums and custom validators\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Schema parsing, formatting and custom validators\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Schema types, required fields, enums and validators\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Types, required fields, enums and validators\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Key composition\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Composite attribute lists and templates\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"computeKey\"), \", transforms and linked attributes\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Explicit keys or schema conversion logic\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Schema value templates\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Query builder\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Named access patterns, sort-key conditions and filters\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"QueryCommand\"), \" with explicit partition, index and range\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"Model.query()\"), \" chain\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"Model.find()\"), \" with properties and index options\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Read/write transactions\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Service transaction builders\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Transaction actions and execute helpers\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"dynamoose.transaction()\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"table.transact()\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Batch operations\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Batch get, put and delete\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Batch get/write commands and requests\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"batchGet\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"batchPut\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"batchDelete\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"table.batchGet\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"table.batchWrite\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Pagination\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Opaque \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"cursor\"), \", page controls\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"LastEvaluatedKey\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"exclusiveStartKey\"), \", async \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"paginate()\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"lastKey\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"startAt()\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"all()\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Result \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"next\"), \"/\", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"prev\"), \" keys, page controls\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"SDK v3\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Yes, document client\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Yes, document client\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Yes, low-level client\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Yes, built-in adapter\")))), mdx(\"p\", null, \"The API references document \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://electrodb.dev/en/modeling/indexes/\"\n  }, \"ElectroDB's keys\"), \", \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.dynamodbtoolbox.com/docs/entities/usage\"\n  }, \"Toolbox entities\"), \", \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://dynamoosejs.com/guide/Model/\"\n  }, \"Dynamoose models\"), \" and \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://doc.onetable.io/start/quick-tour/\"\n  }, \"OneTable's schema and operations\"), \". Dynamoose's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://dynamoosejs.com/getting_started/TypeScript/\"\n  }, \"TypeScript documentation\"), \" still labels its typings beta; its domain class and runtime schema must be kept in agreement. OneTable's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://doc.onetable.io/start/typescript/\"\n  }, \"TypeScript guide\"), \" explains schema-derived models.\"), mdx(\"p\", null, \"Transaction support is documented for \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://electrodb.dev/en/mutations/transact-write/\"\n  }, \"ElectroDB\"), \", \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.dynamodbtoolbox.com/docs/entities/actions/transactions\"\n  }, \"Toolbox\"), \", \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://dynamoosejs.com/guide/Transaction/\"\n  }, \"Dynamoose\"), \" and \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://doc.onetable.io/api/table/methods/\"\n  }, \"OneTable\"), \". These wrappers do not change DynamoDB's limits: a write transaction supports up to 100 distinct items and 4 MB, while a batch write is not atomic. Use the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-transactions/\"\n  }, \"transaction guide\"), \" when Customer and Order must be committed together. The examples below deliberately perform independent writes.\"), mdx(\"h2\", null, \"Which DynamoDB library should you choose?\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Your situation\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Starting choice\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Reason\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"A TypeScript service with several entities and derived keys\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"ElectroDB\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Access patterns live beside attributes, and updates can maintain derived index keys\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"You want explicit commands and control over an existing key layout\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"DynamoDB Toolbox\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Schema validation and inferred types without requiring a particular key naming convention\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Your team prefers models with create/get/update/query methods\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Dynamoose\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"A familiar model-oriented API with runtime schemas\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"You already run OneTable\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Evaluate a maintained replacement or an owned fork\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Its repository is archived; preserve key templates and stored metadata during migration\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"A small service with a handful of known requests\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"SDK v3 document client\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Less abstraction to learn and maintain\")))), mdx(\"h3\", null, \"ElectroDB vs DynamoDB Toolbox\"), mdx(\"p\", null, \"Choose based on where you want access-pattern knowledge to live. In the ElectroDB example, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"byStatus\"), \" is a named query derived from attributes. In Toolbox, the table query names the index and supplies the partition value explicitly. Toolbox can also derive attributes through \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.dynamodbtoolbox.com/docs/schemas/defaults-and-links\"\n  }, \"defaults and links\"), \"; it is not limited to hand-built keys.\"), mdx(\"p\", null, \"ElectroDB's conventions become helpful when they eliminate repeated key logic across many entities. Toolbox is a good fit when you want to see individual commands and adapt them to an existing schema. Both can generate request parameters for inspection. Neither choice removes the need to inspect the resulting key condition and distinguish it from a filter.\"), mdx(\"h3\", null, \"Dynamoose vs ElectroDB\"), mdx(\"p\", null, \"Dynamoose centers the model and its schema. ElectroDB centers the entity's attributes and access patterns. If your main problem is expressing several composite-key queries consistently, start with ElectroDB. If your team wants model methods and accepts maintaining explicit TypeScript classes alongside the schemas, evaluate Dynamoose.\"), mdx(\"p\", null, \"Do not use local request durations to choose a winner. The examples measure correctness, not speed, cold starts, bundle size or production throughput. The broader \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-tools/\"\n  }, \"DynamoDB tools directory\"), \" covers clients and infrastructure tools outside this comparison.\"), mdx(\"h2\", null, \"The same Customer and Order model in every library\"), mdx(\"p\", null, \"Each implementation uses its own table so library-specific entity metadata cannot interfere with another example. The business attributes and physical key values are identical. This is single-table design within each implementation, not four libraries sharing one live data format.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Item\"), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"th\"\n  }, \"pk\")), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"th\"\n  }, \"sk\")), mdx(\"th\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Business attributes\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Customer\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"CUSTOMER#c1\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"PROFILE\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"customerId=c1\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"name=Ada\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, \"Order\"), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"CUSTOMER#c1\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"ORDER#o1\")), mdx(\"td\", {\n    parentName: \"tr\",\n    \"align\": null\n  }, mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"customerId=c1\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"orderId=o1\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"status=PAID\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"totalCents=4200\"))))), mdx(\"p\", null, \"The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"byStatus\"), \" GSI has partition key \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gsi1pk\"), \" and sort key \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gsi1sk\"), \", with an \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ALL\"), \" projection. The order starts with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gsi1pk=CUSTOMER#c1#STATUS#PAID\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gsi1sk=ORDER#o1\"), \". Customer items omit both GSI keys, so they do not appear in that index.\"), mdx(\"p\", null, \"The access patterns are: retrieve a customer, retrieve an order by customer and order IDs, and list one customer's orders with a given status. Including the customer ID in the GSI partition avoids putting every customer's paid orders under one global \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"PAID\"), \" key. It does not eliminate the possibility of a particularly busy customer becoming a hot key.\"), mdx(\"p\", null, mdx(\"img\", {\n    parentName: \"p\",\n    \"src\": \"/images/posts/dynamodb-orm/status-index-update.svg\",\n    \"alt\": \"Changing the order from PAID to SHIPPED preserves its base key and changes gsi1pk; the GSI reflects the new status after asynchronous propagation.\"\n  })), mdx(\"p\", null, \"Each program writes both items, reads them, queries paid orders, updates the order to shipped, and queries both statuses again. ElectroDB and OneTable derive the changed index key from templates. The Toolbox and Dynamoose examples update it explicitly in the same item request. Updating only \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"status\"), \" in those explicit implementations would leave the index entry under the old key.\"), mdx(\"h2\", null, \"Run the examples against DynamoDB Local\"), mdx(\"p\", null, \"Use an empty working directory and Node.js 24. The tested environment was Node.js \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"24.18.1\"), \", TypeScript \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"7.0.2\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tsx\"), \" \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"4.23.15\"), \", AWS SDK packages \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"3.1156.0\"), \", and DynamoDB Local \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"3.3.2\"), \". The library versions are pinned below.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-bash\"\n  }, \"npm init -y\\nnpm pkg set type=module\\nnpm install --save-exact electrodb@3.9.3 dynamodb-toolbox@2.11.1 \\\\\\n  dynamoose@4.2.0 dynamodb-onetable@2.7.7 \\\\\\n  @aws-sdk/client-dynamodb@3.1156.0 @aws-sdk/lib-dynamodb@3.1156.0 \\\\\\n  typescript@7.0.2 tsx@4.23.15 @types/node@26.6.5\\n\\ndocker run -d --rm --name dynamodb-local -p 8000:8000 \\\\\\n  amazon/dynamodb-local:3.3.2 -jar DynamoDBLocal.jar \\\\\\n  -inMemory -sharedDb -disableTelemetry\\n\")), mdx(\"p\", null, \"Wait for the container to start. All clients below use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"http://localhost:8000\"), \" and dummy credentials. Save the following five TypeScript files together. Their \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".js\"), \" import specifiers work with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tsx\"), \" and the NodeNext TypeScript configuration shown later.\"), mdx(\"p\", null, \"The immediate GSI assertions are a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"local correctness check\"), \". AWS documents that \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.UsageNotes.html\"\n  }, \"DynamoDB Local reads often appear strongly consistent\"), \", while \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html\"\n  }, \"GSI updates in AWS propagate asynchronously\"), \". An application using the managed service must tolerate index lag; a successful write is not a guarantee that the next GSI query sees it.\"), mdx(\"h3\", null, \"Shared setup: \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"shared.ts\")), mdx(\"p\", null, \"This creates one table per example and checks the normalized results. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"totalCents\"), \" keeps the sample amount as an integer. The scripts expect fresh table names; to repeat the full exercise, stop and restart the in-memory container.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import assert from \\\"node:assert/strict\\\"\\nimport {\\n  CreateTableCommand, DynamoDBClient, waitUntilTableExists,\\n} from \\\"@aws-sdk/client-dynamodb\\\"\\nimport { DynamoDBDocumentClient } from \\\"@aws-sdk/lib-dynamodb\\\"\\n\\nexport const config = {\\n  endpoint: \\\"http://localhost:8000\\\",\\n  region: \\\"us-east-1\\\",\\n  credentials: { accessKeyId: \\\"fake\\\", secretAccessKey: \\\"fake\\\" },\\n}\\nexport const client = new DynamoDBClient(config)\\nexport const documentClient = DynamoDBDocumentClient.from(client)\\n\\nexport async function createTable(name: string) {\\n  await client.send(new CreateTableCommand({\\n    TableName: name,\\n    BillingMode: \\\"PAY_PER_REQUEST\\\",\\n    AttributeDefinitions: [\\\"pk\\\", \\\"sk\\\", \\\"gsi1pk\\\", \\\"gsi1sk\\\"].map(name => ({\\n      AttributeName: name, AttributeType: \\\"S\\\",\\n    })),\\n    KeySchema: [\\n      { AttributeName: \\\"pk\\\", KeyType: \\\"HASH\\\" },\\n      { AttributeName: \\\"sk\\\", KeyType: \\\"RANGE\\\" },\\n    ],\\n    GlobalSecondaryIndexes: [{\\n      IndexName: \\\"byStatus\\\",\\n      KeySchema: [\\n        { AttributeName: \\\"gsi1pk\\\", KeyType: \\\"HASH\\\" },\\n        { AttributeName: \\\"gsi1sk\\\", KeyType: \\\"RANGE\\\" },\\n      ],\\n      Projection: { ProjectionType: \\\"ALL\\\" },\\n    }],\\n  }))\\n  await waitUntilTableExists({ client, maxWaitTime: 30 }, { TableName: name })\\n  return name\\n}\\n\\nexport const customer = { customerId: \\\"c1\\\", name: \\\"Ada\\\" }\\nexport const order = {\\n  customerId: \\\"c1\\\", orderId: \\\"o1\\\", status: \\\"PAID\\\", totalCents: 4200,\\n} as const\\nexport const id = { customerId: \\\"c1\\\", orderId: \\\"o1\\\" }\\n\\nexport function report(library: string, result: {\\n  customer: string | undefined; before: string | undefined\\n  paid: string[]; after: string | undefined\\n  shipped: string[]; paidAfter: string[]\\n}) {\\n  assert.deepEqual(result, {\\n    customer: \\\"Ada\\\", before: \\\"PAID\\\", paid: [\\\"o1\\\"],\\n    after: \\\"SHIPPED\\\", shipped: [\\\"o1\\\"], paidAfter: [],\\n  })\\n  console.log(JSON.stringify({ library, ...result }))\\n}\\n\")), mdx(\"h3\", null, \"ElectroDB: \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"electro.ts\")), mdx(\"p\", null, \"The explicit templates preserve the common layout. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"casing: \\\"none\\\"\"), \" preserves the key text exactly; ElectroDB's default key casing differs. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"patch()\"), \" requires an existing item, and changing \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"status\"), \" causes ElectroDB to calculate the GSI key from the supplied composite attributes.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import { Entity } from \\\"electrodb\\\"\\nimport { createTable, documentClient, customer, order, id, report } from \\\"./shared.js\\\"\\n\\nconst table = await createTable(\\\"OrmElectro\\\")\\nconst options = { table, client: documentClient }\\nconst Customer = new Entity({\\n  model: { entity: \\\"Customer\\\", service: \\\"shop\\\", version: \\\"1\\\" },\\n  attributes: {\\n    customerId: { type: \\\"string\\\", required: true },\\n    name: { type: \\\"string\\\", required: true },\\n  },\\n  indexes: {\\n    primary: {\\n      pk: { field: \\\"pk\\\", composite: [\\\"customerId\\\"], template: \\\"CUSTOMER#${customerId}\\\", casing: \\\"none\\\" },\\n      sk: { field: \\\"sk\\\", composite: [], template: \\\"PROFILE\\\", casing: \\\"none\\\" },\\n    },\\n  },\\n}, options)\\nconst Order = new Entity({\\n  model: { entity: \\\"Order\\\", service: \\\"shop\\\", version: \\\"1\\\" },\\n  attributes: {\\n    customerId: { type: \\\"string\\\", required: true },\\n    orderId: { type: \\\"string\\\", required: true },\\n    status: { type: [\\\"PAID\\\", \\\"SHIPPED\\\"] as const, required: true },\\n    totalCents: { type: \\\"number\\\", required: true },\\n  },\\n  indexes: {\\n    primary: {\\n      pk: { field: \\\"pk\\\", composite: [\\\"customerId\\\"], template: \\\"CUSTOMER#${customerId}\\\", casing: \\\"none\\\" },\\n      sk: { field: \\\"sk\\\", composite: [\\\"orderId\\\"], template: \\\"ORDER#${orderId}\\\", casing: \\\"none\\\" },\\n    },\\n    byStatus: {\\n      index: \\\"byStatus\\\",\\n      pk: { field: \\\"gsi1pk\\\", composite: [\\\"customerId\\\", \\\"status\\\"], template: \\\"CUSTOMER#${customerId}#STATUS#${status}\\\", casing: \\\"none\\\" },\\n      sk: { field: \\\"gsi1sk\\\", composite: [\\\"orderId\\\"], template: \\\"ORDER#${orderId}\\\", casing: \\\"none\\\" },\\n    },\\n  },\\n}, options)\\n\\nawait Customer.put(customer).go()\\nawait Order.put(order).go()\\nconst savedCustomer = await Customer.get({ customerId: \\\"c1\\\" }).go({ consistent: true })\\nconst before = await Order.get(id).go({ consistent: true })\\nconst paid = await Order.query.byStatus({ customerId: \\\"c1\\\", status: \\\"PAID\\\" }).go()\\nconst after = await Order.patch(id).set({ status: \\\"SHIPPED\\\" }).go({ response: \\\"all_new\\\" })\\nconst shipped = await Order.query.byStatus({ customerId: \\\"c1\\\", status: \\\"SHIPPED\\\" }).go()\\nconst paidAfter = await Order.query.byStatus({ customerId: \\\"c1\\\", status: \\\"PAID\\\" }).go()\\nreport(\\\"ElectroDB\\\", {\\n  customer: savedCustomer.data?.name, before: before.data?.status,\\n  paid: paid.data.map(item => item.orderId), after: after.data?.status,\\n  shipped: shipped.data.map(item => item.orderId), paidAfter: paidAfter.data.map(item => item.orderId),\\n})\\n\")), mdx(\"h3\", null, \"DynamoDB Toolbox: \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"toolbox.ts\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"computeKey\"), \" derives the primary key from the schema's key attributes. Here the GSI attributes are explicit so the update shows exactly what is sent. If you replace this with schema links, configure update-time behavior too: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".link()\"), \" on a non-key attribute is a put-time shorthand, not an automatic update hook. The update also requires the existing order attribute.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import {\\n  Table, Entity, item, string, number,\\n  PutItemCommand, GetItemCommand, QueryCommand, UpdateItemCommand,\\n} from \\\"dynamodb-toolbox\\\"\\nimport { createTable, documentClient, customer, order, id, report } from \\\"./shared.js\\\"\\n\\nconst table = new Table({\\n  name: await createTable(\\\"OrmToolbox\\\"), documentClient,\\n  partitionKey: { name: \\\"pk\\\", type: \\\"string\\\" },\\n  sortKey: { name: \\\"sk\\\", type: \\\"string\\\" },\\n  indexes: {\\n    byStatus: {\\n      type: \\\"global\\\",\\n      partitionKey: { name: \\\"gsi1pk\\\", type: \\\"string\\\" },\\n      sortKey: { name: \\\"gsi1sk\\\", type: \\\"string\\\" },\\n    },\\n  },\\n})\\nconst Customer = new Entity({\\n  name: \\\"Customer\\\", table, timestamps: false,\\n  schema: item({ customerId: string().key(), name: string() }),\\n  computeKey: ({ customerId }) => ({ pk: `CUSTOMER#${customerId}`, sk: \\\"PROFILE\\\" }),\\n})\\nconst Order = new Entity({\\n  name: \\\"Order\\\", table, timestamps: false,\\n  schema: item({\\n    customerId: string().key(), orderId: string().key(),\\n    status: string().enum(\\\"PAID\\\", \\\"SHIPPED\\\"), totalCents: number(),\\n    gsi1pk: string(), gsi1sk: string(),\\n  }),\\n  computeKey: ({ customerId, orderId }) => ({\\n    pk: `CUSTOMER#${customerId}`, sk: `ORDER#${orderId}`,\\n  }),\\n})\\nconst statusKey = (status: string) => `CUSTOMER#c1#STATUS#${status}`\\nconst byStatus = (status: string) => table.build(QueryCommand)\\n  .entities(Order).query({ index: \\\"byStatus\\\", partition: statusKey(status) }).send()\\n\\nawait Customer.build(PutItemCommand).item(customer).send()\\nawait Order.build(PutItemCommand).item({\\n  ...order, gsi1pk: statusKey(order.status), gsi1sk: \\\"ORDER#o1\\\",\\n}).send()\\nconst savedCustomer = await Customer.build(GetItemCommand).key({ customerId: \\\"c1\\\" })\\n  .options({ consistent: true }).send()\\nconst before = await Order.build(GetItemCommand).key(id).options({ consistent: true }).send()\\nconst paid = await byStatus(\\\"PAID\\\")\\nconst after = await Order.build(UpdateItemCommand).item({\\n  ...id, status: \\\"SHIPPED\\\", gsi1pk: statusKey(\\\"SHIPPED\\\"),\\n}).options({ returnValues: \\\"ALL_NEW\\\", condition: { attr: \\\"orderId\\\", exists: true } }).send()\\nconst shipped = await byStatus(\\\"SHIPPED\\\")\\nconst paidAfter = await byStatus(\\\"PAID\\\")\\nreport(\\\"DynamoDB Toolbox\\\", {\\n  customer: savedCustomer.Item?.name, before: before.Item?.status,\\n  paid: (paid.Items ?? []).map(item => item.orderId), after: after.Attributes?.status,\\n  shipped: (shipped.Items ?? []).map(item => item.orderId),\\n  paidAfter: (paidAfter.Items ?? []).map(item => item.orderId),\\n})\\n\")), mdx(\"h3\", null, \"Dynamoose: \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"dynamoose.ts\")), mdx(\"p\", null, \"Both models belong to one \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"dynamoose.Table\"), \". Its automatic table creation and updates are disabled because the shared setup already created the resources. The two \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Item\"), \" subclasses provide domain-field types; the runtime schemas provide validation. See the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://dynamoosejs.com/guide/Table/\"\n  }, \"Dynamoose Table options\"), \" for deployment settings.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import dynamoose from \\\"dynamoose\\\"\\nimport { Item } from \\\"dynamoose/dist/Item.js\\\"\\nimport { config, createTable, customer, order, id, report } from \\\"./shared.js\\\"\\n\\nclass CustomerItem extends Item {\\n  pk!: string; sk!: string; customerId!: string; name!: string\\n}\\nclass OrderItem extends Item {\\n  pk!: string; sk!: string; customerId!: string; orderId!: string\\n  status!: \\\"PAID\\\" | \\\"SHIPPED\\\"; totalCents!: number\\n  gsi1pk!: string; gsi1sk!: string\\n}\\ndynamoose.aws.ddb.set(new dynamoose.aws.ddb.DynamoDB(config))\\nconst keys = { pk: { type: String, hashKey: true }, sk: { type: String, rangeKey: true } }\\nconst Customer = dynamoose.model<CustomerItem>(\\\"Customer\\\", new dynamoose.Schema({\\n  ...keys, customerId: { type: String, required: true }, name: { type: String, required: true },\\n}))\\nconst Order = dynamoose.model<OrderItem>(\\\"Order\\\", new dynamoose.Schema({\\n  ...keys,\\n  customerId: { type: String, required: true }, orderId: { type: String, required: true },\\n  status: { type: String, enum: [\\\"PAID\\\", \\\"SHIPPED\\\"], required: true },\\n  totalCents: { type: Number, required: true },\\n  gsi1pk: { type: String, index: { name: \\\"byStatus\\\", type: \\\"global\\\", rangeKey: \\\"gsi1sk\\\" } },\\n  gsi1sk: String,\\n}))\\nnew dynamoose.Table(await createTable(\\\"OrmDynamoose\\\"), [Customer, Order], {\\n  create: false, update: false, waitForActive: false,\\n})\\nconst key = { pk: `CUSTOMER#${id.customerId}`, sk: `ORDER#${id.orderId}` }\\nconst statusKey = (status: string) => `CUSTOMER#c1#STATUS#${status}`\\nconst byStatus = (status: string) => Order.query(\\\"gsi1pk\\\").eq(statusKey(status))\\n  .using(\\\"byStatus\\\").exec()\\n\\nawait Customer.create({ ...customer, pk: \\\"CUSTOMER#c1\\\", sk: \\\"PROFILE\\\" }, { overwrite: true })\\nawait Order.create({\\n  ...order, ...key, gsi1pk: statusKey(order.status), gsi1sk: key.sk,\\n}, { overwrite: true })\\nconst savedCustomer = await Customer.get({ pk: \\\"CUSTOMER#c1\\\", sk: \\\"PROFILE\\\" }, { consistent: true })\\nconst before = await Order.get(key, { consistent: true })\\nconst paid = await byStatus(\\\"PAID\\\")\\nconst after = await Order.update(key, {\\n  status: \\\"SHIPPED\\\", gsi1pk: statusKey(\\\"SHIPPED\\\"),\\n}, { condition: new dynamoose.Condition().where(\\\"pk\\\").exists() })\\nconst shipped = await byStatus(\\\"SHIPPED\\\")\\nconst paidAfter = await byStatus(\\\"PAID\\\")\\nreport(\\\"Dynamoose\\\", {\\n  customer: savedCustomer?.name, before: before?.status,\\n  paid: paid.map(item => item.orderId), after: after?.status,\\n  shipped: shipped.map(item => item.orderId), paidAfter: paidAfter.map(item => item.orderId),\\n})\\n\")), mdx(\"h3\", null, \"OneTable: \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"onetable.ts\")), mdx(\"p\", null, \"This example is for understanding or maintaining an existing OneTable application. Schema value templates derive all four keys, and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"as const\"), \" preserves the schema's literal types. Passing the low-level v3 client lets OneTable wrap it internally. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"exists: null\"), \" makes the initial create calls behave as replacement puts; \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"update()\"), \" requires an existing item by default.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-ts\"\n  }, \"import { Table } from \\\"dynamodb-onetable\\\"\\nimport { client, createTable, customer, order, id, report } from \\\"./shared.js\\\"\\n\\nconst schema = {\\n  format: \\\"onetable:1.1.0\\\", version: \\\"1.0.0\\\",\\n  indexes: {\\n    primary: { hash: \\\"pk\\\", sort: \\\"sk\\\" },\\n    byStatus: { hash: \\\"gsi1pk\\\", sort: \\\"gsi1sk\\\" },\\n  },\\n  models: {\\n    Customer: {\\n      pk: { type: String, value: \\\"CUSTOMER#${customerId}\\\" },\\n      sk: { type: String, value: \\\"PROFILE\\\" },\\n      customerId: { type: String, required: true },\\n      name: { type: String, required: true },\\n    },\\n    Order: {\\n      pk: { type: String, value: \\\"CUSTOMER#${customerId}\\\" },\\n      sk: { type: String, value: \\\"ORDER#${orderId}\\\" },\\n      gsi1pk: { type: String, value: \\\"CUSTOMER#${customerId}#STATUS#${status}\\\" },\\n      gsi1sk: { type: String, value: \\\"ORDER#${orderId}\\\" },\\n      customerId: { type: String, required: true },\\n      orderId: { type: String, required: true },\\n      status: { type: String, enum: [\\\"PAID\\\", \\\"SHIPPED\\\"], required: true },\\n      totalCents: { type: Number, required: true },\\n    },\\n  },\\n  params: { timestamps: false },\\n} as const\\nconst table = new Table({ name: await createTable(\\\"OrmOneTable\\\"), client, schema, partial: true })\\nconst Customer = table.getModel(\\\"Customer\\\")\\nconst Order = table.getModel(\\\"Order\\\")\\nconst byStatus = (status: \\\"PAID\\\" | \\\"SHIPPED\\\") => Order.find(\\n  { customerId: \\\"c1\\\", status }, { index: \\\"byStatus\\\" },\\n)\\n\\nawait Customer.create(customer, { exists: null })\\nawait Order.create(order, { exists: null })\\nconst savedCustomer = await Customer.get({ customerId: \\\"c1\\\" }, { consistent: true })\\nconst before = await Order.get(id, { consistent: true })\\nconst paid = await byStatus(\\\"PAID\\\")\\nconst after = await Order.update({ ...id, status: \\\"SHIPPED\\\" })\\nconst shipped = await byStatus(\\\"SHIPPED\\\")\\nconst paidAfter = await byStatus(\\\"PAID\\\")\\nreport(\\\"OneTable\\\", {\\n  customer: savedCustomer?.name, before: before?.status,\\n  paid: paid.map(item => item.orderId), after: after?.status,\\n  shipped: shipped.map(item => item.orderId), paidAfter: paidAfter.map(item => item.orderId),\\n})\\n\")), mdx(\"h3\", null, \"Type-check and execute\"), mdx(\"p\", null, \"Save this as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tsconfig.json\"), \". It checks application code in strict mode and skips checking dependency declaration files.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-json\"\n  }, \"{\\n  \\\"compilerOptions\\\": {\\n    \\\"target\\\": \\\"ES2022\\\", \\\"module\\\": \\\"NodeNext\\\", \\\"moduleResolution\\\": \\\"NodeNext\\\",\\n    \\\"strict\\\": true, \\\"skipLibCheck\\\": true, \\\"noEmit\\\": true\\n  },\\n  \\\"include\\\": [\\\"shared.ts\\\", \\\"electro.ts\\\", \\\"toolbox.ts\\\", \\\"dynamoose.ts\\\", \\\"onetable.ts\\\"]\\n}\\n\")), mdx(\"p\", null, \"Run each implementation once:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-bash\"\n  }, \"npx tsc --noEmit\\nnpx tsx electro.ts\\nnpx tsx toolbox.ts\\nnpx tsx dynamoose.ts\\nnpx tsx onetable.ts\\n\")), mdx(\"p\", null, \"Observed output, October 23, 2026:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-text\"\n  }, \"{\\\"library\\\":\\\"ElectroDB\\\",\\\"customer\\\":\\\"Ada\\\",\\\"before\\\":\\\"PAID\\\",\\\"paid\\\":[\\\"o1\\\"],\\\"after\\\":\\\"SHIPPED\\\",\\\"shipped\\\":[\\\"o1\\\"],\\\"paidAfter\\\":[]}\\n{\\\"library\\\":\\\"DynamoDB Toolbox\\\",\\\"customer\\\":\\\"Ada\\\",\\\"before\\\":\\\"PAID\\\",\\\"paid\\\":[\\\"o1\\\"],\\\"after\\\":\\\"SHIPPED\\\",\\\"shipped\\\":[\\\"o1\\\"],\\\"paidAfter\\\":[]}\\n{\\\"library\\\":\\\"Dynamoose\\\",\\\"customer\\\":\\\"Ada\\\",\\\"before\\\":\\\"PAID\\\",\\\"paid\\\":[\\\"o1\\\"],\\\"after\\\":\\\"SHIPPED\\\",\\\"shipped\\\":[\\\"o1\\\"],\\\"paidAfter\\\":[]}\\n{\\\"library\\\":\\\"OneTable\\\",\\\"customer\\\":\\\"Ada\\\",\\\"before\\\":\\\"PAID\\\",\\\"paid\\\":[\\\"o1\\\"],\\\"after\\\":\\\"SHIPPED\\\",\\\"shipped\\\":[\\\"o1\\\"],\\\"paidAfter\\\":[]}\\n\")), mdx(\"p\", null, \"The report helper throws if any result differs. This verifies the basic writes, reads, index-key update and GSI queries. It does not exercise every batch, transaction, pagination or validation feature in the comparison table.\"), mdx(\"p\", null, \"When finished, remove the in-memory database by stopping its container:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-bash\"\n  }, \"docker stop dynamodb-local\\n\")), mdx(\"h2\", null, \"Mistakes a modeling library does not prevent\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Confusing a schema with a migration.\"), \" Changing a key template does not rewrite existing items. When moving between libraries, account for entity discriminator attributes, hidden fields, case conversion and timestamp defaults as well as PK/SK values. Test against representative stored items before changing production readers.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Updating a source attribute without its derived key.\"), \" Our order needs \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"status\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"gsi1pk\"), \" changed together. A plain SDK write bypasses the library's derivation. Check the generated \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-update-expression/\"\n  }, \"UpdateExpression\"), \" and ensure every writer follows the same rule.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Treating a query filter as an index.\"), \" A status filter on the customer partition reads candidate orders before discarding nonmatching ones. The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"byStatus\"), \" key condition directly selects the customer's status partition. A convenient query method does not make the two access patterns equivalent.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Ignoring continuation keys.\"), \" The one-order example fits in a single response. Larger queries need pagination. ElectroDB exposes a \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://electrodb.dev/en/queries/pagination/\"\n  }, \"cursor\"), \"; Toolbox exposes \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.dynamodbtoolbox.com/docs/tables/actions/query\"\n  }, \"page keys and an async iterator\"), \"; Dynamoose uses \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://dynamoosejs.com/guide/Query/\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"lastKey\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"startAt\")), \"; OneTable uses \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://doc.onetable.io/api/model/params/\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"next\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"prev\")), \". A filtered page can be empty and still have a continuation key. Follow the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-pagination/\"\n  }, \"pagination rules\"), \" rather than stopping at an empty array.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Mixing major-version tutorials.\"), \" The examples use ElectroDB 3, Toolbox 2 and Dynamoose 4. ElectroDB's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://electrodb.dev/en/core-concepts/v3-migration/\"\n  }, \"v3 migration guide\"), \" documents changed validation and pagination semantics. Copying older imports or relying on old \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"limit\"), \" behavior can produce code that compiles differently or reads more data than intended.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Treating a model declaration as deployed infrastructure.\"), \" The runtime table and its indexes must match the schema. Keep that definition in infrastructure code; \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-infrastructure-code/\"\n  }, \"Dynobase's infrastructure code generation\"), \" can generate CDK, CloudFormation or Terraform for a table when you need to capture its configuration. It does not replace the application's library schema.\"), mdx(\"h2\", null, \"DynamoDB modeling libraries for other languages\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Python: PynamoDB.\"), \" \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://pynamodb.readthedocs.io/en/stable/\"\n  }, \"PynamoDB\"), \" provides model classes and typed attributes, secondary-index queries and batch operations. Use it when you want a Python model API; use the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-python-with-boto3/\"\n  }, \"boto3 guide\"), \" when you want direct SDK operations. The JavaScript examples above are not PynamoDB compatibility tests.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Java: the SDK 2.x Enhanced Client.\"), \" AWS's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/dynamodb-enhanced-client.html\"\n  }, \"DynamoDB Enhanced Client\"), \" is the successor to the SDK 1.x \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"DynamoDBMapper\"), \". It maps Java classes to items and also provides an Enhanced Document API for flexible documents. Our \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-java-with-dynamodbmapper/\"\n  }, \"Java guide\"), \" covers the current mapping approach despite its historical URL.\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"C# and .NET: the object persistence model.\"), \" AWS's \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DotNetSDKHighLevel.html\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"a\"\n  }, \"DynamoDBContext\")), \" maps classes and properties to DynamoDB items. Use the current SDK v4 conventions, including the \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://aws.amazon.com/blogs/developer/preview-4-of-aws-sdk-for-net-v4/\"\n  }, \"context builder\"), \", rather than copying an obsolete constructor from a v3 tutorial. See our \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"/dynamodb-csharp-dotnet/\"\n  }, \".NET and C# examples\"), \".\"), mdx(FAQ, {\n    questions: [{\n      question: \"What is the best DynamoDB ORM for TypeScript?\",\n      answer: \"Start with ElectroDB when you want schemas that describe composite keys and named access patterns. Start with DynamoDB Toolbox when you prefer explicit typed commands and schema composition. The right choice depends on your access patterns and how much key logic you want the library to own.\"\n    }, {\n      question: \"Does DynamoDB need an ORM?\",\n      answer: \"No. AWS SDK v3's document client supports ordinary JavaScript objects without a model library. Add a library when shared schemas, validation, typed queries or repeated key construction justify the dependency.\"\n    }, {\n      question: \"What is the difference between ElectroDB and DynamoDB Toolbox?\",\n      answer: \"ElectroDB describes access patterns and composite keys inside entity definitions. Toolbox combines schemas with explicit commands, computed keys and linked attributes. Both support single-table layouts and SDK v3.\"\n    }, {\n      question: \"Should I choose Dynamoose or ElectroDB?\",\n      answer: \"Dynamoose fits a model-oriented create/get/update/query API with runtime schemas. ElectroDB fits an application organized around named access patterns and composite-key generation. Dynamoose domain types use explicit Item subclasses, while ElectroDB infers them from entity schemas.\"\n    }, {\n      question: \"Is DynamoDB OneTable still maintained?\",\n      answer: \"No. On October 23, 2026, the OneTable repository was archived and its README explicitly said it was no longer maintained. Version 2.7.7 passed this article's local examples, but new projects should evaluate maintained alternatives.\"\n    }, {\n      question: \"Can a DynamoDB ORM perform SQL joins?\",\n      answer: \"No. These libraries build DynamoDB operations and map items to application models. You still design keys, indexes, denormalized data or multiple reads to satisfy relationships between entities.\"\n    }],\n    mdxType: \"FAQ\"\n  }));\n}\n;\nMDXContent.isMDXComponent = true;","frontmatter":{"publishedDate":"2026-10-23","updatedDate":"2026-10-23","path":"/dynamodb-orm/","metaTitle":"DynamoDB ORM Comparison: Node.js and TypeScript (2026)","metaDescription":"Compare ElectroDB, DynamoDB Toolbox, Dynamoose and OneTable with verified releases, TypeScript models and working DynamoDB Local examples.","title":"DynamoDB ORMs compared: ElectroDB, Toolbox, Dynamoose, OneTable","authorName":"Rafal Wilinski","authorLink":"https://x.com/rafalwilinski","authorImage":"/images/authors/rafal-wilinski.jpg","image":"/images/posts/dynamodb-orm/social.png"}}},"pageContext":{}},
    "staticQueryHashes": ["3649515864"]}