Getting Started with TypeScript
This guide walks you through building a client application using the TypeScript SDK.
Prerequisites
Section titled “Prerequisites”- Node.js 20.0 or later
- pnpm 9.0 or later
- An EvidentSource server instance with deployed state changes and state views
Quick Start with Templates
Section titled “Quick Start with Templates”If you’re building state changes and state views (not just a client), use the project template:
# Clone the SDKs repositorygit clone https://github.com/evidentsystems/evidentsource-sdkscd evidentsource-sdks/typescript/packages/evidentsource-functions/templates
# Create a new project./create-project.sh my-app my-database open-account active-accountsThis creates a complete project with:
- domain/ - Shared types for events and commands
- state-changes/ - Example state change component
- state-views/ - Example state view component
- build_all.sh - Build all WASM components
- install.sh - Deploy components to EvidentSource
- CLAUDE.md - AI assistant context for development
cd my-apppnpm installpnpm build./build_all.sh # Build WASM components./install.sh --all # Deploy to serverSee the Template Guide for full documentation.
Client-Only Project Setup
Section titled “Client-Only Project Setup”For building client applications that connect to existing state changes/views:
1. Create a New Project
Section titled “1. Create a New Project”mkdir my-banking-clientcd my-banking-clientpnpm init2. Install Dependencies
Section titled “2. Install Dependencies”# Install from git (until published to npm)pnpm add github:evidentsystems/evidentsource-sdks#main:typescript/packages/evidentsource-corepnpm add github:evidentsystems/evidentsource-sdks#main:typescript/packages/evidentsource-clientOr clone and link locally:
git clone https://github.com/evidentsystems/evidentsource-sdks.gitcd evidentsource-sdks/typescriptpnpm installpnpm build
# In your projectpnpm link ../evidentsource-sdks/typescript/packages/evidentsource-corepnpm link ../evidentsource-sdks/typescript/packages/evidentsource-client3. Configure TypeScript
Section titled “3. Configure TypeScript”Create tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src/**/*"]}Connecting to EvidentSource
Section titled “Connecting to EvidentSource”Create src/index.ts:
import { EvidentSource, databaseName, stateChangeName, stateViewName, jsonCommandRequest, stateViewContentAsJson,} from "@evidentsource/client";
interface AccountSummary { id: string; name: string; balance: number; isOpen: boolean;}
async function main() { // Connect to the server const es = EvidentSource.connect("http://localhost:50051");Authentication
Section titled “Authentication”For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.
import { EvidentSource, Credentials } from "@evidentsource/client";
async function main() { // Bearer token authentication (production) const token = process.env.EVS_TOKEN!; const es = EvidentSource.connect( "api.example.com:50051", Credentials.bearerToken(token) );
// DevMode for local development (no auth required) const es = EvidentSource.connect("http://localhost:50051");
try { // Connect to a database const dbName = databaseName("banking"); const conn = await es.connectDatabase(dbName);
console.log(`Connected at revision ${conn.revision()}`);
// Execute a state change to open an account const db = await conn.executeStateChange( stateChangeName("open-account"), 1, jsonCommandRequest({ accountId: "acct-001", customerName: "Alice Smith", initialDeposit: 1000.0, }) );
console.log(`New revision: ${db.revision()}`);
// Query the account summary const view = await db.viewState( stateViewName("account-summary"), 1, { account_id: "acct-001" } );
if (view) { const account = stateViewContentAsJson<AccountSummary>(view); console.log(`Account: ${account.name}`); console.log(`Balance: $${account.balance.toFixed(2)}`); }
// Clean up await conn.close(); } finally { es.close(); }}
main().catch(console.error);Working with Events
Section titled “Working with Events”Querying Events
Section titled “Querying Events”import { EventSelector, streamName, eventType } from "@evidentsource/core";
// Query events by streamconst events = await db.queryEvents({ selector: EventSelector.streamEquals(streamName("accounts")), limit: 100,});
for (const event of events) { console.log(`${event.type}: ${JSON.stringify(event.data)}`);}Composing Selectors
Section titled “Composing Selectors”// AND combinationconst selector = EventSelector.and( EventSelector.subjectEquals(subject("acct-001")), EventSelector.typeStartsWith(eventType("com.banking.")));
// OR combinationconst selector = EventSelector.or( EventSelector.typeEquals(eventType("com.banking.account.credited")), EventSelector.typeEquals(eventType("com.banking.account.debited")));Executing State Changes
Section titled “Executing State Changes”State changes are server-side WASM components that handle commands:
// Execute with JSON commandconst db = await conn.executeStateChange( stateChangeName("deposit"), 1, jsonCommandRequest({ accountId: "acct-001", amount: 500.0, description: "Paycheck deposit", }));
// Handle errorstry { await conn.executeStateChange( stateChangeName("withdraw"), 1, jsonCommandRequest({ accountId: "acct-001", amount: 10000.0, // More than balance }) );} catch (e) { if (e instanceof StateChangeError) { console.error(`Validation error: ${e.message}`); }}Querying State Views
Section titled “Querying State Views”State views are materialized read models:
// Query with parametersconst view = await db.viewState( stateViewName("account-summary"), 1, { account_id: "acct-001" });
// Parse as typed JSONinterface AccountSummary { id: string; name: string; balance: number;}
const account = stateViewContentAsJson<AccountSummary>(view);Bi-Temporal Queries
Section titled “Bi-Temporal Queries”Query historical state at a specific revision:
// Get database at a specific revisionconst historicalDb = await conn.atRevision(500);
// Query state as it was at that revisionconst pastView = await historicalDb.viewState( stateViewName("account-summary"), 1, { account_id: "acct-001" });Building a REST API
Section titled “Building a REST API”Create an HTTP API using the SDK:
import { Hono } from "hono";import { EvidentSource, databaseName, stateChangeName, stateViewName, jsonCommandRequest, stateViewContentAsJson } from "@evidentsource/client";
const app = new Hono();const es = EvidentSource.connect("http://localhost:50051");const dbName = databaseName("banking");
// Get accountapp.get("/accounts/:id", async (c) => { const conn = await es.connectDatabase(dbName); try { const db = await conn.latestDatabase(); const view = await db.viewState( stateViewName("account-summary"), 1, { account_id: c.req.param("id") } );
if (!view) { return c.json({ error: "Account not found" }, 404); }
return c.json(stateViewContentAsJson(view)); } finally { await conn.close(); }});
// Open accountapp.post("/accounts", async (c) => { const body = await c.req.json(); const conn = await es.connectDatabase(dbName);
try { const db = await conn.executeStateChange( stateChangeName("open-account"), 1, jsonCommandRequest(body) );
return c.json({ revision: db.revision() }, 201); } catch (e) { return c.json({ error: e.message }, 400); } finally { await conn.close(); }});
// Depositapp.post("/accounts/:id/deposit", async (c) => { const body = await c.req.json(); const conn = await es.connectDatabase(dbName);
try { const db = await conn.executeStateChange( stateChangeName("deposit"), 1, jsonCommandRequest({ accountId: c.req.param("id"), ...body, }) );
return c.json({ revision: db.revision() }); } catch (e) { return c.json({ error: e.message }, 400); } finally { await conn.close(); }});
export default app;Error Handling
Section titled “Error Handling”import { DatabaseError, StateChangeError } from "@evidentsource/client";
try { await conn.executeStateChange(/* ... */);} catch (e) { if (e instanceof StateChangeError) { switch (e.code) { case "VALIDATION": console.error("Invalid input:", e.message); break; case "CONFLICT": console.error("Conflict:", e.message); break; case "NOT_FOUND": console.error("Resource not found:", e.message); break; default: console.error("State change error:", e.message); } } else if (e instanceof DatabaseError) { console.error("Database error:", e.message); } else { throw e; }}Next Steps
Section titled “Next Steps”- Explore the TodoMVC example for a complete web application
- Learn about bi-temporal queries for historical analysis
- Read about DCB constraints used by state changes