Quick Start

Install, create a session in beforeAll, and write a test. Local services start as a subprocess — your AWS SDK v3 clients talk to them automatically.

1

Install

npm install local-web-services-typescript-sdk # also requires local-web-services (Python) for ldk dev pip install local-web-services
2

Configure Jest

// jest.config.ts export default { preset: 'ts-jest', testEnvironment: 'node', testTimeout: 60000, // allow time for ldk dev to start };
3

Write your test

// orders.test.ts import { LwsSession } from 'local-web-services-typescript-sdk'; let session: LwsSession; beforeAll(async () => { session = await LwsSession.create({ tables: [{ name: 'Orders', partitionKey: 'id' }], queues: ['OrderQueue'], }); }); afterAll(async () => { await session.close(); }); beforeEach(async () => { await session.reset(); // wipe state between tests }); test('creates an order', async () => { // Call your real application code const { createOrder } = await import('./orders'); await createOrder({ id: '42', status: 'pending' }); // Assert using the DynamoDB helper const table = session.dynamodb('Orders'); const item = await table.assertItemExists({ id: { S: '42' } }); expect(item.status.S).toBe('pending'); }); test('order sends a queue message', async () => { const { createOrder } = await import('./orders'); await createOrder({ id: '99', status: 'pending' }); const queue = session.sqs('OrderQueue'); await queue.assertMessageCount(1); });
4

Run your tests

npx jest

The session starts ldk dev once in beforeAll, then reset() wipes state before each test.

Drop-in Replacement — Zero Code Changes

Your application already uses AWS SDK v3. The SDK returns fully-typed DynamoDBClient, SQSClient, and S3Client instances pre-configured to talk to local services. Nothing in your application needs to change.

Your application code

// orders.ts — unchanged import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'; const dynamo = new DynamoDBClient({}); const sqs = new SQSClient({}); export async function createOrder(order: { id: string; status: string }) { await dynamo.send(new PutItemCommand({ TableName: 'Orders', Item: { id: { S: order.id }, status: { S: order.status }, }, })); await sqs.send(new SendMessageCommand({ QueueUrl: process.env.ORDER_QUEUE_URL!, MessageBody: JSON.stringify(order), })); }

Your test

// orders.test.ts import { LwsSession } from 'local-web-services-typescript-sdk'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; let session: LwsSession; beforeAll(async () => { session = await LwsSession.create({ tables: [{ name: 'Orders', partitionKey: 'id' }], queues: ['OrderQueue'], }); // Pre-configured client — same type as your app uses const client = session.client<DynamoDBClient>('dynamodb'); }); test('creates an order', async () => { const { createOrder } = await import('./orders'); await createOrder({ id: '42', status: 'pending' }); const table = session.dynamodb('Orders'); await table.assertItemExists({ id: { S: '42' } }); });
🔓

No Credentials

Local services accept any credentials. No AWS account, no IAM setup, no environment variables required in your test environment.

∅

No Refactoring

Application code is identical to production. The SDK starts local services and configures the endpoint — no dependency injection, no mocking libraries.

🔷

Fully Typed

session.client<DynamoDBClient>('dynamodb') returns the exact same type your application uses. Full TypeScript inference and IDE completion.

LwsSession

All factory methods are async — they start ldk dev and wait until services are ready before resolving.

Factory methods

import { LwsSession } from 'local-web-services-typescript-sdk'; // Explicit resource declaration const session = await LwsSession.create({ tables: [ { name: 'Orders', partitionKey: 'id' }, { name: 'Products', partitionKey: 'sku', sortKey: 'version' }, ], queues: ['OrderQueue', 'DeadLetterQueue'], buckets: ['ReceiptsBucket'], }); // Auto-discover from CDK cloud assembly (cdk.out/) const session = await LwsSession.fromCdk('../my-cdk-project'); // Auto-discover from Terraform .tf files const session = await LwsSession.fromHcl('../my-terraform-project'); // Always close after your tests await session.close();

Client factory

import { DynamoDBClient, SQSClient, S3Client, SNSClient, SSMClient, SecretsManagerClient, SFNClient, } from '@aws-sdk/client-*'; const dynamo = session.client<DynamoDBClient>('dynamodb'); const sqs = session.client<SQSClient>('sqs'); const s3 = session.client<S3Client>('s3'); const sns = session.client<SNSClient>('sns'); const ssm = session.client<SSMClient>('ssm'); const secrets = session.client<SecretsManagerClient>('secretsmanager'); const sfn = session.client<SFNClient>('stepfunctions'); await session.reset(); // clear all tables, queues, buckets

Resource Helpers

Typed async helpers with built-in assertion methods. Skip the boilerplate.

DynamoDB

const table = session.dynamodb('Orders'); await table.put({ id: { S: '1' }, status: { S: 'pending' } }); const item = await table.get({ id: { S: '1' } }); await table.delete({ id: { S: '1' } }); const items = await table.scan(); await table.assertItemExists({ id: { S: '1' } }); // returns item await table.assertItemCount(5);

SQS

const queue = session.sqs('OrderQueue'); await queue.send({ orderId: '42', total: 99.99 }); // object → JSON await queue.send('plain text message'); const messages = await queue.receive({ maxMessages: 10 }); await queue.purge(); await queue.assertMessageCount(3); console.log(queue.url); // http://localhost:.../OrderQueue

S3

const bucket = session.s3('ReceiptsBucket'); await bucket.put('receipts/001.pdf', Buffer.from('PDF bytes')); await bucket.put('config.json', JSON.stringify({ key: 'value' })); const data = await bucket.get('receipts/001.pdf'); // Buffer const text = await bucket.getText('config.json'); // string const keys = await bucket.listKeys({ prefix: 'receipts/' }); await bucket.delete('config.json'); await bucket.assertObjectExists('receipts/001.pdf'); await bucket.assertObjectCount({ expected: 5, prefix: 'receipts/' });

Operation Faking

Override any AWS operation response with a fluent builder. No changes to application code needed.

// Inject throttling await session.fake('dynamodb').operation('PutItem').respond({ status: 400, body: { __type: 'ProvisionedThroughputExceededException' }, }); // Add artificial latency await session.fake('sqs').operation('SendMessage').respond({ status: 200, delayMs: 500, }); // Match on a specific request header await session.fake('s3') .operation('GetObject') .withHeader('x-request-id', 'test-123') .respond({ status: 403, body: { __type: 'AccessDenied' } }); // Clear all fakes for a service await session.fake('dynamodb').clear();

Chaos Engineering

Inject failure conditions programmatically and verify your application handles them gracefully.

await session.chaos('dynamodb').errorRate(0.3).latency({ minMs: 50, maxMs: 200 }).apply(); await session.chaos('sqs').connectionResetRate(0.1).timeoutRate(0.05).apply(); await session.chaos('dynamodb').clear();
test('handles DynamoDB latency', async () => { await session.chaos('dynamodb').latency({ minMs: 100, maxMs: 500 }).apply(); const start = Date.now(); const { createOrder } = await import('./orders'); await createOrder({ id: '42', status: 'pending' }); const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(100); await session.chaos('dynamodb').clear(); });

IAM Authorization

Switch IAM modes and define named identities with inline policies to test authorization behaviour.

await session.iam.mode('enforce').apply(); await session.iam.mode('audit').apply(); await session.iam.mode('disabled').apply(); // default await session.iam.defaultIdentity('service-account').apply(); await session.iam .identity('readonly') .allow(['dynamodb:GetItem', 'dynamodb:Scan']) .apply();