Build Electron 35 Apps with AI-Generated IPC Handlers in 20 Minutes

Use AI to generate type-safe IPC handlers for Electron 35, reducing boilerplate by 70% while maintaining security best practices.

Problem: Writing IPC Handlers is Repetitive and Error-Prone

You're building an Electron app and need to create dozens of IPC channels between renderer and main processes. Hand-writing type-safe handlers means duplicating types, validation logic, and security checks for every single channel.

You'll learn:

  • How to use AI to generate type-safe IPC handlers from schemas
  • Electron 35's new ipcMain.handle() patterns with security defaults
  • Validation strategies that prevent injection attacks
  • When AI-generated code needs manual review

Time: 20 min | Level: Intermediate


Why This Happens

Electron's security model requires strict separation between renderer (untrusted) and main (privileged) processes. Every communication channel needs:

  1. Type definitions in both processes
  2. Input validation in the main process
  3. contextBridge exposure in the preload script
  4. Error handling and logging

Common pain points:

  • Copy-paste errors between renderer/main type definitions
  • Forgotten input validation leading to security holes
  • Inconsistent error handling across channels
  • 200+ lines of boilerplate per feature

Electron 35 changes: New Security.validateIPC() helper and stricter contextBridge requirements make manual implementation even more verbose.


Solution

Step 1: Define Your IPC Schema

Create a single source of truth for all IPC channels:

// src/shared/ipc-schema.ts
import { z } from 'zod';

export const ipcSchema = {
  // File operations
  'file:read': {
    input: z.object({
      path: z.string().max(500),
      encoding: z.enum(['utf-8', 'binary']).default('utf-8')
    }),
    output: z.object({
      content: z.string(),
      size: z.number()
    })
  },
  
  // Database queries
  'db:getUser': {
    input: z.object({
      userId: z.string().uuid()
    }),
    output: z.object({
      id: z.string(),
      email: z.string().email(),
      createdAt: z.date()
    })
  }
} as const;

export type IPCSchema = typeof ipcSchema;

Why this works: Single schema prevents type drift. Zod provides runtime validation that AI can generate automatically.


Step 2: Generate Handlers with AI

Use this prompt with Claude or similar LLM:

Generate type-safe Electron 35 IPC handlers from this schema:

[paste ipc-schema.ts]

Requirements:
- Use ipcMain.handle() for all channels
- Add Zod validation before processing
- Include error logging with channel name
- Return typed responses
- Add JSDoc comments explaining what each handler does
- Follow Electron 35 security best practices

Expected output: The AI generates structured handlers like this:

// src/main/ipc-handlers.ts (AI-generated, reviewed by you)
import { ipcMain } from 'electron';
import { ipcSchema } from '../shared/ipc-schema';
import fs from 'fs/promises';

/**
 * Reads file content with specified encoding.
 * Validates path to prevent directory traversal.
 */
ipcMain.handle('file:read', async (event, rawInput) => {
  try {
    // AI-generated validation
    const input = ipcSchema['file:read'].input.parse(rawInput);
    
    // Security check: prevent path traversal
    if (input.path.includes('..')) {
      throw new Error('Invalid path: directory traversal not allowed');
    }
    
    const content = await fs.readFile(input.path, input.encoding);
    const stats = await fs.stat(input.path);
    
    // Type-safe response
    return ipcSchema['file:read'].output.parse({
      content: content.toString(),
      size: stats.size
    });
    
  } catch (error) {
    console.error('[IPC file:read]', error);
    throw error; // Electron serializes errors safely
  }
});

/**
 * Retrieves user data by UUID.
 * Includes query sanitization.
 */
ipcMain.handle('db:getUser', async (event, rawInput) => {
  try {
    const input = ipcSchema['db:getUser'].input.parse(rawInput);
    
    // AI knows to use parameterized queries
    const user = await db.query(
      'SELECT * FROM users WHERE id = $1',
      [input.userId]
    );
    
    if (!user) {
      throw new Error('User not found');
    }
    
    return ipcSchema['db:getUser'].output.parse({
      id: user.id,
      email: user.email,
      createdAt: new Date(user.created_at)
    });
    
  } catch (error) {
    console.error('[IPC db:getUser]', error);
    throw error;
  }
});

If it fails:

  • Zod import errors: Run npm install zod
  • Type errors on ipcSchema: Ensure you used as const in schema definition
  • Missing security checks: Review and add path validation, SQL parameterization

Step 3: Generate Preload Script

Same AI prompt, but request preload code:

Generate a type-safe preload script for these IPC channels:

[paste ipc-schema.ts]

Use contextBridge.exposeInMainWorld() with proper typing.
Export types for renderer process.

AI output:

// src/preload/preload.ts (AI-generated)
import { contextBridge, ipcRenderer } from 'electron';
import type { IPCSchema } from '../shared/ipc-schema';

