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:
- Type definitions in both processes
- Input validation in the main process
- contextBridge exposure in the preload script
- 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 constin 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
contextBridgerequires 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.