# Build an Event-Driven Application with Bangstack

This tutorial shows you how to build a production-ready event-driven application using Bangstack's automatic event infrastructure generation.

## What You'll Build

A real-time task management application with:
- **Event Bus**: Redis pub/sub for distributed event handling
- **WebSocket Server**: Real-time updates pushed to connected clients
- **React Hooks**: Easy-to-use hooks for subscribing to events
- **Auto-generated Types**: TypeScript interfaces for all events
- **Event Handlers**: Subscriber pattern for business logic

## Prerequisites

- Node.js 18+ and npm
- Redis installed locally (or use Upstash for cloud Redis)
- Basic knowledge of React and TypeScript

## Step 1: Define Your Events

Events represent things that happen in your application. Bangstack uses convention-based event naming:

```yaml
# workflow.yaml
workflow:
  id: task-manager
  version: 1.0.0
  name: Task Manager with Events
  description: Real-time task management application

# ============================================================================
# TECHNOLOGY STACK
# ============================================================================
technology:
  frontend:
    framework: react
    language: typescript
    styling: tailwind
    state_management: context

  backend:
    framework: express
    language: typescript
    orm: prisma

  database:
    type: postgresql
    version: "15"

  # IMPORTANT: Redis is required for event-driven architecture
  cache:
    provider: redis      # or 'upstash-redis' for serverless
    use: local          # or 'cloud' for Upstash

# ============================================================================
# DATA MODELS
# ============================================================================
models:
  Task:
    description: A task in the system
    fields:
      title:
        type: string
        required: true
        max_length: 255
      status:
        type: enum
        enum_values: [todo, in_progress, done]
        default: todo
      assigned_to_id:
        type: uuid
        foreign_key: User
    timestamps: true

  User:
    description: Application user
    fields:
      email:
        type: string
        required: true
        unique: true
      name:
        type: string
        required: true
    timestamps: true

# ============================================================================
# EVENTS
# ============================================================================
events:
  # Convention: {model}_created, {model}_updated, {model}_deleted
  task_created:
    description: New task created
    payload:
      task_id: string
      title: string
      created_by_id: string

  task_updated:
    description: Task updated
    payload:
      task_id: string
      old_status: string
      new_status: string
      updated_by_id: string

  task_assigned:
    description: Task assigned to user
    payload:
      task_id: string
      assigned_to_id: string
      assigned_by_id: string

  user_created:
    description: New user registered
    payload:
      user_id: string
      email: string
      name: string
```

## Step 2: Generate Your Application

```bash
# Generate the application
bang generate workflow.yaml --output ./my-task-app

# Navigate to the generated app
cd my-task-app

# Install dependencies
npm install

# Start Redis (required for events)
redis-server

# Or use Docker
docker run -d -p 6379:6379 redis:7-alpine
```

## Step 3: Understanding Generated Event Infrastructure

Bangstack automatically generates:

### Backend Event Infrastructure

**1. Event Types** (`backend/src/types/events.ts`):
```typescript
// Auto-generated TypeScript interfaces
export interface TaskCreatedPayload {
  task_id: string;
  title: string;
  created_by_id: string;
}

export interface TaskCreatedEvent {
  type: 'task_created';
  payload: TaskCreatedPayload;
  timestamp: Date | string;
  correlationId?: string;
  userId?: string;
}

// Union type of all events
export type Event = TaskCreatedEvent | TaskUpdatedEvent | TaskAssignedEvent | UserCreatedEvent;

// Type-safe event payload map
export type EventPayloadMap = {
  'task_created': TaskCreatedPayload;
  'task_updated': TaskUpdatedPayload;
  'task_assigned': TaskAssignedPayload;
  'user_created': UserCreatedPayload;
};
```

**2. Event Bus** (`backend/src/events/event-bus.ts`):
```typescript
// Redis pub/sub event bus
const eventBus = getEventBus({
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
  },
});

// Publish events
await eventBus.publish('task_created', {
  task_id: task.id,
  title: task.title,
  created_by_id: user.id,
}, {
  correlationId: req.correlationId,
  userId: req.userId,
});
```

**3. Event Handlers** (`backend/src/events/handlers/`):
```typescript
// task_created-handler.ts
export async function handleTaskCreatedHandler(event: TaskCreatedEvent): Promise<void> {
  console.log('[TaskCreatedHandler] Processing event:', {
    type: event.type,
    correlationId: event.correlationId,
  });

  // TODO: Implement your business logic here
  // Examples:
  // - Send notification to assignee
  // - Update analytics
  // - Trigger workflow
  const payload = event.payload;

  // Your implementation here
}
```

