Building Your First Event-Sourced App
Let's build a real todo app from scratch using DeltaBase
Time to build something real. We’re going to create a todo app that actually does event sourcing properly. No fake demos with fake data. A real app you could ship.
What we’re building
Section titled “What we’re building”By the end of this tutorial, you’ll have:
- A working todo app with proper event sourcing
- Real-time updates when todos change
- Complete audit trail of every action
- Understanding of DeltaBase core concepts
Should take about 20 minutes. Let’s go.
Step 1: Set up your project
Section titled “Step 1: Set up your project”Create a new directory and initialize it:
mkdir deltabase-todocd deltabase-todonpm init -y
Install DeltaBase and a web framework:
npm install @delta-base/server express corsnpm install -D @types/node @types/express @types/cors tsx nodemon
Create your package.json
scripts:
{ "scripts": { "dev": "nodemon --exec tsx src/server.ts", "build": "tsc", "start": "node dist/server.js" }}
Step 2: Connect to DeltaBase
Section titled “Step 2: Connect to DeltaBase”Create src/deltabase.ts
:
import { DeltaBase } from '@delta-base/server';
export const deltabase = new DeltaBase({ apiKey: process.env.DELTABASE_API_KEY || 'your-api-key-here', baseUrl: process.env.DELTABASE_URL || 'https://api.delta-base.com'});
export const eventStore = deltabase.getEventStore('todos');export const eventBus = deltabase.getEventBus('todos');
Step 3: Define your events
Section titled “Step 3: Define your events”Events are facts. Things that happened. Create src/events.ts
:
// Todo events - notice they're past tenseexport type TodoCreated = { type: 'todo.created'; data: { id: string; title: string; description?: string; };};
export type TodoCompleted = { type: 'todo.completed'; data: { id: string; completedAt: string; };};
export type TodoUncompleted = { type: 'todo.uncompleted'; data: { id: string; };};
export type TodoUpdated = { type: 'todo.updated'; data: { id: string; title?: string; description?: string; };};
export type TodoEvent = TodoCreated | TodoCompleted | TodoUncompleted | TodoUpdated;
Step 4: Build state from events
Section titled “Step 4: Build state from events”Event sourcing means building current state by replaying events. Create src/todo.ts
:
import { TodoEvent } from './events';
export interface Todo { id: string; title: string; description?: string; completed: boolean; createdAt: string; completedAt?: string;}
export function buildTodoFromEvents(events: TodoEvent[]): Todo | null { if (events.length === 0) return null;
// Start with creation event const createEvent = events.find(e => e.type === 'todo.created'); if (!createEvent) return null;
let todo: Todo = { id: createEvent.data.id, title: createEvent.data.title, description: createEvent.data.description, completed: false, createdAt: new Date().toISOString() // You'd get this from event metadata };
// Apply all subsequent events for (const event of events) { switch (event.type) { case 'todo.completed': todo.completed = true; todo.completedAt = event.data.completedAt; break; case 'todo.uncompleted': todo.completed = false; todo.completedAt = undefined; break; case 'todo.updated': if (event.data.title) todo.title = event.data.title; if (event.data.description !== undefined) { todo.description = event.data.description; } break; } }
return todo;}
Step 5: Create your API
Section titled “Step 5: Create your API”Now let’s build an API that speaks events. Create src/server.ts
:
import express from 'express';import cors from 'cors';import { eventStore } from './deltabase';import { TodoEvent } from './events';import { buildTodoFromEvents } from './todo';
const app = express();app.use(cors());app.use(express.json());
// Create a todoapp.post('/todos', async (req, res) => { const { title, description } = req.body; const id = `todo-${Date.now()}`;
const event: TodoEvent = { type: 'todo.created', data: { id, title, description } };
try { await eventStore.appendToStream(id, [event]); res.json({ id, message: 'Todo created' }); } catch (error) { res.status(500).json({ error: 'Failed to create todo' }); }});
// Get a specific todoapp.get('/todos/:id', async (req, res) => { try { const events = await eventStore.readStream<TodoEvent>(req.params.id); const todo = buildTodoFromEvents(events);
if (!todo) { return res.status(404).json({ error: 'Todo not found' }); }
res.json(todo); } catch (error) { res.status(500).json({ error: 'Failed to get todo' }); }});
// Complete a todoapp.post('/todos/:id/complete', async (req, res) => { const event: TodoEvent = { type: 'todo.completed', data: { id: req.params.id, completedAt: new Date().toISOString() } };
try { await eventStore.appendToStream(req.params.id, [event]); res.json({ message: 'Todo completed' }); } catch (error) { res.status(500).json({ error: 'Failed to complete todo' }); }});
// Uncomplete a todoapp.post('/todos/:id/uncomplete', async (req, res) => { const event: TodoEvent = { type: 'todo.uncompleted', data: { id: req.params.id } };
try { await eventStore.appendToStream(req.params.id, [event]); res.json({ message: 'Todo uncompleted' }); } catch (error) { res.status(500).json({ error: 'Failed to uncomplete todo' }); }});
// Update a todoapp.patch('/todos/:id', async (req, res) => { const { title, description } = req.body;
const event: TodoEvent = { type: 'todo.updated', data: { id: req.params.id, ...(title && { title }), ...(description !== undefined && { description }) } };
try { await eventStore.appendToStream(req.params.id, [event]); res.json({ message: 'Todo updated' }); } catch (error) { res.status(500).json({ error: 'Failed to update todo' }); }});
// List all todosapp.get('/todos', async (req, res) => { try { const streams = await eventStore.listStreams({ limit: 100 }); const todos = [];
for (const streamId of streams) { const events = await eventStore.readStream<TodoEvent>(streamId); const todo = buildTodoFromEvents(events); if (todo) todos.push(todo); }
res.json(todos); } catch (error) { res.status(500).json({ error: 'Failed to list todos' }); }});
const PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Todo API running on port ${PORT}`);});
Step 6: Test your app
Section titled “Step 6: Test your app”Start your server:
npm run dev
Test it with curl:
# Create a todocurl -X POST http://localhost:3000/todos \ -H "Content-Type: application/json" \ -d '{"title": "Learn DeltaBase", "description": "Build a real app"}'
# Get the todocurl http://localhost:3000/todos/todo-1640995200000
# Complete itcurl -X POST http://localhost:3000/todos/todo-1640995200000/complete
# List all todoscurl http://localhost:3000/todos
What you just built
Section titled “What you just built”Actually pretty impressive:
- Event-sourced architecture - Every change is an event
- Complete audit trail - You can see every action ever taken
- Time travel - Replay events to any point in time
- Immutable data - Events never change, only accumulate
- Conflict resolution - Built-in optimistic concurrency control
What’s next?
Section titled “What’s next?”Your todo app works, but you can make it better:
- Add real-time updates with WebSockets
- Build read models for faster queries
- Add user authentication and multi-tenancy
- Create a web frontend
- Add more complex business logic
The beauty of event sourcing is that you can add these features without breaking existing functionality. Your events are your single source of truth.
Ready to ship something real? You just learned the foundations.