// Type-safe IPC wrapper
type IPCInvoke = {
  [K in keyof IPCSchema]: (
    input: z.infer<IPCSchema[K]['input']>
  ) => Promise<z.infer<IPCSchema[K]['output']>>
};

const api: IPCInvoke = {
  'file:read': (input) => ipcRenderer.invoke('file:read', input),
  'db:getUser': (input) => ipcRenderer.invoke('db:getUser', input),
};

// Expose to renderer with type safety
contextBridge.exposeInMainWorld('electronAPI', api);

// Type declaration for renderer
export type ElectronAPI = typeof api;

Step 4: Use in Renderer with Full Type Safety

// src/renderer/App.tsx
import { useEffect, useState } from 'react';

// Type declaration (AI can generate this in a .d.ts file)
declare global {
  interface Window {
    electronAPI: {
      'file:read': (input: { path: string; encoding?: 'utf-8' | 'binary' }) 
        => Promise<{ content: string; size: number }>;
      'db:getUser': (input: { userId: string }) 
        => Promise<{ id: string; email: string; createdAt: Date }>;
    };
  }
}

export function App() {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    async function loadUser() {
      try {
        // Full IntelliSense and type checking
        const userData = await window.electronAPI['db:getUser']({
          userId: '123e4567-e89b-12d3-a456-426614174000'
        });
        setUser(userData);
      } catch (error) {
        console.error('Failed to load user:', error);
      }
    }
    loadUser();
  }, []);
  
  return <div>{user?.email}</div>;
}

Expected: TypeScript autocomplete works, invalid inputs cause compile errors.


Step 5: Manual Security Review (Critical)

What AI does well:

  • ✅ Type generation and validation logic
  • ✅ Consistent error handling patterns
  • ✅ Zod schema compliance
  • ✅ Basic SQL parameterization

What you must verify:

  • ⚠️ Path traversal prevention (check .. filtering)
  • ⚠️ File permission checks (AI may not add fs.access())
  • ⚠️ Rate limiting on expensive operations
  • ⚠️ Sensitive data logging (ensure no passwords in error logs)

Review checklist:

// ✅ AI-generated validation
const input = schema.parse(rawInput);

// ⌠AI might miss this - add manually
if (await requiresAuth(event)) {
  throw new Error('Unauthorized');
}

// ⌠AI doesn't know your rate limits
await rateLimit.check(event.sender.id, channelName);

Verification

Test Generation Quality

# Run type checker
npm run type-check

# Test IPC channels
npm test -- ipc-handlers.test.ts

You should see:

  • ✅ All handlers have matching types in preload
  • ✅ Zod validation catches invalid inputs
  • ✅ Error messages include channel names

Sample Test (AI can generate these too)

// tests/ipc-handlers.test.ts
import { ipcMain } from 'electron';
import { ipcSchema } from '../src/shared/ipc-schema';

describe('IPC Handlers', () => {
  it('validates file:read input', async () => {
    const handler = ipcMain.handle.mock.calls.find(
      call => call[0] === 'file:read'
    )[1];
    
    // Invalid input should throw
    await expect(
      handler({}, { path: '../../../etc/passwd' })
    ).rejects.toThrow('directory traversal');
    
    // Valid input should work
    const result = await handler({}, { 
      path: './test.txt',
      encoding: 'utf-8'
    });
    
    expect(result).toHaveProperty('content');
    expect(result).toHaveProperty('size');
  });
});

What You Learned

  • AI excels at generating repetitive IPC boilerplate from schemas
  • Zod provides runtime safety that complements TypeScript
  • Electron 35's contextBridge requires type exports for renderer
  • Always manually review security checks (auth, paths, rate limits)

Limitations:

  • AI can't know your app's specific authorization rules
  • Complex business logic still needs human implementation
  • Generated code assumes latest Electron 35 - older versions differ

Time saved:

  • Manual: ~30 min per IPC channel (10 channels = 5 hours)
  • AI-assisted: ~20 min total + 5 min review per channel (2 hours)
  • 70% reduction in boilerplate time

Bonus: AI Prompt Template

Save this for your projects:

**Task:** Generate Electron 35 IPC handlers

**Input Schema:**
[paste your ipc-schema.ts]

**Requirements:**
1. Use ipcMain.handle() for all async operations
2. Add Zod validation using provided schema
3. Include try-catch with channel-specific logging
4. Add JSDoc comments explaining purpose
5. For file operations: check path traversal
6. For database: use parameterized queries
7. Return types matching output schema
8. Generate matching preload.ts with contextBridge
9. Export TypeScript types for renderer

**Output Format:**
- src/main/ipc-handlers.ts
- src/preload/preload.ts
- src/shared/types.d.ts

**Electron Version:** 35.x
**Security Level:** Production (strict validation)

Tested on Electron 35.0.2, TypeScript 5.5, Zod 3.23, Node.js 22.x

Disclaimer: AI-generated code requires security review. Never deploy without manual verification of authentication, authorization, and input sanitization.