**4. WebSocket Server** (`backend/src/websocket/server.ts`):
```typescript
// WebSocket server for real-time updates
const realtimeServer = initializeRealtimeServer(httpServer);

// Broadcasts events to connected clients
realtimeServer.broadcast('task_created', {
  task_id: 'uuid',
  title: 'New Task',
}, {
  correlationId: 'abc123',
});
```

### Frontend Event Infrastructure

**1. WebSocket Client** (`frontend/src/lib/websocket-client.ts`):
```typescript
// WebSocket client with reconnection
const client = new WebSocketClient('ws://localhost:3000/ws');
await client.connect(authToken);

// Subscribe to events
client.subscribe('task_created', (payload) => {
  console.log('New task:', payload);
});
```

**2. React Hooks** (`frontend/src/hooks/useEvents.ts`):
```typescript
// Easy-to-use React hooks
function TaskList() {
  const [tasks, setTasks] = useState([]);

  // Subscribe to task_created events
  useEvent('task_created', (payload) => {
    setTasks(prev => [...prev, payload]);
  });

  // Subscribe to task_updated events
  useEvent('task_updated', (payload) => {
    setTasks(prev => prev.map(t =>
      t.task_id === payload.task_id
        ? { ...t, status: payload.new_status }
        : t
    ));
  });

  return <div>{/* Render tasks */}</div>;
}
```

## Step 4: Automatic Event Emission

Bangstack automatically emits events from CRUD operations:

```typescript
// backend/src/routes/tasks.ts (auto-generated)
router.post('/tasks', async (req, res) => {
  const task = await prisma.task.create({
    data: req.body,
  });

  // Automatically emitted by Bangstack!
  await emitEventWithContext(req, 'task_created', {
    task_id: task.id,
    title: task.title,
    created_by_id: req.userId,
  });

  res.json(task);
});
```

**Convention-based events:**
- `POST /tasks` → emits `task_created`
- `PATCH /tasks/:id` → emits `task_updated`
- `DELETE /tasks/:id` → emits `task_deleted`

## Step 5: Implementing Custom Event Handlers

Add business logic to your event handlers:

```typescript
// backend/src/events/handlers/task_assigned-handler.ts
import { TaskAssignedEvent } from '../../types/events';
import { sendEmail } from '../../services/email';
import { prisma } from '../../database/client';

export async function handleTaskAssignedHandler(event: TaskAssignedEvent): Promise<void> {
  console.log('[TaskAssignedHandler] Processing event:', event.correlationId);

  try {
    const { task_id, assigned_to_id, assigned_by_id } = event.payload;

    // 1. Fetch user details
    const assignedUser = await prisma.user.findUnique({
      where: { id: assigned_to_id },
    });

    const task = await prisma.task.findUnique({
      where: { id: task_id },
    });

    // 2. Send notification email
    await sendEmail({
      to: assignedUser.email,
      subject: 'New Task Assigned',
      body: `You have been assigned: ${task.title}`,
    });

    // 3. Create in-app notification
    await prisma.notification.create({
      data: {
        user_id: assigned_to_id,
        type: 'task_assigned',
        message: `You have been assigned: ${task.title}`,
        metadata: { task_id, assigned_by_id },
      },
    });

    console.log('[TaskAssignedHandler] Notification sent successfully');
  } catch (error) {
    console.error('[TaskAssignedHandler] Error:', error);
    throw error; // Will be retried
  }
}
```

## Step 6: Using Events in Your React Frontend

### Setup WebSocket Provider

```typescript
// frontend/src/App.tsx
import { WebSocketProvider } from './lib/websocket-context';

function App() {
  const authToken = localStorage.getItem('auth_token');

  return (
    <WebSocketProvider
      url="ws://localhost:3000/ws"
      autoConnect={true}
      token={authToken}
    >
      <TaskManager />
    </WebSocketProvider>
  );
}
```

### Subscribe to Events

