Claude-skill-registry hono-ipc-routes

Implement type-safe IPC communication in Electron using Hono RPC. Use when creating API routes between main and renderer processes, setting up typed clients, or implementing request/response IPC patterns. Provides HTTP-like semantics over Electron IPC.

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-routes" ~/.claude/skills/majiayu000-claude-skill-registry-hono-ipc-routes && rm -rf "$T"
manifest: skills/data/hono-ipc-routes/SKILL.md
source content

Hono IPC Routes for Electron

Overview

This skill documents how to implement type-safe IPC communication using Hono in Electron applications. Hono provides HTTP-like semantics with full TypeScript type inference.

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                      RENDERER PROCESS                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                       │
│   const res = await client.users.$get()                              │
│   const data = await res.json()                                      │
│         │                                                             │
│         │ Type: { displayName: string, id: string }[]                │
│         ▼                                                             │
│   ┌─────────────────────────────────────────────────────────────┐   │
│   │  Hono Client (hc<CallableType>)                              │   │
│   │                                                               │   │
│   │  Custom fetch: ipcRenderer.invoke('hono-rpc-electron', ...)  │   │
│   └───────────────────────────────┬───────────────────────────────┘ │
│                                   │                                  │
└───────────────────────────────────┼──────────────────────────────────┘
                                    │ IPC
┌───────────────────────────────────┼──────────────────────────────────┐
│                      MAIN PROCESS │                                  │
├───────────────────────────────────┼──────────────────────────────────┤
│                                   │                                  │
│   ┌───────────────────────────────▼───────────────────────────────┐ │
│   │  ipcMain.handle('hono-rpc-electron', ...)                     │ │
│   │                                                                │ │
│   │  const res = await callable.request(url, { method, body })    │ │
│   │  return { ...res, data: await res.json() }                    │ │
│   └───────────────────────────────┬───────────────────────────────┘ │
│                                   │                                  │
│   ┌───────────────────────────────▼───────────────────────────────┐ │
│   │  Hono App (callable)                                          │ │
│   │                                                                │ │
│   │  app.route('/users', usersRoutes)                             │ │
│   │  app.route('/events', eventsRoutes)                           │ │
│   │  app.route('/notifications', notificationsRoutes)             │ │
│   └───────────────────────────────┬───────────────────────────────┘ │
│                                   │                                  │
│   ┌───────────────────────────────▼───────────────────────────────┐ │
│   │  Route Handler                                                 │ │
│   │                                                                │ │
│   │  .get('/', (c) => {                                           │ │
│   │    const users = await c.var.services.users.list()            │ │
│   │    return c.json(users, 200)                                  │ │
│   │  })                                                            │ │
│   └───────────────────────────────────────────────────────────────┘ │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Quick Start

1. Define Shared Routes

// src/shared/callable/index.ts
import { Hono } from 'hono';
import { users } from './users';
import { events } from './events';
import { notifications } from './notifications';

// App type for dependency injection
export type AppVariables = {
  services: {
    users: UserService;
    events: EventService;
    notifications: NotificationService;
  };
};

// Create app with typed variables
const app = new Hono<{ Variables: AppVariables }>()
  .route('/users', users.route)
  .route('/events', events.routes)
  .route('/notifications', notifications);

export type CallableType = typeof app;
export default app;

2. Implement Route Handlers

// src/shared/callable/users/index.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { firstValueFromResult } from '@utils/observable';
import type { AppVariables } from '../index';

const createUserSchema = z.object({
  displayName: z.string().min(1),
  bio: z.string().optional(),
});

export const route = new Hono<{ Variables: AppVariables }>()
  // GET /users - List all users
  .get('/', async (c) => {
    const result = await firstValueFromResult(
      c.var.services.users.list()
    );

    return result.match(
      (users) => c.json(users, 200),
      (error) => c.json({ error: error.message }, 500)
    );
  })

  // GET /users/:id - Get single user
  .get('/:id', async (c) => {
    const id = c.req.param('id');
    const result = await firstValueFromResult(
      c.var.services.users.get(id)
    );

    return result.match(
      (user) => user
        ? c.json(user, 200)
        : c.json({ error: 'Not found' }, 404),
      (error) => c.json({ error: error.message }, 500)
    );
  })

  // POST /users - Create user
  .post('/', zValidator('json', createUserSchema), async (c) => {
    const data = c.req.valid('json');
    const result = await c.var.services.users.create(data);

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

export const users = { route };

3. Initialize Main Process Handler

// src/main/callable/index.ts
import { Hono } from 'hono';
import routes from '@shared/callable';

export const createCallable = (services: AppVariables['services']) => {
  const app = new Hono<{ Variables: AppVariables }>()
    .use('*', async (c, next) => {
      // Inject services into context
      c.set('services', services);
      await next();
    })
    .route('/', routes);

  return app;
};

4. Register IPC Handler

// src/main/index.ts
import { ipcMain } from 'electron';
import { createCallable } from '@callable';

// Create callable with injected services
const callable = createCallable({
  users: userService,
  events: eventService,
  notifications: notificationService,
});

// Register IPC handler
ipcMain.handle('hono-rpc-electron', async (
  _,
  url: string,
  method?: string,
  headers?: [string, string][],
  body?: string | ArrayBuffer
) => {
  const res = await callable.request(url, { method, headers, body });
  const data = await res.json();

  if (res.status >= 400) {
    logService.write('error', 'api-error', {
      error: `Request failed: ${url} - ${res.status}`
    });
  }

  return { ...res, data };
});

5. Create Typed Client

// src/renderer/src/adapters/client.ts
import { hc } from 'hono/client';
import type { CallableType } from '@shared/callable';

// Serialize headers for IPC
const serializeHeader = (headers: HeadersInit | undefined): [string, string][] => {
  if (headers === undefined) return [];
  if (headers instanceof Headers) {
    return [...headers.entries()];
  }
  if (Array.isArray(headers)) return headers;
  return Object.entries(headers);
};

// Create typed client with custom IPC fetch
export const client = hc<CallableType>('http://internal.localhost', {
  async fetch(input, init) {
    const url = input.toString();
    const args = [
      url,
      init?.method,
      serializeHeader(init?.headers),
      init?.body
    ];

    const { data, ...rest } = await window.electron.ipcRenderer.invoke(
      'hono-rpc-electron',
      ...args
    );

    return Response.json(data, rest);
  },
});

Type Safety Flow

// Route definition infers response type
const route = new Hono()
  .get('/users', (c) => c.json([{ id: '1', name: 'John' }], 200));
//                              ▲ Return type: { id: string, name: string }[]

// Client inherits type
const res = await client.users.$get();
const users = await res.json();
//    ▲ TypeScript knows: { id: string, name: string }[]

// POST with typed body
await client.users.$post({
  json: { displayName: 'Jane', bio: 'Hello' }
//        ▲ TypeScript validates against zValidator schema
});

Additional Resources