# How to generate data fetching code with Cursor

- Tool: Cursor
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Cursor Free+, React projects with SWR
- Last updated: March 2026

## TL;DR

Cursor can generate type-safe SWR data fetching hooks when you provide your API types and fetcher patterns as context. This tutorial shows how to set up .cursorrules for consistent SWR patterns, reference your API types with @file, and prompt Cursor to generate reusable hooks with proper error handling, revalidation, and TypeScript generics.

## Generating data fetching code with Cursor and SWR

SWR by Vercel provides React hooks for data fetching with caching, revalidation, and optimistic updates. Cursor can generate these hooks quickly, but without context about your API types and fetcher pattern, the output lacks type safety and proper error handling. This tutorial establishes a pattern that produces production-ready SWR hooks every time.

## Before you start

- Cursor installed with a React + TypeScript project
- SWR installed: npm install swr
- API endpoints to fetch data from
- Familiarity with React hooks and Cursor Chat (Cmd+L)

## Step-by-step guide

### 1. Create a base fetcher module

Generate a typed fetcher function that all SWR hooks will share. This centralizes error handling, authentication headers, and base URL configuration. Use Cursor Chat (Cmd+L) to generate it.

```
// Cursor Chat prompt (Cmd+L):
// Create a typed fetcher function for SWR at
// src/lib/fetcher.ts. It should: use the API_BASE_URL
// from environment, include auth token from localStorage,
// throw typed errors with status code and message,
// handle JSON parsing, and be generic over the response type.

import { config } from '@/config/env';

export class FetchError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'FetchError';
  }
}

export async function fetcher<T>(url: string): Promise<T> {
  const token = localStorage.getItem('auth_token');
  const res = await fetch(`${config.apiBaseUrl}${url}`, {
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
  });
  if (!res.ok) {
    throw new FetchError(res.status, await res.text());
  }
  return res.json();
}
```

**Expected result:** A reusable, typed fetcher function that handles auth, errors, and JSON parsing.

### 2. Add SWR rules to .cursor/rules

Create rules that enforce consistent SWR patterns across all generated hooks. This prevents Cursor from generating inline fetchers or missing error states.

```
---
description: SWR data fetching conventions
globs: "src/hooks/**/*.ts,src/hooks/**/*.tsx"
alwaysApply: true
---

## SWR Hook Rules
- ALWAYS import fetcher from @/lib/fetcher
- ALWAYS type the return value with SWR's generic: useSWR<Type>
- ALWAYS handle loading, error, and data states in returned object
- Name hooks as use{Resource}: useUsers, useOrder, useProducts
- Place all hooks in src/hooks/
- Use SWR keys as API paths: '/users', '/orders/123'
- Include mutate function in return for cache invalidation
- NEVER create inline fetch calls inside components
```

**Expected result:** Cursor follows consistent SWR conventions when generating data fetching hooks.

### 3. Generate a data fetching hook

Ask Cursor to generate a hook for a specific resource. Reference the fetcher and your API types so the output is fully typed.

```
// Cursor Chat prompt (Cmd+L):
// @src/lib/fetcher.ts @src/types/user.ts
// Generate a useUser(id) hook using SWR that:
// - Fetches from /users/:id
// - Returns { user, isLoading, isError, mutate }
// - Only fetches when id is provided
// - Uses our typed fetcher

import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
import type { User } from '@/types/user';

export function useUser(id: string | undefined) {
  const { data, error, isLoading, mutate } = useSWR<User>(
    id ? `/users/${id}` : null,
    fetcher
  );

  return {
    user: data,
    isLoading,
    isError: !!error,
    error,
    mutate,
  };
}
```

> Pro tip: Pass null as the SWR key to conditionally skip fetching. Cursor understands this pattern when you say 'only fetch when id is provided.'

**Expected result:** A typed SWR hook with conditional fetching, error state, and cache mutation.

### 4. Generate a paginated list hook

For list endpoints, generate a hook that handles pagination with SWR. Reference your pagination types and the fetcher module.

```
// Cursor Chat prompt (Cmd+L):
// @src/lib/fetcher.ts @src/types/api.ts
// Generate a useUsers hook that fetches a paginated list
// from /users?page=N&limit=N. Return users array, total
// count, loading state, error, and a setPage function.
// Use SWR with the page number in the key.

import useSWR from 'swr';
import { useState } from 'react';
import { fetcher } from '@/lib/fetcher';
import type { PaginatedResponse } from '@/types/api';
import type { User } from '@/types/user';

export function useUsers(limit = 20) {
  const [page, setPage] = useState(1);
  const { data, error, isLoading, mutate } = useSWR<PaginatedResponse<User>>(
    `/users?page=${page}&limit=${limit}`,
    fetcher
  );

  return {
    users: data?.items ?? [],
    total: data?.total ?? 0,
    page,
    setPage,
    isLoading,
    isError: !!error,
    mutate,
  };
}
```

**Expected result:** A paginated SWR hook with page state management and typed response handling.

### 5. Generate a mutation helper alongside the hook

SWR hooks are read-only by default. Ask Cursor to generate a companion mutation function that updates the server and revalidates the SWR cache. This completes the data fetching pattern.