```typescript
// frontend/src/components/TaskList.tsx
import { useEvent, useEvents } from '../hooks/useEvents';
import { useState, useEffect } from 'react';

function TaskList() {
  const [tasks, setTasks] = useState([]);
  const [notifications, setNotifications] = useState([]);

  // Method 1: Subscribe to individual events
  useEvent('task_created', (payload) => {
    setTasks(prev => [...prev, {
      id: payload.task_id,
      title: payload.title,
      status: 'todo',
    }]);

    setNotifications(prev => [...prev, 'New task created!']);
  });

  useEvent('task_updated', (payload) => {
    setTasks(prev => prev.map(task =>
      task.id === payload.task_id
        ? { ...task, status: payload.new_status }
        : task
    ));
  });

  // Method 2: Subscribe to all events at once
  const events = useEvents();

  useEffect(() => {
    events.task_assigned((payload) => {
      // Handle task assignment
      console.log('Task assigned:', payload);
    });
  }, []);

  return (
    <div>
      {notifications.map((notif, i) => (
        <div key={i} className="notification">{notif}</div>
      ))}

      {tasks.map(task => (
        <TaskCard key={task.id} task={task} />
      ))}
    </div>
  );
}
```

### Real-time Collaboration

```typescript
// frontend/src/components/TaskBoard.tsx
function TaskBoard() {
  const [activeUsers, setActiveUsers] = useState<string[]>([]);

  // Show who else is viewing this task
  useEvent('task_viewed', (payload) => {
    setActiveUsers(prev => [...new Set([...prev, payload.user_id])]);
  });

  // Show real-time status changes
  useEvent('task_updated', (payload) => {
    toast.info(`Task moved to ${payload.new_status} by ${payload.updated_by_id}`);
  });

  return (
    <div>
      <div className="active-users">
        {activeUsers.length} users viewing
      </div>
      {/* Task board content */}
    </div>
  );
}
```

## Step 7: Testing Your Event-Driven App

### Test Event Emission

```typescript
// backend/src/test/integration/tasks.test.ts
import { getEventBus } from '../../events/event-bus';

describe('Task Events', () => {
  it('should emit task_created event when task is created', async () => {
    const eventBus = getEventBus();
    const events: any[] = [];

    // Subscribe to events
    await eventBus.subscribe('task_created', (event) => {
      events.push(event);
    });

    // Create task via API
    const response = await request(app)
      .post('/api/tasks')
      .send({ title: 'Test Task' })
      .expect(201);

    // Wait for event
    await new Promise(resolve => setTimeout(resolve, 100));

    // Verify event was emitted
    expect(events).toHaveLength(1);
    expect(events[0].type).toBe('task_created');
    expect(events[0].payload.title).toBe('Test Task');
  });
});
```

### Test Event Handlers

```typescript
// backend/src/events/handlers/task_assigned-handler.test.ts
import { handleTaskAssignedHandler } from './task_assigned-handler';
import { sendEmail } from '../../services/email';

jest.mock('../../services/email');

describe('TaskAssignedHandler', () => {
  it('should send notification email', async () => {
    const event = {
      type: 'task_assigned' as const,
      payload: {
        task_id: 'task-123',
        assigned_to_id: 'user-456',
        assigned_by_id: 'user-789',
      },
      timestamp: new Date().toISOString(),
    };

    await handleTaskAssignedHandler(event);

    expect(sendEmail).toHaveBeenCalledWith(
      expect.objectContaining({
        subject: 'New Task Assigned',
      })
    );
  });
});
```

## Step 8: Running Your Application

### Local Development

```bash
# Terminal 1: Start Redis
redis-server

# Terminal 2: Start backend
cd backend
npm run dev

# Terminal 3: Start frontend
cd frontend
npm run dev

# Visit http://localhost:5173
```

### Using Docker

```bash
# Start everything with docker-compose
docker-compose up

# Backend: http://localhost:3000
# Frontend: http://localhost:5173
# Redis: localhost:6379
```

## Best Practices

### 1. Event Naming Conventions

```yaml
# Good: Clear, descriptive names
events:
  task_created:          # ✓ Model + action
  user_password_reset:   # ✓ Specific action
  order_payment_failed:  # ✓ Specific failure

# Avoid: Vague names
events:
  task_event:           # ✗ Too generic
  update:               # ✗ What was updated?
  notification:         # ✗ What kind of notification?
```

### 2. Keep Event Payloads Minimal

