electrodb

This skill covers ElectroDB v2+ patterns for building type-safe, performant DynamoDB applications with single-table design.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "electrodb" with this command: npx skills add tejovanthn/rasikalife/tejovanthn-rasikalife-electrodb

ElectroDB Patterns

This skill covers ElectroDB v2+ patterns for building type-safe, performant DynamoDB applications with single-table design.

Core Concepts

ElectroDB Benefits:

  • Type-safe queries and mutations

  • Automatic key generation

  • Collection queries (fetch related entities)

  • Composite attributes and computed keys

  • Built-in validation

  • Query building with IntelliSense

Key Terms:

  • Entity: Table schema definition

  • Service: Multiple entities working together

  • Collection: Group related entities for efficient queries

  • Access Pattern: How you retrieve data (GSI, queries)

  • Composite Attributes: Combine fields into keys

Installation

npm install electrodb

Entity Definition

Basic Entity

// src/entities/user.entity.ts import { Entity } from "electrodb"; import { dynamoDBClient } from "../lib/db";

export const UserEntity = new Entity( { model: { entity: "user", version: "1", service: "myapp", }, attributes: { userId: { type: "string", required: true, }, email: { type: "string", required: true, }, name: { type: "string", required: true, }, role: { type: ["ADMIN", "TEACHER", "STUDENT"] as const, required: true, default: "STUDENT", }, verified: { type: "boolean", default: false, }, createdAt: { type: "string", required: true, default: () => new Date().toISOString(), readOnly: true, }, updatedAt: { type: "string", required: true, default: () => new Date().toISOString(), set: () => new Date().toISOString(), watch: "*", // Update on any attribute change }, }, indexes: { primary: { pk: { field: "pk", composite: ["userId"], }, sk: { field: "sk", composite: [], }, }, byEmail: { index: "gsi1", pk: { field: "gsi1pk", composite: ["email"], }, sk: { field: "gsi1sk", composite: [], }, }, byRole: { index: "gsi2", pk: { field: "gsi2pk", composite: ["role"], }, sk: { field: "gsi2sk", composite: ["createdAt"], }, }, }, }, { client: dynamoDBClient, table: "MyAppTable" } );

// Type inference export type User = typeof UserEntity.model.schema; export type UserItem = ReturnType<typeof UserEntity.parse>;

Advanced Attributes

export const SessionEntity = new Entity({ model: { entity: "session", version: "1", service: "myapp", }, attributes: { sessionId: { type: "string", required: true, }, userId: { type: "string", required: true, }, courseId: { type: "string", required: true, }, // Composite attribute (virtual) userCourse: { type: "string", hidden: true, // Not returned in queries readOnly: true, get: (_, item) => ${item.userId}#${item.courseId}, }, // Date as string scheduledDate: { type: "string", required: true, validate: (value) => { const date = new Date(value); if (isNaN(date.getTime())) { throw new Error("Invalid date format"); } }, }, // Duration in minutes durationMinutes: { type: "number", required: true, validate: (value) => { if (value < 15 || value > 480) { throw new Error("Duration must be between 15 and 480 minutes"); } }, }, // Enum-like status status: { type: ["SCHEDULED", "IN_PROGRESS", "COMPLETED", "CANCELLED"] as const, required: true, default: "SCHEDULED", }, // Optional nested object metadata: { type: "map", properties: { zoomMeetingId: { type: "string" }, recordingUrl: { type: "string" }, notes: { type: "string" }, }, }, // Array of strings tags: { type: "list", items: { type: "string", }, }, // Set attribute (for watch) watchedAttributes: { type: "set", items: "string", }, }, indexes: { primary: { pk: { field: "pk", composite: ["sessionId"], }, sk: { field: "sk", composite: [], }, }, byCourse: { index: "gsi1", pk: { field: "gsi1pk", composite: ["courseId"], }, sk: { field: "gsi1sk", composite: ["scheduledDate"], }, }, byUser: { index: "gsi2", pk: { field: "gsi2pk", composite: ["userId"], }, sk: { field: "gsi2sk", composite: ["scheduledDate"], }, }, }, });

CRUD Operations

Create

// Create single item const user = await UserEntity.create({ userId: "user_123", email: "john@example.com", name: "John Doe", role: "STUDENT", }).go();

// Create with custom options const user = await UserEntity.create({ userId: "user_123", email: "john@example.com", name: "John Doe", }).go({ response: "all_new", // Return all attributes });

// Conditional create (fail if exists) const user = await UserEntity.create({ userId: "user_123", email: "john@example.com", name: "John Doe", }).go({ conditions: { exists: false }, // Only create if doesn't exist });

Read (Get)

// Get single item const user = await UserEntity.get({ userId: "user_123", }).go();

// user.data contains the item or null if not found if (!user.data) { throw new Error("User not found"); }

// Get with specific attributes const user = await UserEntity.get({ userId: "user_123", }).go({ attributes: ["name", "email"], // Only fetch these fields });

// Get with consistent read const user = await UserEntity.get({ userId: "user_123", }).go({ consistent: true, // Consistent read (costs more) });

Update

// Update specific attributes const updated = await UserEntity.update({ userId: "user_123", }).set({ name: "Jane Doe", verified: true, }).go();

// Add to number await SessionEntity.update({ sessionId: "session_123", }).add({ attendeeCount: 1, // Increment by 1 }).go();

// Remove attribute await UserEntity.update({ userId: "user_123", }).remove(["temporaryToken"]).go();

// Conditional update await UserEntity.update({ userId: "user_123", }).set({ verified: true, }).go({ conditions: { verified: false }, // Only update if not already verified });

// Update with custom condition expression await UserEntity.update({ userId: "user_123", }).set({ name: "New Name", }).go({ conditions: { attr: "role", eq: "ADMIN", // Only update if role is ADMIN }, });

Delete

// Delete item await UserEntity.delete({ userId: "user_123", }).go();

// Conditional delete await UserEntity.delete({ userId: "user_123", }).go({ conditions: { role: "STUDENT" }, // Only delete if student });

// Return deleted item const deleted = await UserEntity.delete({ userId: "user_123", }).go({ response: "all_old", // Return the deleted item });

Queries

Basic Queries

// Query by primary key const users = await UserEntity.query .primary({ userId: "user_123", }) .go();

// users.data is an array of items

// Query with begins_with const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .begins({ scheduledDate: "2025-01", // All sessions in January 2025 }) .go();

// Query with between const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .between( { scheduledDate: "2025-01-01" }, { scheduledDate: "2025-01-31" } ) .go();

// Query with gt/gte/lt/lte const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .gt({ scheduledDate: "2025-01-01" }) // Greater than .go();

Query Options

// Limit results const users = await UserEntity.query .byRole({ role: "STUDENT", }) .go({ limit: 10, // Only return 10 items });

// Pagination const firstPage = await UserEntity.query .byRole({ role: "STUDENT", }) .go({ limit: 10, });

// Get next page using cursor if (firstPage.cursor) { const secondPage = await UserEntity.query .byRole({ role: "STUDENT", }) .go({ limit: 10, cursor: firstPage.cursor, }); }

// Scan index forward/backward const latest = await SessionEntity.query .byCourse({ courseId: "course_123", }) .go({ order: "desc", // Most recent first (ScanIndexForward: false) });

// Filter after query const activeSessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .where( ({ status }, { eq }) => eq(status, "IN_PROGRESS") ) .go();

// Select specific attributes const users = await UserEntity.query .byRole({ role: "STUDENT", }) .go({ attributes: ["userId", "name", "email"], });

Complex Filters

// Multiple conditions (AND) const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .where( ({ status, durationMinutes }, { eq, gte }) => ${eq(status, "COMPLETED")} AND ${gte(durationMinutes, 60)} ) .go();

// OR conditions const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .where( ({ status }, { eq }) => ${eq(status, "SCHEDULED")} OR ${eq(status, "IN_PROGRESS")} ) .go();

// NOT condition const users = await UserEntity.query .byRole({ role: "STUDENT", }) .where( ({ verified }, { eq }) => NOT ${eq(verified, true)} ) .go();

// Contains (for strings) const users = await UserEntity.scan .where( ({ email }, { contains }) => contains(email, "@gmail.com") ) .go();

// Between (in filter) const sessions = await SessionEntity.query .byCourse({ courseId: "course_123", }) .where( ({ durationMinutes }, { between }) => between(durationMinutes, 30, 120) ) .go();

Scan Operations

// Scan entire table (use sparingly!) const allUsers = await UserEntity.scan.go();

// Scan with filter const verifiedUsers = await UserEntity.scan .where( ({ verified }, { eq }) => eq(verified, true) ) .go();

// Scan with pagination const firstPage = await UserEntity.scan.go({ limit: 100, });

if (firstPage.cursor) { const secondPage = await UserEntity.scan.go({ cursor: firstPage.cursor, }); }

// Parallel scan for large tables const segment1 = await UserEntity.scan.go({ segments: { total: 4, segment: 0 }, }); const segment2 = await UserEntity.scan.go({ segments: { total: 4, segment: 1 }, }); // ... segments 2 and 3

Batch Operations

Batch Get

// Batch get multiple items const results = await UserEntity.get([ { userId: "user_1" }, { userId: "user_2" }, { userId: "user_3" }, ]).go();

// results.data is an array of items (nulls for not found)

// Batch get with options const results = await UserEntity.get([ { userId: "user_1" }, { userId: "user_2" }, ]).go({ unprocessed: "raw", // Return unprocessed keys consistent: true, });

Batch Write

// Batch put await UserEntity.put([ { userId: "user_1", email: "user1@example.com", name: "User 1", role: "STUDENT", }, { userId: "user_2", email: "user2@example.com", name: "User 2", role: "STUDENT", }, ]).go();

// Batch delete await UserEntity.delete([ { userId: "user_1" }, { userId: "user_2" }, ]).go();

// Note: Batch operations support up to 25 items // For more, chunk them: const chunks = chunk(items, 25); for (const chunk of chunks) { await UserEntity.put(chunk).go(); }

Transactions

import { Entity, Service } from "electrodb";

// Define entities first const service = new Service({ user: UserEntity, session: SessionEntity, });

// Transactional write await service .transaction .write(({ user, session }) => [ user.create({ userId: "user_123", email: "john@example.com", name: "John Doe", role: "STUDENT", }), session.create({ sessionId: "session_456", userId: "user_123", courseId: "course_789", scheduledDate: "2025-01-15", durationMinutes: 60, status: "SCHEDULED", }), ]) .go();

// Conditional transaction await service .transaction .write(({ user, session }) => [ user.update({ userId: "user_123" }) .set({ verified: true }) .commit({ conditions: { verified: false } }), session.create({ sessionId: "session_456", userId: "user_123", courseId: "course_789", scheduledDate: "2025-01-15", durationMinutes: 60, }).commit({ conditions: { exists: false } }), ]) .go();

// Transactional get (requires primary keys) const results = await service .transaction .get([ { user: { userId: "user_123" } }, { session: { sessionId: "session_456" } }, ]) .go();

Collections

Collections allow querying multiple entity types together:

export const UserEntity = new Entity({ model: { entity: "user", version: "1", service: "myapp", }, attributes: { userId: { type: "string", required: true }, email: { type: "string", required: true }, name: { type: "string", required: true }, }, indexes: { primary: { pk: { field: "pk", composite: ["userId"] }, sk: { field: "sk", composite: [] }, }, byOrg: { index: "gsi1", pk: { field: "gsi1pk", composite: ["orgId"] }, sk: { field: "gsi1sk", composite: ["userId"] }, collection: "organization", // Collection name }, }, });

export const CourseEntity = new Entity({ model: { entity: "course", version: "1", service: "myapp", }, attributes: { courseId: { type: "string", required: true }, orgId: { type: "string", required: true }, title: { type: "string", required: true }, }, indexes: { primary: { pk: { field: "pk", composite: ["courseId"] }, sk: { field: "sk", composite: [] }, }, byOrg: { index: "gsi1", pk: { field: "gsi1pk", composite: ["orgId"] }, sk: { field: "gsi1sk", composite: ["courseId"] }, collection: "organization", // Same collection }, }, });

// Create service const service = new Service({ user: UserEntity, course: CourseEntity, });

// Query collection (gets both users and courses for an org) const orgData = await service.collections .organization({ orgId: "org_123" }) .go();

// orgData.data contains { user: [...], course: [...] }

Service Patterns

// Create a service with multiple entities const AppService = new Service( { user: UserEntity, session: SessionEntity, course: CourseEntity, enrollment: EnrollmentEntity, }, { client: dynamoDBClient, table: "MyAppTable" } );

// Use service for transactions await AppService.transaction.write(({ user, enrollment }) => [ user.update({ userId: "user_123" }).set({ verified: true }), enrollment.create({ enrollmentId: "enroll_456", userId: "user_123", courseId: "course_789", enrolledAt: new Date().toISOString(), }), ]).go();

// Collections query const userData = await AppService.collections .userCourses({ userId: "user_123" }) .go();

Advanced Patterns

Optimistic Locking

// Add version attribute export const UserEntity = new Entity({ model: { entity: "user", version: "1", service: "myapp" }, attributes: { userId: { type: "string", required: true }, name: { type: "string", required: true }, version: { type: "number", required: true, default: 0, watch: "*", // Increment on any change set: (_, item) => (item.version ?? 0) + 1, }, }, indexes: { primary: { pk: { field: "pk", composite: ["userId"] }, sk: { field: "sk", composite: [] }, }, }, });

// Update with version check const user = await UserEntity.get({ userId: "user_123" }).go();

await UserEntity.update({ userId: "user_123" }) .set({ name: "New Name" }) .go({ conditions: { version: user.data!.version }, // Only update if version matches });

Soft Delete

export const UserEntity = new Entity({ model: { entity: "user", version: "1", service: "myapp" }, attributes: { userId: { type: "string", required: true }, name: { type: "string", required: true }, deletedAt: { type: "string" }, // ISO timestamp or null }, indexes: { primary: { pk: { field: "pk", composite: ["userId"] }, sk: { field: "sk", composite: [] }, }, active: { index: "gsi1", pk: { field: "gsi1pk", composite: ["deletedAt"] }, sk: { field: "gsi1sk", composite: ["userId"] }, }, }, });

// Soft delete await UserEntity.update({ userId: "user_123" }) .set({ deletedAt: new Date().toISOString() }) .go();

// Query only active users (where deletedAt is not set) // Use sparse index - items without gsi1pk won't appear const activeUsers = await UserEntity.query .active({ deletedAt: "ACTIVE" }) // Special marker .go();

Sparse Indexes

// Only index published posts export const PostEntity = new Entity({ model: { entity: "post", version: "1", service: "myapp" }, attributes: { postId: { type: "string", required: true }, title: { type: "string", required: true }, published: { type: "boolean", default: false }, publishedAt: { type: "string" }, // Only set when published }, indexes: { primary: { pk: { field: "pk", composite: ["postId"] }, sk: { field: "sk", composite: [] }, }, published: { index: "gsi1", pk: { field: "gsi1pk", composite: [], // No composite, just a constant template: "PUBLISHED#POST", // All published posts }, sk: { field: "gsi1sk", composite: ["publishedAt"], }, }, }, });

// Only items with gsi1pk set will appear in the index await PostEntity.create({ postId: "post_123", title: "My Post", published: true, publishedAt: new Date().toISOString(), }).go();

// Query only published posts const publishedPosts = await PostEntity.query .published({}) .go();

Time-to-Live (TTL)

export const SessionTokenEntity = new Entity({ model: { entity: "sessionToken", version: "1", service: "myapp" }, attributes: { token: { type: "string", required: true }, userId: { type: "string", required: true }, expiresAt: { type: "number", // Unix timestamp in seconds required: true, default: () => Math.floor(Date.now() / 1000) + 86400, // 24 hours }, }, indexes: { primary: { pk: { field: "pk", composite: ["token"] }, sk: { field: "sk", composite: [] }, }, }, });

// Configure TTL on the table (do this once) // Table attribute: expiresAt // DynamoDB will automatically delete expired items

Type Helpers

// Extract types from entity type User = typeof UserEntity.model.schema; type UserItem = ReturnType<typeof UserEntity.parse>; type UserKeys = Parameters<typeof UserEntity.get>[0];

// Infer return types type QueryResult = Awaited<ReturnType<typeof UserEntity.query.byRole>>; type CreateResult = Awaited<ReturnType<typeof UserEntity.create>>;

// Custom typed helpers export async function getUser(userId: string): Promise<UserItem | null> { const result = await UserEntity.get({ userId }).go(); return result.data; }

export async function requireUser(userId: string): Promise<UserItem> { const user = await getUser(userId); if (!user) { throw new Error("User not found"); } return user; }

Error Handling

import { ElectroError } from "electrodb";

try { await UserEntity.create({ userId: "user_123", email: "john@example.com", name: "John Doe", role: "STUDENT", }).go(); } catch (error) { if (error instanceof ElectroError) { // ElectroDB-specific error console.error("ElectroDB error:", error.message);

// Check error codes
if (error.code === 4001) {
  // Validation error
} else if (error.code === 5003) {
  // Item already exists (conditional check failed)
}

} else { // DynamoDB SDK error console.error("DynamoDB error:", error); } }

// Common error codes: // 4001: Invalid/missing required attribute // 4002: Invalid attribute value // 5001: DynamoDB operation failed // 5003: Conditional check failed

Best Practices

  1. Use Type Exports

// Export types for reuse export type User = typeof UserEntity.model.schema; export type UserItem = ReturnType<typeof UserEntity.parse>;

  1. Validate Complex Data

attributes: { email: { type: "string", required: true, validate: (email) => { if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)) { throw new Error("Invalid email format"); } }, }, }

  1. Use Readonly for Timestamps

createdAt: { type: "string", required: true, default: () => new Date().toISOString(), readOnly: true, // Cannot be updated }

  1. Watch for Auto-Updates

updatedAt: { type: "string", required: true, default: () => new Date().toISOString(), set: () => new Date().toISOString(), watch: "*", // Update on any attribute change }

  1. Use Hidden Attributes for Computed Values

fullName: { type: "string", hidden: true, // Not stored in DB get: (_, item) => ${item.firstName} ${item.lastName}, }

  1. Design Access Patterns First

Before creating entities, list your access patterns:

  • Get user by ID

  • Get user by email

  • List users by role

  • List sessions by course

  • List sessions by user

Then design indexes to support these patterns.

  1. Use Collections for Related Entities

Group related entities under the same partition key for efficient queries.

  1. Check .data Before Using

const result = await UserEntity.get({ userId }).go(); if (!result.data) { throw new Error("Not found"); } const user = result.data; // Now TypeScript knows it's not null

  1. Use Services for Related Operations

const service = new Service({ user: UserEntity, session: SessionEntity, });

// Better for transactions and collections

  1. Leverage Composite Attributes

sk: { field: "sk", composite: ["courseId", "userId"], // Creates: COURSE#123#USER#456 }

Common Gotchas

  • Batch size limit: Max 25 items per batch operation

  • Transaction limit: Max 100 items per transaction (across all tables)

  • Cursor is opaque: Don't parse or modify cursors

  • Consistent reads: Cost more, use sparingly

  • Scan is expensive: Avoid scanning large tables

  • Index projections: Consider what attributes you need in GSIs

  • Type inference: Let TypeScript infer types from entity definitions

  • Hidden attributes: Not stored in DB, computed on read

  • ReadOnly attributes: Can't be updated after creation

  • Watch attribute: Triggers on specified attribute changes

Testing

import { describe, test, expect, beforeAll } from "vitest"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { UserEntity } from "./user.entity";

// Use local DynamoDB for tests const localClient = new DynamoDBClient({ endpoint: "http://localhost:8000", });

describe("UserEntity", () => { beforeAll(async () => { // Create test table await createTestTable(); });

test("creates user", async () => { const result = await UserEntity.create({ userId: "user_123", email: "test@example.com", name: "Test User", role: "STUDENT", }).go();

expect(result.data).toMatchObject({
  userId: "user_123",
  email: "test@example.com",
  name: "Test User",
});

});

test("queries users by role", async () => { const result = await UserEntity.query .byRole({ role: "STUDENT" }) .go();

expect(result.data.length).toBeGreaterThan(0);

}); });

Resources

  • ElectroDB Documentation

  • ElectroDB GitHub

  • DynamoDB Best Practices

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-templates

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review
General

conform

No summary provided by upstream source.

Repository SourceNeeds Review