Dynamoose: a Mongoose-style schema layer over DynamoDB
Dynamoose is a modeling tool for Amazon's DynamoDB
At a glance
- What is it?
- Dynamoose wraps Amazon's DynamoDB in a Mongoose-shaped schema API, adding validation, required attributes and transforms that DynamoDB does not have natively. It buys type safety and single table design support at the cost of schema rigidity in a store designed to need no schema.
- Who is it for?
- Adopt Dynamoose when you want DynamoDB to behave like a validated document store and your team already writes in the Mongoose idiom, and start by putting Single Table Design Support and per-attribute transforms to work. Do not adopt it for tables whose attributes genuinely vary, or where a full-table Book.scan().exec() would sit on a hot path.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 33 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the modelling layer actually adds
Dynamoose describes itself as a modeling tool for Amazon's DynamoDB, heavily inspired by Mongoose, and that inheritance is the whole design brief. DynamoDB has no schema, so nothing stops a client writing an item with a key one code path expects and a different key another. Dynamoose puts a schema in front of that and enforces it.
The feature list is where the value sits: type safety, a high level API, easy to use syntax, DynamoDB Single Table Design Support, the ability to transform data before saving or retrieving items, strict data modeling covering validation and required attributes, support for DynamoDB Transactions, powerful conditional and filtering support, callback and promise support, AWS Multi-region support, and DynamoDB Streams support.
Two entries on that list are doing more work than the others. Single Table Design Support matters because the single-table pattern pushes several entity types into one table with composite keys, which is exactly where a schema library earns its keep. Transforms matter because DynamoDB has no types beyond strings, numbers and binary, so dates and numbers have to be serialised on the way in and revived on the way out. A library that handles both removes a category of bug that is otherwise written by hand in every module.
Defining a model with a hash key, required fields and an enum
The getting-started example defines a Book model, and it is worth reading attribute by attribute because each one is a DynamoDB concern expressed in JavaScript terms.
import * as dynamoose from "dynamoose";
import * as crypto from "crypto";
const Book = dynamoose.model("Book", {
"id": {
"type": String,
"hashKey": true,
"default": () => crypto.randomUUID()
},
"title": {
"type": String,
"required": true
},
"publishedDate": {
"type": Date,
"required": true
},
"pageCount": Number
});`hashKey: true` on `id` declares the partition key. `default` is a function, so `crypto.randomUUID()` runs per item rather than once at module load, which is the difference between one shared identifier and one per record. `required: true` makes the attribute mandatory, and that check happens before the call leaves your process.
The `genre` attribute in the longer example goes further and lists every value it will accept.
"genre": {
"type": String,
"required": true,
"enum": ["fantasy", "sci-fi", "mystery", "other"]
}An enum is where a modelling layer starts to behave like a database rather than a type helper. Writing an item fails if the value is not on that list, which means adding a genre is a code change and a deploy, not a silent data problem three weeks later.
Writing and reading: save, scan and exec
Two calls carry the whole example. Creating an instance constructs the document, and `save()` persists it.
const newBook = new Book({
"title": "Harry Potter and the Philosopher's Stone",
"author": "J.K. Rowling",
"publishedDate": new Date("1997-06-26"),
"genre": "fantasy",
"pageCount": 223
});
await newBook.save();Reading back uses a chain that ends in `exec()`.
const allBooks = await Book.scan().exec();The README labels that line as retrieving all items from the Book table, which is the important detail: `scan` is the full-table path, not an indexed lookup. DynamoDB has no query language to fall back on, so a scan reads until it finds what you asked for. That makes it the right shape for an admin screen and the wrong shape for a hot path, and it makes conditional and filtering support, which the feature list calls out explicitly, the thing that narrows a read rather than returning everything.
The `exec()` suffix is also the switch for the dual API the README advertises. Callback and promise support means the same chain can end differently depending on what you need, and `exec()` is where the promise form resolves.
Transforms are the reason to use a library at all
The feature list mentions the ability to transform data before saving or retrieving items, and that line deserves more attention than it usually gets.
DynamoDB's type system is thin. Items are attribute name and value pairs, and the values are strings, numbers or binary. A JavaScript `Date` has no representation there, so something has to convert it on the way in and something has to convert it back on the way out. Doing that by hand produces two functions per model, and the failure mode when you forget one is a date arriving at the UI as a number or a string, which does not throw and does not get noticed for a while.
Transforms centralise that per attribute, so the serialisation decision lives next to the type declaration. The same mechanism covers derived and computed values, which is useful when an attribute you want to query on is not an attribute the caller supplied.
Strict data modeling, the phrase the README uses for validation and required attributes, is the other half of the bargain. It is also the half that costs you flexibility. DynamoDB's design is deliberately schemaless so that adding a field does not require a migration, and a required-attribute check converts a free-form write into a validated one. Neither is wrong, but they are opposing philosophies and adopting Dynamoose means choosing validation over schema freedom.
The branch table is a documentation warning
The README's Branch Strategy section states plainly that work on the listed branches may be further ahead than what is on NPM, and that the documentation links are also reflective of the published version on NPM. Then it publishes a table with five rows.
The `main` branch tracks 4.x.x and documents at dynamoose.pages.dev with no NPM tag attached, because it is unreleased. The `v3` branch tracks 3.3.x at v3.dynamoose.pages.dev, and the `v3.3.0` tag maps to the `latest-3` NPM tag and its own site at v3.dynamoosejs.com. The same pattern repeats one version down: `v2` tracks 2.8.x, and the `v2.8.8` tag carries the `latest-2` tag with documentation at v2.dynamoosejs.com.
Two consequences. The documentation site and the code you have installed are not guaranteed to match, so when the docs describe a method you do not have, check which tag you are on. And two major versions are maintained in parallel, with 2.x still receiving tags under the `latest-2` NPM tag.
If you want to run unreleased code, the README gives the form, replacing BRANCH with a name from the table.
npm install dynamoose/dynamoose#BRANCHRead the repository as three tracked versions plus two active branches, not as a single current release.
A lerna monorepo with one runtime dependency
The repository is a lerna workspace over `packages/*`, and the root `package.json` is a build orchestrator rather than the published artifact.
{
"workspaces": ["packages/*"],
"scripts": {
"build": "lerna run build",
"test": "lerna run test",
"lint": "eslint . --ext .ts,.js --max-warnings 0"
}
}The runtime dependency list has exactly one entry, `js-object-utilities` at `^2.2.1`. A DynamoDB client wrapper that depends only on an object helper and nothing else means the AWS SDK is supplied by your application, not vendored here. That is a deliberate design and it is the reason the dependency footprint stays small.
The tooling tells you what to expect from a contribution. TypeScript at `^5.3.3`, jest at `^29.7.0`, and an ESLint invocation that passes `--max-warnings 0`, so a new warning fails the lint script rather than scrolling past. Docs are a separate npm project in `docs/` with its own install and build scripts, and `site:crowdin:sync` points at the Crowdin project behind dynamoosejs.com, which is how the documentation is translated.
Where Dynamoose is the wrong choice
Four situations argue against it, and they are structural rather than fixable with configuration.
Schema rigidity against a schemaless store is the first, and it is the one teams hit late. An `enum` list or a `required: true` is a code change and a deploy, on a database whose entire selling point is that it does not need one. If your product genuinely writes varying attributes, you are adding a migration path you did not want. The second is the full-table read. `Book.scan().exec()` returning every item is fine at a thousand rows and not fine at a million, and Dynamoose will not save you from that. The third is that the schema has to be defined before you can read data written by anything else, so an existing table populated by other services needs either a matching schema or the raw client anyway. The fourth is bus factor. The `package.json` names one author and one contributor, and while the repository is not archived and the last push was on 2026-08-31, a modelling layer is infrastructure, and infrastructure outlives the people who chose it.
Dynamoose against calling DynamoDB directly
The alternative is to call the client yourself, and the README actually points at it. Transactions, Streams and multi-region are all documented by linking to Amazon's own guides rather than to a Dynamoose wrapper page, which tells you where the boundary sits.
The difference in approach is one of where the type information lives. Against the raw client you write attribute names and value types at every call site, and nothing checks that two code paths agree on whether `publishedDate` is a Date or an ISO string. You get full access to every DynamoDB feature as it ships, including anything Dynamoose has not wrapped yet, and you pay for it in hand-written serialisation and no schema enforcement.
Dynamoose inverts that. The schema is declared once and the library derives the calls, which is why it can validate before a write and transform on both sides of one. The cost is the abstraction: anything Dynamoose does not model stays awkward, and its version lags the service. The transaction and streams links in the feature list are a good place to start checking which side of that line a feature falls on before you commit.
Editorial conclusion
Adopt Dynamoose when you want DynamoDB to behave like a validated document store and your team already writes in the Mongoose idiom, and start by putting Single Table Design Support and per-attribute transforms to work. Do not adopt it for tables whose attributes genuinely vary, or where a full-table Book.scan().exec() would sit on a hot path. Verify first which tag you are on, because the README warns the main branch and the documentation site can both run ahead of the published NPM version.
Frequently asked questions
How do I install Dynamoose and create my first model?
The package is dynamoose on NPM, and a model is declared with dynamoose.model, passing the name and an attribute map. The getting-started example uses Book with an id attribute marked hashKey: true and a default of () => crypto.randomUUID().
What licence is Dynamoose released under?
The Unlicense, as declared in the package.json license field. That is a public-domain dedication rather than a conventional permissive licence; check the LICENSE file for the exact terms, as this is not legal advice.
How do I query data with Dynamoose?
Chains end in exec(). The README example uses Book.scan().exec() to retrieve all items from the table, so scan is the full-table path, and conditional and filtering support is the listed way to narrow a read.
What is the difference between the main branch and the published version?
The README states work on the listed branches may be ahead of what is on NPM, and its documentation links reflect the published version. Unreleased code can be installed with npm install dynamoose/dynamoose#BRANCH, replacing BRANCH with main, v3 or v2.
Does Dynamoose support DynamoDB transactions and streams?
Both are on the README's key feature list, alongside single table design support, multi-region support and the ability to transform data before saving or retrieving items.
When was Dynamoose last released?
Release v4.2.0 was published on 2026-05-05, after v4.1.4 and v4.1.5 which both landed on 2026-01-11. The repository is not archived and the last push was on 2026-08-31, so commits are landing after the latest tag.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dynamoose-dynamoose)