Working directly with DynamoDB can be intimidating. The AWS SDK is powerful, but it often feels too low-level for everyday development. You spend too much time writing boilerplate, validating data, and worrying about missing type safety.
That’s why I built DynaORM - a simple, type-safe DynamoDB ORM powered by TypeScript and Zod.
DynaORM makes it easy to define schemas, work with strongly typed models, and perform CRUD operations without sacrificing flexibility or control. Whether you’re building a small side project or a large-scale system, DynaORM helps you move faster with confidence.
Why Another ORM for DynamoDB?
If you’ve ever worked with DynamoDB in TypeScript, you’ve probably run into these issues:
-
Type Safety Gaps: The AWS SDK doesn’t enforce strict typing on your data. You end up with runtime surprises.
-
Manual Validation: Ensuring your objects conform to expected shapes usually requires custom validation logic.
-
Boilerplate Overload: CRUD operations, batch writes, and queries often mean rewriting the same code again and again.
-
Complex Querying: Building queries against indexes requires a lot of string concatenation and DynamoDB-specific quirks.
DynaORM solves these pain points by combining the strengths of TypeScript (types), Zod (validation), and a clean ORM-like API.
🚀 Key Features
-
End-to-End Type Safety – Infer types directly from Zod schemas. No more mismatches between your models and database.
-
Schema Validation – Zod ensures your data is always valid before writing to DynamoDB.
-
Clean CRUD API – Simple methods like create, findOne, update, and delete.
-
QueryBuilder – Build expressive, chainable queries for tables and indexes.
-
Batch Operations – Perform batch gets, writes, and upserts with minimal code.
-
Configurable Throttling – Apply per-model or global rate limits to keep AWS happy.
📦 Getting Started
Install DynaORM and its peer dependencies:
npm install dynaorm @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb p-throttle zod
Step 1: Define Your Schema
Schemas are defined using Zod and wrapped with DynaORM’s defineSchema
.
import { defineSchema } from "dynaorm";
import { z } from "zod";
export const userSchema = defineSchema({
tableName: "Users",
partitionKey: "userId",
sortKey: "createdAt",
fields: z.object({
userId: z.string().uuid(),
createdAt: z.string().datetime(),
email: z.string().email(),
username: z.string().min(3).max(20),
age: z.number().optional(),
}),
});
Type inference comes built-in:
import { InferSchema } from "dynaorm";
import { userSchema } from "./schemas/user-schema";
export type User = InferSchema<typeof userSchema>;
const newUser: User = {
userId: "c44a8f2d-7f7d-421b-8f4e-7a1c0a9e6df9",
createdAt: new Date().toISOString(),
email: "[email protected]",
username: "exampleUser",
};
Step 2: Create the Client
You pass your schemas into createClient
along with AWS config and optional throttling rules.
import { createClient } from "dynaorm";
import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
import { userSchema } from "./schemas/user-schema";
const clientConfig: DynamoDBClientConfig = {
region: "ap-southeast-2",
};
export const client = createClient({ users: userSchema }, { config: clientConfig });
Step 3: Perform Operations
CRUD is straightforward and always type-safe:
import { client } from "./client";
async function main() {
// Create
await client.users.create({
userId: "123",
createdAt: new Date().toISOString(),
email: "[email protected]",
username: "newUser",
});
// Read
const user = await client.users.findOne({ userId: "123", createdAt: "..." });
console.log("User:", user);
// Update
await client.users.update({ userId: "123", createdAt: "..." }, { age: 30 });
// Delete
await client.users.delete({ userId: "123", createdAt: "..." });
}
⚡ Advanced Usage
Queries Made Simple
const recentUsers = await client.users
.query()
.where("userId", "=", "123")
.where("createdAt", ">", "2024-01-01T00:00:00Z")
.exec();
Batch Operations
await client.users.upsertMany([
{ userId: "u1", createdAt: "2024-09-01T00:00:00Z", email: "[email protected]", username: "alpha" },
{ userId: "u2", createdAt: "2024-09-02T00:00:00Z", email: "[email protected]", username: "beta" },
]);
Index Support
const electronics = await client.products.findByIndex("byCategory", {
category: "electronics",
});
🛠 When Should You Use DynaORM?
-
✅ You want strong typing without maintaining separate interfaces and schemas.
-
✅ You’re tired of boilerplate DynamoDB queries.
-
✅ You want an ORM-like abstraction without losing DynamoDB’s performance benefits.
-
✅ You need safe queries and validation built-in.
If you need raw DynamoDB performance at all costs and prefer total manual control, stick with the AWS SDK. But if you value developer experience, DynaORM gives you the best of both worlds.