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();