```yaml
# Good: Only essential data
task_created:
  payload:
    task_id: string
    title: string
    created_by_id: string

# Avoid: Too much data
task_created:
  payload:
    task_id: string
    title: string
    description: string          # Can fetch if needed
    created_by_id: string
    created_by_name: string      # Redundant, can join
    created_by_email: string     # Redundant, can join
    all_comments: array          # Too large
```

### 3. Error Handling in Event Handlers

```typescript
export async function handleTaskCreatedHandler(event: TaskCreatedEvent): Promise<void> {
  try {
    // Your business logic
    await sendNotification(event.payload);

  } catch (error) {
    // Log error with correlation ID for debugging
    console.error('[TaskCreatedHandler] Error:', {
      correlationId: event.correlationId,
      error: error.message,
    });

    // Consider implementing:
    // - Retry logic with exponential backoff
    // - Dead letter queue for failed events
    // - Monitoring/alerting

    throw error; // Re-throw to mark as failed
  }
}
```

### 4. Use Correlation IDs

```typescript
// Backend: Events automatically include correlation IDs
await emitEventWithContext(req, 'task_created', payload);
// correlationId and userId are automatically attached

// This allows you to trace events across services
console.log('Processing event', event.correlationId);
```

### 5. Handle WebSocket Disconnections

```typescript
// Frontend: WebSocket client automatically reconnects
function TaskList() {
  const { isConnected } = useWebSocket();

  if (!isConnected) {
    return <div>Reconnecting to real-time updates...</div>;
  }

  useEvent('task_created', handleTaskCreated);

  return <TaskListContent />;
}
```

## Advanced Patterns

### Saga Pattern (Distributed Transactions)

```typescript
// Coordinate multi-step workflows across services
export async function handleOrderCreatedHandler(event: OrderCreatedEvent) {
  const { order_id } = event.payload;

  try {
    // Step 1: Reserve inventory
    await inventoryService.reserve(order_id);

    // Step 2: Process payment
    await paymentService.charge(order_id);

    // Step 3: Emit success event
    await emitEvent('order_completed', { order_id });

  } catch (error) {
    // Compensating transaction: Rollback
    await inventoryService.release(order_id);
    await emitEvent('order_failed', { order_id, reason: error.message });
  }
}
```

### Event Sourcing Pattern

```typescript
// Store all events as the source of truth
export async function handleTaskUpdatedHandler(event: TaskUpdatedEvent) {
  // 1. Store the event
  await prisma.taskEvent.create({
    data: {
      task_id: event.payload.task_id,
      event_type: event.type,
      event_data: event.payload,
      timestamp: event.timestamp,
    },
  });

  // 2. Update read model
  await prisma.task.update({
    where: { id: event.payload.task_id },
    data: { status: event.payload.new_status },
  });
}
```

## Troubleshooting

### Events Not Being Received

1. Check Redis connection:
```bash
redis-cli ping  # Should return PONG
```

2. Check event bus initialization:
```typescript
// backend/src/index.ts
console.log('[EventBus] Initialized:', eventBus.isConnected());
```

3. Check handler registration:
```typescript
// backend/src/events/subscriber-initializer.ts
console.log('[Subscribers] Registered:', getSubscriberConfigs().length);
```

### WebSocket Connection Issues

1. Check WebSocket server:
```bash
# Should show WebSocket upgrade
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" http://localhost:3000/ws
```

2. Check CORS configuration:
```typescript
// backend/src/index.ts
app.use(cors({
  origin: 'http://localhost:5173',  // Frontend URL
  credentials: true,
}));
```

## Next Steps

- Explore the [example-event-driven.workflow.yaml](../../examples/example-event-driven.workflow.yaml) for a complete example
- Read the [EXTENDED-SCHEMA-GUIDE.md](../EXTENDED-SCHEMA-GUIDE.md) for all event configuration options
- Check [Architecture Documentation](../ARCHITECTURE.md) for system design patterns
- See [Troubleshooting Guide](../TROUBLESHOOTING.md) for common issues

## Summary

You now have a production-ready event-driven application with:
- ✅ Type-safe event definitions
- ✅ Redis pub/sub event bus
- ✅ Automatic event emission from CRUD operations
- ✅ Event handler subscriber pattern
- ✅ Real-time WebSocket updates
- ✅ React hooks for easy event subscription
- ✅ Comprehensive error handling and reconnection
- ✅ Correlation IDs for tracing

The entire event infrastructure was generated automatically from your YAML workflow definition!
