A comprehensive DynamoDB toolkit for TypeScript/JavaScript with automatic single-table design, Laravel-style ORM, migrations, and CLI tools.
- Zero-Config Single-Table Design - Automatically generates pk/sk patterns and GSIs from your model definitions
- Laravel-Style ORM - Familiar query builder with
where(),with(),orderBy(), and more - Automated Migrations - Schema generation, diffing, and migrations from your models
- Factory System - Generate fake data for testing with states and sequences
- Seeding System - Populate your database with test data
- CLI Tools - Manage tables, run migrations, seed data, and more
- DynamoDB Local - Built-in support for local development
- TypeScript First - Full type safety and IntelliSense support
bun install dynamodb-tooling
# or
npm install dynamodb-toolingCreate a dynamodb.config.ts file:
import type { DynamoDBConfig } from 'dynamodb-tooling'
export default {
region: 'us-east-1',
defaultTableName: 'MyApp',
singleTableDesign: {
enabled: true,
partitionKeyName: 'pk',
sortKeyName: 'sk',
keyDelimiter: '#',
gsiCount: 5,
},
local: {
enabled: true,
port: 8000,
},
} satisfies DynamoDBConfigCreate models in your app/models directory:
// app/models/User.ts
import { DynamoDBModel } from 'dynamodb-tooling'
export class User extends DynamoDBModel {
static table = 'users'
static primaryKey = 'id'
static timestamps = true
static softDeletes = true
// Attributes
attributes = {
id: { type: 'string', required: true },
email: { type: 'string', required: true, unique: true },
name: { type: 'string', required: true },
age: { type: 'number' },
}
// Relationships
relationships = {
posts: { type: 'hasMany', model: 'Post', foreignKey: 'userId' },
profile: { type: 'hasOne', model: 'Profile', foreignKey: 'userId' },
}
}import { User } from './app/models/User'
// Find by ID
const user = await User.find('123')
// Query with conditions
const users = await User.query()
.where('status', 'active')
.where('age', '>=', 18)
.orderByDesc('createdAt')
.limit(10)
.get()
// With eager loading
const usersWithPosts = await User.query()
.with('posts')
.get()
// Create
const newUser = await User.create({
email: 'john@example.com',
name: 'John Doe',
})
// Update
await user.update({ name: 'Jane Doe' })
// Soft delete
await user.delete()
// Force delete
await user.forceDelete()The CLI is available as dbtooling:
# DynamoDB Local
dbtooling start # Start DynamoDB Local
dbtooling stop # Stop DynamoDB Local
dbtooling status # Show running instances
dbtooling install # Install DynamoDB Local
# Migrations
dbtooling migrate # Run migrations
dbtooling migrate:status # Show migration status
dbtooling migrate:rollback # Rollback last migration
dbtooling migrate:fresh # Drop and re-migrate
dbtooling migrate:generate # Generate migration from models
# Tables
dbtooling table:create # Create table from models
dbtooling table:describe # Describe a table
dbtooling table:list # List all tables
dbtooling table:delete # Delete a table
# Seeding
dbtooling seed # Run seeders
dbtooling make:seeder User # Generate a seeder
dbtooling make:factory User # Generate a factory
dbtooling db:fresh # Drop, migrate, and seed
# Queries
dbtooling query --pk USER#1 # Query by partition key
dbtooling scan # Scan table
dbtooling get --pk X --sk Y # Get single item
# Utilities
dbtooling access-patterns # Show access patterns
dbtooling export # Export table data
dbtooling import # Import data
dbtooling ci:validate # Validate models for CI// Select specific columns
User.query().select('id', 'name', 'email').get()
// Where conditions
User.query().where('status', 'active').get()
User.query().where('age', '>=', 18).get()
User.query().whereIn('role', ['admin', 'moderator']).get()
User.query().whereBetween('age', 18, 65).get()
User.query().whereNull('deletedAt').get()
User.query().whereBeginsWith('sk', 'USER#').get()
User.query().whereContains('tags', 'featured').get()
// Ordering
User.query().orderBy('name').get()
User.query().orderByDesc('createdAt').get()
User.query().latest().get() // orderBy('createdAt', 'desc')
User.query().oldest().get() // orderBy('createdAt', 'asc')
// Limiting
User.query().limit(10).get()
User.query().take(10).get()
// Pagination
const page1 = await User.query().paginate(20)
const page2 = await User.query().cursorPaginate(cursor, 20)
// Chunking for large datasets
await User.query().chunk(100, async (users) => {
for (const user of users) {
// Process user
}
})// Eager loading
User.query().with('posts').get()
User.query().with('posts', 'profile').get()
User.query().with('posts.comments').get() // Nested
// Relationship counts
User.query().withCount('posts').get()
// Relationship existence
User.query().has('posts').get()
User.query().doesntHave('posts').get()
User.query().whereHas('posts', q => q.where('published', true)).get()await User.query().count()
await User.query().sum('balance')
await User.query().avg('age')
await User.query().min('age')
await User.query().max('age')
await User.query().exists()
await User.query().doesntExist()// Include soft deleted
User.query().withTrashed().get()
// Only soft deleted
User.query().onlyTrashed().get()
// Restore
await user.restore()
// Force delete (permanent)
await user.forceDelete()// Define global scopes
User.addGlobalScope('active', (query) => {
return query.where('status', 'active')
})
// Use scopes
User.query().scope('active').get()Create factories for generating test data:
// factories/UserFactory.ts
import { Factory, uniqueEmail, randomInt } from 'dynamodb-tooling'
Factory.define('User', {
entityType: 'USER',
definition: () => ({
id: crypto.randomUUID(),
email: uniqueEmail(),
name: 'Test User',
age: randomInt(18, 65),
}),
states: {
admin: { role: 'admin' },
inactive: { status: 'inactive' },
},
})
// Usage
const users = await Factory.for('User').count(10).create()
const admin = await Factory.for('User').state('admin').createOne()
const fakeUsers = Factory.for('User').count(5).make() // No persistCreate seeders to populate your database:
// seeders/UserSeeder.ts
import { Seeder, SeederContext } from 'dynamodb-tooling'
export class UserSeeder extends Seeder {
static order = 1
async run(ctx: SeederContext): Promise<void> {
await ctx.factory('USER', {
attributes: () => ({
id: crypto.randomUUID(),
email: `user${Date.now()}@example.com`,
name: 'Test User',
}),
}).count(50).create()
}
}The toolkit automatically generates single-table design patterns from your models:
User:
pk: USER#{id}
sk: USER#{id}
Post:
pk: POST#{id}
sk: POST#{id}
gsi1pk: USER#{userId} (for querying posts by user)
gsi1sk: POST#{id}
- Get User by ID
- Get Post by ID
- Get all Posts by User (via GSI1)
- Get User's Profile (via relationship)
View patterns with:
dbtooling access-patterns --format markdownMigrations are automatically generated from your models:
# Generate migration
dbtooling migrate:generate
# Preview changes
dbtooling migrate --dry-run
# Apply migration
dbtooling migrate
# Rollback
dbtooling migrate:rollbackThe migration system handles:
- Table creation with pk/sk
- GSI creation/deletion
- LSI configuration
- TTL settings
- Stream configuration
interface DynamoDBConfig {
// AWS Settings
region?: string
endpoint?: string
credentials?: {
accessKeyId: string
secretAccessKey: string
}
// Table Settings
defaultTableName: string
tableNamePrefix?: string
tableNameSuffix?: string
// Single-Table Design
singleTableDesign: {
enabled: boolean
partitionKeyName: string // default: 'pk'
sortKeyName: string // default: 'sk'
keyDelimiter: string // default: '#'
entityTypeAttribute: string // default: '_type'
gsiCount: number // default: 5
}
// Query Builder
queryBuilder: {
modelsPath: string
timestampFormat: 'iso' | 'unix' | 'unix_ms'
softDeletes: {
enabled: boolean
attribute: string
}
}
// Capacity
capacity: {
billingMode: 'PAY_PER_REQUEST' | 'PROVISIONED'
read?: number
write?: number
}
// DynamoDB Local
local: {
enabled: boolean
port: number
installPath: string
}
}Start local development:
import { dynamoDb } from 'dynamodb-tooling'
// Start
await dynamoDb.launch({ port: 8000 })
// With persistent storage
await dynamoDb.launch({
port: 8000,
dbPath: './data/dynamodb',
})
// Stop
dynamoDb.stop(8000)Or via CLI:
dbtooling start --port 8000
dbtooling start --db-path ./data # Persistent
dbtooling stopbun testPlease see our releases page for more information on what has changed recently.
Please review the Contributing Guide for details.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the Stacks Discord Server
Stacks OSS will always stay open-sourced, and we will always love to receive postcards from wherever Stacks is used! And we also publish them on our website. Thank you, Spatie.
Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States π
We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.
The MIT License (MIT). Please see LICENSE for more information.
Made with π
