Chapter 13
Cursor + Product Teams
Product teams live in the gap between vision and execution. Ideas are plentiful; validating them is expensive. Prototypes take weeks. Specs drift from implementation. Cursor collapses this gap.
ℹ️Bridging Vision and Execution
13.1 Writing User Stories Directly into Prompts
The Traditional Handoff Problem
The typical workflow creates multiple translation layers:
- PM writes user story in Jira
- Engineering interprets story
- Implementation doesn't match intent
- Back-and-forth clarification
- Rework and delay
⚠️The Disconnect
Cursor-Native User Stories
With Cursor, stories become executable specifications that generate implementation directly.
❌ Traditional story:
As a user, I want to filter products by price range
So that I can find items within my budget
✅ Cursor-enhanced story:
# User Story: Product Price Filter
## Acceptance Criteria
1. Display price range slider ($0-$10,000)
2. Filter updates in real-time (debounced 300ms)
3. Show count of matching products
4. Persist filter in URL query params
5. Clear filter button resets range
## Technical Requirements
- Component: @components/ProductFilters.jsx
- State: Use existing useProducts hook
- API: GET /api/products?minPrice={min}&maxPrice={max}
- UI: Follow @design/components/Slider.tsx
## Edge Cases
- Handle empty results gracefully
- Validate min <= max
- Round to 2 decimals
## Testing
- Unit: Component state management
- Integration: API filtering
- E2E: User can filter and share URLPrompt to engineering:
Implement this user story:
@specs/price-filter-story.md
Generate:
1. React component with price slider
2. Integration with useProducts hook
3. URL state management
4. Unit and integration tests✅Zero Interpretation Gap
Best Practices for Executable Stories
1. Include Technical Context
## Technical Context
- Existing components: @components/
- API documentation: @docs/api.md
- State management: Redux (@store/products.js)
- Design system: @design/components/This ensures generated code integrates seamlessly with existing architecture.
2. Define Success Explicitly
❌ Vague:
"Should be fast"
"Handle errors"
✅ Specific:
"API response < 200ms p95, UI updates < 16ms (60fps)"
"Display user-friendly error message, log to Sentry with context, allow retry"
3. Specify Edge Cases
## Edge Cases
- No products match → Display "No results found" with filter summary
- User types negative price → Validate and show inline error
- Slider at maximum → Include products at exact max price
- Network failure → Show cached results + retry buttonExplicit edge cases prevent AI from making incorrect assumptions.
13.2 Rapid Prototyping with Cursor
From Sketch to Working Demo in Minutes
Traditional approach: Designers create static mockups. Stakeholders see pictures, not interactions. Feedback comes late, after engineering starts.
With Cursor: PMs create interactive prototypes during discussions—validating ideas in real-time before committing engineering resources.
Example: Dashboard Prototype in 60 Seconds
Scenario: Your CEO wants a real-time analytics dashboard. You need to demo tomorrow morning.
Prompt:
Create an interactive dashboard prototype:
Layout:
- Header with logo and user menu
- 4 metric cards (Revenue, Users, Conversion, Churn)
- 2 charts (Revenue trend line, User growth bars)
Data:
- Mock data (last 30 days)
- Update every 5 seconds (simulate real-time)
- Animate transitions
Tech: React + Recharts, Tailwind CSS, responsive
Style: Modern, purple/blue theme, dark mode supportCursor generates (abbreviated):
export function AnalyticsDashboard() {
const [metrics, setMetrics] = useState({
revenue: '$142,500',
users: '8,234',
conversion: '3.2%',
churn: '1.8%'
});
const [chartData] = useState(generateMockData());
const [darkMode, setDarkMode] = useState(false);
// Simulate real-time updates
useEffect(() => {
const interval = setInterval(() => {
setMetrics(prev => ({
...prev,
revenue: formatCurrency(
parseCurrency(prev.revenue) + Math.random() * 1000
),
users: (
parseInt(prev.users.replace(/,/g, '')) +
Math.floor(Math.random() * 10)
).toLocaleString()
}));
}, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className={darkMode ? 'dark' : ''}>
<header className="bg-white dark:bg-gray-800 px-6 py-4
flex justify-between items-center">
<h1 className="text-2xl font-bold">Analytics Pro</h1>
<button
onClick={() => setDarkMode(!darkMode)}
className="text-2xl"
>
{darkMode ? '🌞' : '🌙'}
</button>
</header>
<div className="grid grid-cols-4 gap-6 p-6">
<MetricCard
title="Revenue"
value={metrics.revenue}
change={12.5}
/>
<MetricCard
title="Active Users"
value={metrics.users}
change={8.3}
/>
<MetricCard
title="Conversion"
value={metrics.conversion}
change={-2.1}
/>
<MetricCard
title="Churn Rate"
value={metrics.churn}
change={-5.4}
/>
</div>
<div className="grid grid-cols-2 gap-6 p-6">
<RevenueLineChart data={chartData} darkMode={darkMode} />
<UserGrowthBarChart data={chartData} darkMode={darkMode} />
</div>
</div>
);
}✅Ready in Minutes
Prototyping from Design Files
Modern workflow:
- Export Figma design as PNG
- Upload to Cursor
- Prompt:
"Convert this design to React component, match layout exactly, use Tailwind CSS"
Cursor analyzes the image and generates matching code—80-90% accurate, enough to validate interactions and gather feedback immediately.
When to Prototype vs. Build
✅ Use prototypes for:
- Early-stage concept validation
- Stakeholder alignment meetings
- User research sessions
- A/B test design exploration
- Technical feasibility assessment
❌ Don't use prototypes for:
- Production deployments
- Security-sensitive features
- Complex business logic
- Performance-critical paths
ℹ️The Golden Rule
13.3 Aligning Product Specs with AI-Assisted Dev
The Spec-Implementation Drift
Traditional gap:
- Product writes spec (intent)
- Engineering interprets (understanding)
- Code gets written (implementation)
- QA discovers gap between intent and implementation
- Rework cycle begins
Each iteration wastes days or weeks.
Spec-Driven Development
When specs are structured for AI, they become living contracts that generate and validate code.
Spec document (specs/email-notifications.md):
# Email Notifications System
## Notification Types
### Security (Always Sent)
- Password changed
- New login from unknown device
- Two-factor auth enabled/disabled
### Activity (User Configurable)
- New follower
- Comment on post
- Weekly digest
### Marketing (Opt-in Only)
- Product updates
- Promotional offers
## Business Rules
1. Security notifications are mandatory (cannot disable)
2. Activity notifications: max 10 emails/day (rate limiting)
3. Marketing requires explicit consent
4. All emails include unsubscribe link
5. Failed sends retry 3 times (exponential backoff)
## API Endpoints
### GET /api/notifications/preferences
Returns user's current notification preferences
### PUT /api/notifications/preferences
Updates preferences
Validation:
- Cannot disable security notifications (return 400)
- Marketing opt-in requires explicit consent field
## Implementation Requirements
- Use existing EmailService (@services/email.js)
- Store preferences in users.notification_settings (JSONB)
- Log all preference changes for audit
- Rate limiting tracked in RedisPrompt to engineering:
Implement Email Notifications System:
@specs/email-notifications.md
Generate:
1. API endpoints with validation
2. Service layer enforcing business rules
3. Tests verifying acceptance criteriaCursor generates implementation enforcing all rules:
// src/api/notifications.js
router.put('/preferences', async (req, res) => {
const updates = req.body;
// Rule: Cannot disable security notifications
if (updates.security?.enabled === false) {
return res.status(400).json({
error: 'Security notifications cannot be disabled'
});
}
// Rule: Marketing requires explicit consent
if (updates.marketing?.enabled && !req.body.marketingConsent) {
return res.status(400).json({
error: 'Marketing emails require explicit consent'
});
}
await NotificationService.updatePreferences(req.user.id, updates);
res.json({ success: true });
});
// src/services/NotificationService.js
async sendNotification(userId, type, data) {
// Rule: Activity rate limiting (10/day)
if (type.startsWith('activity.')) {
const todayCount = await this.getActivityEmailCount(userId);
if (todayCount >= 10) {
logger.warn('Activity email rate limit hit', { userId, type });
return { queued: false, reason: 'rate_limit' };
}
}
// Rule: Retry 3 times with exponential backoff
await EmailQueue.add({
userId,
type,
data,
retries: 3,
backoff: { type: 'exponential', delay: 1000 }
});
return { queued: true };
}Tests verify spec compliance:
test('rejects disabling security notifications', async () => {
const response = await request(app)
.put('/api/notifications/preferences')
.send({ security: { enabled: false } })
.expect(400);
expect(response.body.error).toContain('cannot be disabled');
});
test('enforces activity rate limiting', async () => {
// Send 10 activity emails
for (let i = 0; i < 10; i++) {
await NotificationService.sendNotification(
userId,
'activity.comment',
{}
);
}
// 11th should be rate limited
const result = await NotificationService.sendNotification(
userId,
'activity.comment',
{}
);
expect(result.queued).toBe(false);
expect(result.reason).toBe('rate_limit');
});
test('requires explicit consent for marketing emails', async () => {
const response = await request(app)
.put('/api/notifications/preferences')
.send({
marketing: { enabled: true }
// Missing marketingConsent
})
.expect(400);
expect(response.body.error).toContain('explicit consent');
});✅The Magic
Spec Templates for Common Features
Build reusable templates:
specs/templates/
├── api-endpoint.md # RESTful API specification
├── user-workflow.md # Multi-step user journey
├── data-model.md # Database schema and rules
├── integration.md # Third-party integration
└── background-job.md # Async processing taskExample template:
# API Endpoint Specification Template
## Endpoint
[METHOD] /api/[resource]/[action]
## Purpose
[One sentence describing what this endpoint does]
## Authentication
- [ ] Public (no auth required)
- [ ] Authenticated users only
- [ ] Specific roles: [list roles]
## Request
### Headers
- Content-Type: application/json
- Authorization: Bearer {token}
### Body Schema
```json
{
"field1": "string (required, max 100 chars)",
"field2": "number (optional, min 0)"
}
```
### Validation Rules
1. [Field-specific validation]
2. [Cross-field validation]
3. [Business rule validation]
## Response
### Success (200)
```json
{
"data": { ... },
"meta": { ... }
}
```
### Error Cases
* 400: Validation error
* 401: Unauthorized
* 403: Forbidden
* 404: Resource not found
* 429: Rate limit exceeded
## Business Rules
1. [Rule with enforcement point]
2. [Rule with edge case handling]
## Testing Requirements
* [ ] Happy path test
* [ ] All error cases covered
* [ ] Rate limiting verified
* [ ] Authorization checked
## Performance Requirements
* Response time: < [X]ms p95
* Throughput: > [Y] req/sec13.4 Measuring Product-Engineering Alignment
Key Alignment Metrics
Track these to ensure specs and implementation stay synchronized:
| Metric | Target | Measurement Method |
|---|---|---|
| Spec-to-Code Match | > 95% | Automated validation against spec |
| Rework Rate | < 10% | Stories requiring reimplementation |
| Clarification Requests | < 2 per story | Questions during implementation |
| Acceptance on First Review | > 80% | Stories accepted without changes |
| Time from Spec to Done | < 3 days | Cycle time tracking |
Automated Spec Validation
Create tests that verify implementation matches specification:
// tests/spec-validation/email-notifications.test.js
describe('Email Notifications Spec Compliance', () => {
test('security notifications cannot be disabled per spec', async () => {
// Reference: specs/email-notifications.md, Rule #1
const response = await updatePreferences({
security: { enabled: false }
});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/cannot be disabled/i);
});
test('activity emails rate limited to 10/day per spec', async () => {
// Reference: specs/email-notifications.md, Rule #2
const emailsSent = await sendActivityEmails(11);
expect(emailsSent).toBe(10);
expect(lastEmailResult.reason).toBe('rate_limit');
});
test('marketing requires explicit consent per spec', async () => {
// Reference: specs/email-notifications.md, Rule #3
const response = await updatePreferences({
marketing: { enabled: true }
// No marketingConsent field
});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/explicit consent/i);
});
});ℹ️Living Contract
13.5 Common Pitfalls for Product Teams
Pitfall 1: Over-Specifying Implementation
Problem: Specs become too technical, constraining engineering flexibility.
❌ Bad:
Use React useState hook with debounced callback wrapped in useCallback and memoized with useMemo. Store filter state in Redux using filterReducer action UPDATE_PRICE_FILTER.
✅ Good:
Filter updates should feel instant but not hammer the API. Debounce appropriately (300ms recommended). Persist in URL for shareability. Use existing state management patterns.
ℹ️The Rule
Pitfall 2: Vague Acceptance Criteria
Problem: AI generates plausible code that doesn't solve the actual problem.
❌ Bad:
Users should be able to search products easily. The search should be good.
✅ Good:
Search requirements:
- Text input with autocomplete (min 2 chars)
- Search across: name, description, SKU, category
- Results update as user types (debounced 300ms)
- Show top 5 matches with highlighting
- Display "No results" after 1 second of no matches
- Pressing Enter navigates to full results page
Pitfall 3: Ignoring Technical Constraints
Problem: Specs ignore system limitations, leading to impossible implementations.
Example: "Display 10,000 products on one page with real-time updates"
Solution: Involve engineering early. Use Cursor to validate feasibility:
Analyze technical feasibility:
@specs/product-display.md
Consider:
- Browser rendering limits
- Memory constraints
- Network bandwidth
- Real-time update scalability
Suggest alternative approaches if infeasible.Cursor might respond:
Current spec proposes rendering 10,000 DOM elements with WebSocket updates. This will cause:
- Browser memory: ~500MB for DOM alone
- Render blocking: 2-3 second initial paint
- WebSocket overhead: ~100 messages/sec = unsustainable
Recommended alternatives:
Option 1: Virtual scrolling
- Render only visible rows (~20-30)
- Memory: ~10MB, instant rendering
- Updates: Only refresh visible items
Option 2: Pagination + filters
- 50 items per page with smart filtering
- Pre-load next page for smooth UX
- Real-time updates only for current page
Option 3: Progressive loading
- Show first 50 instantly
- Infinite scroll loads more
- Updates via periodic polling (every 5 sec)
Recommendation: Option 1 (virtual scrolling) provides best UX without technical limitations.
✅Prevent Costly Rework
Key Takeaways for Product Teams
Executable Specifications
Structure user stories with enough detail that they become direct inputs to implementation, eliminating interpretation gaps.
Prototype Relentlessly
Validate ideas with interactive prototypes before committing engineering resources. Minutes of prototyping save weeks of rework.
Spec-Driven Development
Treat specifications as living contracts. When specs are AI-readable, implementation automatically aligns with intent.
Measure Alignment
Track spec-to-code match rate, rework rate, and cycle time. Poor metrics indicate specification quality issues.
Balance Clarity and Flexibility
Specify what and why clearly, but let engineering determine how. Over-specifying implementation details constrains solutions.