```
// Cursor Chat prompt (Cmd+L):
// @src/hooks/useUser.ts @src/lib/fetcher.ts
// Add a useUpdateUser hook that:
// - Takes userId
// - Returns an updateUser(data) async function
// - Sends PUT /users/:id with the data
// - Calls mutate() from useUser to revalidate the cache
// - Uses optimistic updates

import useSWRMutation from 'swr/mutation';
import type { User } from '@/types/user';
import { config } from '@/config/env';

async function updateUserFn(
  url: string,
  { arg }: { arg: Partial<User> }
) {
  const res = await fetch(`${config.apiBaseUrl}${url}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(arg),
  });
  if (!res.ok) throw new Error('Update failed');
  return res.json();
}

export function useUpdateUser(id: string) {
  return useSWRMutation(`/users/${id}`, updateUserFn);
}
```

**Expected result:** A mutation hook that updates the server and revalidates the SWR cache with optimistic updates.

## Complete code example

File: `src/lib/fetcher.ts`

```typescript
/**
 * Shared SWR fetcher with authentication and error handling.
 * All useXxx() hooks import this fetcher for consistent behavior.
 */

import { config } from '@/config/env';

export class FetchError extends Error {
  public status: number;
  public info: unknown;

  constructor(status: number, message: string, info?: unknown) {
    super(message);
    this.name = 'FetchError';
    this.status = status;
    this.info = info;
  }
}

export async function fetcher<T>(url: string): Promise<T> {
  const token =
    typeof window !== 'undefined'
      ? localStorage.getItem('auth_token')
      : null;

  const res = await fetch(`${config.apiBaseUrl}${url}`, {
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
  });

  if (!res.ok) {
    const info = await res.json().catch(() => null);
    throw new FetchError(
      res.status,
      info?.message || `Request failed: ${res.status}`,
      info
    );
  }

  return res.json() as Promise<T>;
}

export async function mutationFetcher<T>(
  url: string,
  { arg }: { arg: { method: string; body?: unknown } }
): Promise<T> {
  const token = localStorage.getItem('auth_token');
  const res = await fetch(`${config.apiBaseUrl}${url}`, {
    method: arg.method,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
    },
    body: arg.body ? JSON.stringify(arg.body) : undefined,
  });

  if (!res.ok) {
    const info = await res.json().catch(() => null);
    throw new FetchError(res.status, info?.message || 'Mutation failed', info);
  }

  return res.json() as Promise<T>;
}
```

## Common mistakes

- **Creating inline fetch calls inside components instead of reusable hooks** — Cursor generates fetch calls directly in components when not told to use SWR, losing caching, revalidation, and deduplication benefits. Fix: Add 'NEVER create inline fetch calls inside components' to .cursorrules and always ask for 'a custom hook using SWR.'
- **Forgetting conditional fetching with null keys** — Cursor may generate hooks that always fetch, even when required parameters are undefined, causing 404 errors on page load. Fix: In your prompt, specify 'only fetch when id is provided' and Cursor will use the null key pattern.
- **Not typing the SWR generic parameter** — Without the type parameter, useSWR returns unknown data type, losing all type safety downstream. Fix: Always specify the expected type: useSWR<User>('/users/1', fetcher). Add this to your SWR rules.

## Best practices

- Create a shared fetcher module that all SWR hooks import for consistent auth and error handling
- Add SWR rules to .cursor/rules mandating typed hooks, shared fetchers, and proper error states
- Use the null key pattern for conditional fetching when parameters may be undefined
- Always type SWR's generic parameter (useSWR<Type>) for full type safety
- Generate mutation hooks alongside read hooks for complete CRUD coverage
- Place all hooks in src/hooks/ with consistent naming: use{Resource}
- Reference @src/lib/fetcher.ts and @src/types/ in every SWR hook generation prompt

## Frequently asked questions

### Should I use SWR or React Query with Cursor?

Cursor generates good code for both. SWR is simpler with fewer concepts. React Query (TanStack Query) offers more features like query invalidation patterns and devtools. Specify your choice in .cursorrules so Cursor does not mix them.

### How do I handle authentication token refresh with SWR hooks?

Add token refresh logic to the shared fetcher: if a 401 is received, refresh the token and retry the request. Add this requirement to your Cursor prompt when generating the fetcher.

### Can Cursor generate SWR hooks with real-time subscriptions?

Yes. Ask Cursor to use SWR's refreshInterval option for polling, or combine SWR with a WebSocket connection for push-based updates. Specify the approach in your prompt.

### How many SWR hooks should I have per component?

Keep it to 1-3 SWR hooks per component. If a component needs more data sources, create a composite hook that combines multiple SWR calls into a single return object.

### Will Cursor handle SWR's cache properly across page navigations?

SWR caching is automatic. Cursor-generated hooks benefit from SWR's cache without extra configuration. For cross-page cache sharing, ensure your SWR keys are consistent. Add this to your .cursorrules.

---

Source: https://www.rapidevelopers.com/cursor-tutorial/how-to-instruct-cursor-ai-to-auto-generate-swr-react-hooks-for-data-fetching
© RapidDev — https://www.rapidevelopers.com/cursor-tutorial/how-to-instruct-cursor-ai-to-auto-generate-swr-react-hooks-for-data-fetching
