Documentation Index
Fetch the complete documentation index at: https://mintlify.com/wtyler2505/ProtoPulse/llms.txt
Use this file to discover all available pages before exploring further.
ProtoPulse is a TypeScript-first codebase with strict mode enabled across all layers — client, server, and shared. The conventions below are not preferences; most are enforced by ESLint, the TypeScript compiler, or the test suite. A contribution that violates any of them will fail CI.
Frontend
Backend
Database
TypeScript and imports
strict: true is set in tsconfig.json — noImplicitAny, strict null checks, and strict function types are all active.
- All type-only imports must use
import type. This is enforced by the eslint-plugin-import-x rule import/consistent-type-specifier-style.
// correct
import type { BomItem } from '@shared/schema';
import { useState } from 'react';
// incorrect — will fail lint
import { BomItem } from '@shared/schema';
- Public API functions (exported hooks, context values, utility functions) must have explicit return types.
as any is an ESLint error. Use type narrowing, discriminated unions, or generics instead.
State management
All server state is managed by TanStack React Query (@tanstack/react-query). There is no Redux, no Zustand, and no useState for remote data.// correct — server state via React Query
const { data: bomItems } = useQuery({
queryKey: ['bom', projectId],
queryFn: () => apiRequest('GET', `/api/projects/${projectId}/bom`),
});
// incorrect — server state in local state
const [bomItems, setBomItems] = useState([]);
Use Wouter for client-side routing. Do not add React Router.Component conventions
- Every interactive element must have a
data-testid attribute.
<button data-testid="export-gerber-btn" onClick={handleExport}>
Export Gerber
</button>
- Wrap every top-level view component in an
ErrorBoundary. ProjectWorkspace renders each view lazily inside its own boundary so that one crashing view does not take down the whole app.
- Use shadcn/ui primitives from
client/src/components/ui/ for all UI building blocks. Do not reach for a new UI library.
- Use Tailwind CSS v4 utility classes for all styling. No inline
style props except for dynamic values that cannot be expressed as utilities (e.g., canvas coordinates).
Naming
| Thing | Convention | Example |
|---|
| Files | kebab-case | bom-diff-panel.tsx |
| React components | PascalCase | BomDiffPanel |
| Functions and variables | camelCase | handleExport, bomItems |
| CSS classes | Tailwind utilities | flex items-center gap-2 |
data-testid | kebab-case | data-testid="bom-export-btn" |
Path aliases
| Alias | Resolves to |
|---|
@/* | client/src/* |
@shared/* | shared/* |
Use these aliases for all cross-directory imports. Never use ../../.. relative paths to cross the client/shared boundary.TypeScript and imports
The same tsconfig.json covers server code. All import type and strict mode rules apply equally on the backend.API route conventions
Every route handler must:
- Be wrapped in
asyncHandler from server/routes/utils.ts so that thrown errors propagate to the global error handler.
- Validate the request body with a Zod schema from
shared/schema.ts before touching storage.
- Return a consistent error shape on validation failure.
import { asyncHandler, HttpError } from '../routes/utils';
import { insertBomItemSchema } from '@shared/schema';
router.post(
'/api/projects/:id/bom',
asyncHandler(async (req, res) => {
const result = insertBomItemSchema.safeParse(req.body);
if (!result.success) {
throw new HttpError(result.error.message, 400);
}
const item = await storage.createBomItem(req.params.id, result.data);
res.status(201).json(item);
}),
);
Use drizzle-zod to derive Zod validators from Drizzle table definitions — do not hand-write validators that duplicate the schema.Response envelopes
| Operation | Status | Body |
|---|
GET list | 200 | { data: T[], total: number } |
POST create | 201 | Created object |
PATCH / PUT | 200 | Updated object |
DELETE | 204 | Empty |
Error responses always have the shape { message: string }. Never expose raw stack traces — the global error handler sanitizes messages in production.Storage layer
All data access goes through IStorage (server/storage.ts). Do not import the Drizzle db object directly in a route file.// correct
const items = await storage.getBomItems(projectId);
// incorrect — bypasses cache and interface contract
const items = await db.select().from(bomItems).where(...);
DatabaseStorage sits behind an LRU cache with prefix-based invalidation. Mutations must call the appropriate invalidate* helpers so cached reads stay consistent.Import order (ESLint enforced)
- Node.js built-ins (
node:crypto, node:path)
- Framework and third-party packages
- Internal path aliases (
@shared/)
- Relative imports within the server tree
- Type-only imports (
import type)
Error handling
Throw HttpError for all expected failure conditions. The global handler maps HttpError.status to the HTTP response status code.throw new HttpError('Project not found', 404);
throw new HttpError('Validation failed', 400);
For unexpected errors, let them propagate unhandled — asyncHandler catches them and the global handler returns 500.Schema location
All 27 Drizzle ORM table definitions live in shared/schema.ts. This file is the single source of truth — it is imported by both server and client code. Do not define tables anywhere else.Naming
| Thing | Convention | Example |
|---|
| Table names | snake_case | bom_items, circuit_designs |
| Column names in Drizzle | camelCase | projectId, deletedAt |
| Column names in PostgreSQL | snake_case | project_id, deleted_at |
| Primary keys | id (serial) | — |
Drizzle’s .references() and column mapping handle the camelCase ↔ snake_case translation automatically.Soft deletes
The following tables use soft deletes via a deletedAt timestamp column:
projects
architecture_nodes
architecture_edges
bom_items
Never issue a DELETE statement against these tables. Set deletedAt = new Date() instead. All read queries must filter with isNull(deletedAt).Other tables (validation_issues, history_items, chat_messages, circuit tables) use hard deletes and have no deletedAt column.Primary keys
Use serial (auto-increment integer) primary keys for all tables. Use crypto.randomUUID() for client-generated IDs that need to be stable before the row is persisted (e.g., React Flow node IDs stored in node_id). Never use Date.now() as an ID.Zod validators
Use drizzle-zod to generate insert validators from Drizzle table definitions:import { createInsertSchema } from 'drizzle-zod';
import { bomItems } from './schema';
export const insertBomItemSchema = createInsertSchema(bomItems);
These validators are used by route handlers for request body validation. Keeping validators derived from the schema ensures they stay in sync when the schema changes.Migrations
Use drizzle-kit to manage schema changes:npm run db:push # Push schema directly (development)
npm run db:generate # Generate a migration file
npm run db:migrate # Apply pending migrations
In development, db:push is the fastest path. For production or team environments, generate and apply migration files.
Vertical slice pattern
Every feature — no matter how small — is implemented as a complete vertical slice across all layers. This is the only allowed development pattern:
shared/schema.ts ← 1. Add/modify Drizzle table + Zod schema
server/storage.ts ← 2. Add IStorage method + DatabaseStorage implementation
server/routes/ ← 3. Add route with asyncHandler + Zod validation
project-context.tsx ← 4. Add React Query hook
client/src/components/ ← 5. Build UI component with data-testid attributes
__tests__/ ← 6. Write test covering the new behavior
Do not implement step 5 before step 2. A UI component that calls a non-existent API route will always produce a runtime error.
File organization
shared/ # Code shared between client and server
schema.ts # All Drizzle tables + Zod validators (source of truth)
component-types.ts # Component editor type system
drc-engine.ts # Design rule check engine (runs on both sides)
bom-diff.ts # BOM snapshot comparison logic
server/
routes.ts # Barrel — registers all 21 domain routers
circuit-routes.ts # Barrel — registers all 13 circuit routers
routes/ # One file per domain (auth, projects, bom, ...)
circuit-routes/ # One file per circuit domain (designs, instances, nets, ...)
storage.ts # IStorage interface + DatabaseStorage
ai-tools/ # 11 AI tool modules (barrel: ai-tools.ts)
export/ # 16 export format modules
client/src/
components/
views/ # Top-level view components (one per workspace tab)
panels/ # Sidebar panels (ChatPanel, ExportPanel)
circuit-editor/ # Schematic, breadboard, PCB canvas components
lib/
project-context.tsx # ProjectProvider — all React Query hooks
circuit-editor/ # Wire router, ERC engine, view sync
simulation/ # SPICE generator, circuit solver
component-editor/ # Constraint solver, diff engine, snap engine
New barrel files for routes go in server/routes.ts or server/circuit-routes.ts. New shared utilities go in shared/. Do not add new top-level directories without discussion.
Testing conventions
- Tests live in
__tests__/ subdirectories co-located with the source they test.
- Vitest 4 is the test runner, configured as a workspace with separate
server (Node.js environment) and client (happy-dom environment) projects.
- Use
@testing-library/react for all component tests — assert on what the user sees, not on internal state.
- Name tests descriptively:
"should display error message when BOM item creation fails".
- The v8 coverage provider is available via
npm run test:coverage.
// good test name
it('should show a toast error when the API returns 400', async () => { ... });
// poor test name
it('handles error', async () => { ... });
Prettier configuration
Prettier is enforced project-wide. The .prettierrc settings are:
| Option | Value |
|---|
semi | true |
singleQuote | true |
tabWidth | 2 |
trailingComma | "all" |
printWidth | 120 |
bracketSpacing | true |
arrowParens | "always" |
Run npx prettier --write . before committing. The CI check will fail on unformatted files.
Related pages