Claude-skill-registry hono-ipc-patterns

Advanced patterns for Hono-based Electron IPC including CQRS, Zod validation, error handling, and reactive data with RxJS. Use when implementing complex IPC patterns or needing guidance on architecture decisions.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hono-ipc-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-hono-ipc-patterns && rm -rf "$T"
manifest: skills/data/hono-ipc-patterns/SKILL.md
source content

Hono IPC Patterns

This skill provides advanced patterns for building robust IPC communication in Electron applications using Hono.

When This Skill Applies

  • Implementing CQRS (Command Query Responsibility Segregation) pattern
  • Adding Zod validation to routes
  • Handling errors with Result types (neverthrow)
  • Implementing reactive data streams with RxJS
  • Testing IPC routes

CQRS Pattern

Core Principles

AspectQueryCommand
PurposeRead dataModify state
Return Type
Observable<T>
ResultAsync<void, Error>
Side EffectsNoneDatabase/API writes
CachingYes (reactive updates)No

Service Interface Example

// src/shared/services/event.service.ts
import type { Observable } from 'rxjs';
import type { ResultAsync } from 'neverthrow';

export interface EventService {
  // === QUERIES (return Observable) ===

  // Get active event (reactive - auto-updates on changes)
  active(): Observable<EventInstance | undefined>;

  // Get event history (reactive)
  histories(includeHidden?: boolean): Observable<EventInstance[]>;

  // Get single event
  get(id: number): Observable<EventInstance | undefined>;

  // === COMMANDS (return ResultAsync) ===

  // Create event (one-shot operation)
  create(data: CreateEventData): ResultAsync<void, ApplicationError>;

  // Update event
  update(id: number, data: UpdateEventData): ResultAsync<void, ApplicationError>;

  // Delete event
  delete(id: number): ResultAsync<void, ApplicationError>;

  // Perform action on event
  invite(userId: string, location: Location): ResultAsync<void, ApplicationError>;
}

Route Handler Pattern

// For Queries - convert Observable to single value
.get('/', async (c) => {
  const result = await firstValueFrom(c.var.services.events.histories());
  return c.json(result, 200);
})

// For Commands - use Result pattern matching
.post('/', zValidator('json', CreateEventBody), async (c) => {
  const body = c.req.valid('json');

  const result = await c.var.services.events.create(body);

  return result.match(
    () => c.json({ success: true }, 201),
    (error) => c.json({ error: error.message }, error.statusCode ?? 500)
  );
})

See CQRS.md for complete CQRS documentation.

Zod Validation Patterns

Request Validation Layers

import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

// Path parameter validation
const IdParam = z.object({
  id: z.string().regex(/^[a-z]{3}_[a-zA-Z0-9]+$/),
});

// Query parameter validation (with coercion)
const PaginationQuery = z.object({
  limit: z.coerce.number().int().positive().max(100).default(10),
  offset: z.coerce.number().int().nonnegative().default(0),
});

// JSON body validation
const CreateBody = z.object({
  name: z.string().min(1).max(100),
  description: z.string().max(1000).optional(),
});

// Combined usage
.get('/:id', zValidator('param', IdParam), (c) => { ... })
.get('/', zValidator('query', PaginationQuery), (c) => { ... })
.post('/', zValidator('json', CreateBody), (c) => { ... })

See ZOD-VALIDATION.md for complete validation documentation.

Error Handling Patterns

Centralized Error Handler

const factory = (deps: Dependencies) =>
  createFactory<HonoEnv>({
    initApp(app) {
      // Global error handler
      app.onError((err, c) => {
        // Log error
        deps.logger?.error('Request error:', err);

        // Handle known error types
        if (err instanceof HTTPException) {
          return c.json({ error: err.message }, err.status);
        }

        if (err instanceof ValidationError) {
          return c.json({ error: err.message, details: err.issues }, 400);
        }

        // Unknown errors
        return c.json({ error: 'Internal Server Error' }, 500);
      });
    },
  });

Result Type Pattern Matching

.post('/action', async (c) => {
  const result = await c.var.services.action.perform();

  return result.match(
    (data) => c.json(data, 200),
    (error) => {
      // Type-safe error handling
      switch (error.code) {
        case 'NOT_FOUND':
          return c.json({ error: error.message }, 404);
        case 'UNAUTHORIZED':
          return c.json({ error: error.message }, 401);
        case 'VALIDATION_ERROR':
          return c.json({ error: error.message, details: error.details }, 400);
        default:
          return c.json({ error: 'Operation failed' }, 500);
      }
    }
  );
})

Reactive Data Patterns

BehaviorSubject for Query Updates

// Service implementation pattern
export class UserServiceImpl implements UserService {
  #notify = new BehaviorSubject(Date.now());

  // Query: Returns Observable that updates on changes
  list(): Observable<User[]> {
    return concat(
      // Initial data fetch
      from(this.#fetchUsers()),
      // Re-fetch when notified
      this.#notify.pipe(
        distinctUntilChanged(),
        mergeMap(() => this.#fetchUsers())
      )
    );
  }

  // Command: Modifies data and notifies subscribers
  async create(data: CreateUserData): ResultAsync<void, Error> {
    const result = await this.#createUser(data);

    if (result.isOk()) {
      // Notify all query subscribers to refresh
      this.#notify.next(Date.now());
    }

    return result;
  }
}

Helper for Observable to Value

// utils/observable.ts
import { firstValueFrom, Observable } from 'rxjs';

export const firstValueFromResult = <T>(
  observable: Observable<T>
): Promise<T> => {
  return firstValueFrom(observable);
};

// Usage in routes
.get('/', async (c) => {
  const users = await firstValueFromResult(c.var.services.users.list());
  return c.json(users, 200);
})

Testing Patterns

Route Unit Testing

import { Hono } from 'hono';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { of } from 'rxjs';
import { ok, err } from 'neverthrow';

describe('Events Route', () => {
  let app: Hono<HonoEnv>;
  let mockEventService: Partial<EventService>;

  beforeEach(() => {
    mockEventService = {
      list: vi.fn(),
      create: vi.fn(),
    };

    app = new Hono<HonoEnv>();
    app.use((c, next) => {
      c.set('services', { events: mockEventService } as any);
      return next();
    });
    app.route('/events', routes);
  });

  it('should list events', async () => {
    mockEventService.list.mockReturnValue(of([{ id: 1, name: 'Event' }]));

    const res = await app.request('/events');
    const data = await res.json();

    expect(res.status).toBe(200);
    expect(data).toHaveLength(1);
  });

  it('should handle create error', async () => {
    mockEventService.create.mockReturnValue(
      err({ code: 'VALIDATION_ERROR', message: 'Invalid data' })
    );

    const res = await app.request('/events', {
      method: 'POST',
      body: JSON.stringify({ name: '' }),
      headers: { 'Content-Type': 'application/json' },
    });

    expect(res.status).toBe(400);
  });
});

Files Reference