How to use Cursor for coding
# I Spent 30 Days Coding with Cursor—Here's What Actually Works
You know that feeling when you're staring at a red squiggly line under a TypeScript error you've seen a hundred times, and you still can't remember the exact syntax for `Omit` with nested generics? That was me three months ago, until I started using Cursor, and I'm never going back to plain VS Code for complex projects.
But here's the thing: Cursor isn't magic. I spent my first week treating it like a glorified autocomplete, and I got garbage results. After 30 days of deliberate practice, I've found the patterns that actually save time—and the ones that waste it.
## The Setup That Matters
First, let's get the basics right. I'm running Cursor 0.45.x on macOS, but the same principles apply on Windows/Linux. After installing, I immediately changed two settings that most tutorials skip:
1. **Disable "Auto Completions" for inline suggestions** (Settings > Editor > Cursor > Completions: Off). The constant ghost text was making me second-guess every keystroke. Instead, I use `Ctrl+K` to trigger inline generation only when I want it.
2. **Enable "Apply Edits" mode** (Cmd+Shift+P > "Cursor: Toggle Apply Edits"). This lets Cursor modify your code directly rather than just showing a diff. Huge time saver.
## The 80/20 Rule: Two Commands You'll Actually Use
After 30 days, I've boiled Cursor down to two commands that handle 80% of my daily work:
### 1. `Cmd+K`: The "Fix This Block" Command
This is your bread and butter. Select a block of code, hit `Cmd+K`, and tell Cursor what to do. Here's a real example from a React project I'm building:
```typescript
// I selected this mess I wrote:
const handleSubmit = async (e) => {
e.preventDefault()
const data = new FormData(e.target)
const body = {}
for (let [key, val] of data.entries()) {
body[key] = val
}
// ...50 more lines of manual validation...
}
```
I typed: "Refactor to use react-hook-form with zod validation, handle loading states, and add error boundaries"
Cursor generated this in 4 seconds:
```typescript
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
type FormData = z.infer<typeof schema>
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<FormData> = async (data) => {
try {
setError(null)
await api.login(data)
toast.success('Logged in!')
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed')
}
}
```
**The trick**: Be specific about the library and patterns you want. "Refactor" alone gives you generic code. "Refactor to use react-hook-form with zod" gives you production-ready code.
### 2. `Cmd+L`: The "Explain This" Command
This is secretly the most powerful feature. I use it constantly when I'm working with unfamiliar codebases or legacy spaghetti.
Example: I was debugging a Python script that used `__getattr__` in a way I couldn't follow:
```python
class ConfigProxy:
def __init__(self, config):
self._config = config
def __getattr__(self, name):
if name.startswith('_'):
return super().__getattr__(name)
return self._config.get(name, None)
```
I highlighted it and asked: "Explain this pattern, and show me how to type-hint it properly for Pyright"
Cursor's response was surprisingly thorough—it explained the proxy pattern, pointed out that `__getattr__` would cause infinite recursion on missing attributes, and gave me a typed version with `Protocol`:
```python
from typing import Any, Protocol
class ConfigProtocol(Protocol):
def get(self, key: str, default: Any = None) -> Any: ...
class TypedConfigProxy:
def __init__(self, config: ConfigProtocol):
self._config = config
def __getattr__(self, name: str) -> Any:
if name.startswith('_'):
raise AttributeError(name)
return self._config.get(name, None)
```
**The trick**: Always ask for "type hints" or "proper typing" if you're in a typed language. Cursor defaults to untyped code unless you ask.
## The Pattern That Changed My Workflow
Here's what I do now for any new feature:
1. **Write a comment describing the function** (not the implementation)
2. **Highlight the comment** and press `Cmd+K`
3. **Review the generated code** carefully—I always find at least one edge case it missed
4. **Iterate with follow-up prompts** like "Add error handling for network failures" or "Make this handle empty arrays"
Example from a recent project:
```typescript
// I wrote this comment:
// Function that fetches user data, caches it for 5 minutes,
// and handles stale-while-revalidate pattern
// Cursor generated this (after 3 iterations):
const userCache = new Map<string, { data: User; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000
async function getUser(id: string): Promise<User> {
const cached = userCache.get(id)
const isStale = cached && Date.now() - cached.timestamp > CACHE_TTL
if (cached && !isStale) {
return cached.data
}
// Stale-while-revalidate: return stale data immediately, refresh in background
if (cached && isStale) {
fetchUser(id).then(data => {
userCache.set(id, { data, timestamp: Date.now() })
})
return cached.data
}
const user = await fetchUser(id)
userCache.set(id, { data: user, timestamp: Date.now() })
return user
}
```
## Where Cursor Fails (And What to Do Instead)
I've hit three major pain points:
**1. It hallucinates APIs.** Cursor once generated code using a `useMemo` hook with a dependency array that didn't exist in the React version I was using. **Fix**: Always verify generated code against your actual dependencies' docs.
**2. It's terrible at large refactors.** I tried asking it to "convert this Redux store to Zustand" across 5 files. It created a mess of inconsistent imports and missing state slices. **Fix**: Do refactors one file at a time, with explicit instructions about the target pattern.
**3. It struggles with project-specific conventions.** My team uses `snake_case` for database columns but `camelCase` in TypeScript. Cursor consistently generates `camelCase` everywhere. **Fix**: Add a system prompt (`.cursorrules` file in project root) with your conventions:
```
# .cursorrules
- Use camelCase for TypeScript variables
- Database column names use snake_case
- Prefer async/await over .then()
- Error handling: always use try/catch with typed errors
```
## The Real Productivity Hack
The biggest time saver isn't code generation—it's **debugging assistance**. When I get a cryptic error message, I copy it into Cursor's chat (`Cmd+L`) along with the relevant code block. It's saved me hours on:
- **TypeScript type errors**: "Type 'X' is not assignable to type 'Y'" → Cursor explains the type mismatch and suggests fixes
- **React render issues**: "Maximum update depth exceeded" → Cursor identifies the infinite loop in my useEffect dependencies
- **API integration bugs**: "Cannot read properties of undefined" → Cursor traces through async code to find the null reference
## Your Next Step
Don't try to use all of Cursor's features at once. Here's your 7-day plan:
**Day 1-2**: Only use `Cmd+K` for inline code generation. No chat, no explain, no composer.
**Day 3-4**: Add `Cmd+L` for debugging and explaining unfamiliar code. Still no composer.
**Day 5-7**: Add `.cursorrules` to your project and try the composer for multi-file edits.
After 7 days, you'll have muscle memory for the patterns that actually work. And when you inevitably hit a hallucination or a bad refactor, you'll know exactly when to trust Cursor and when to write the code yourself.
Now go open that file you've been avoiding, highlight the ugliest function, and hit `Cmd+K`. Your future self will thank you.