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.
Prerequisites
- 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
Create a base fetcher module
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.
1// Cursor Chat prompt (Cmd+L):2// Create a typed fetcher function for SWR at3// src/lib/fetcher.ts. It should: use the API_BASE_URL4// from environment, include auth token from localStorage,5// throw typed errors with status code and message,6// handle JSON parsing, and be generic over the response type.78import { config } from '@/config/env';910export class FetchError extends Error {11 constructor(public status: number, message: string) {12 super(message);13 this.name = 'FetchError';14 }15}1617export async function fetcher<T>(url: string): Promise<T> {18 const token = localStorage.getItem('auth_token');19 const res = await fetch(`${config.apiBaseUrl}${url}`, {20 headers: {21 'Content-Type': 'application/json',22 ...(token ? { Authorization: `Bearer ${token}` } : {}),23 },24 });25 if (!res.ok) {26 throw new FetchError(res.status, await res.text());27 }28 return res.json();29}Expected result: A reusable, typed fetcher function that handles auth, errors, and JSON parsing.
Add SWR rules to .cursor/rules
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.
1---2description: SWR data fetching conventions3globs: "src/hooks/**/*.ts,src/hooks/**/*.tsx"4alwaysApply: true5---67## SWR Hook Rules8- ALWAYS import fetcher from @/lib/fetcher9- ALWAYS type the return value with SWR's generic: useSWR<Type>10- ALWAYS handle loading, error, and data states in returned object11- Name hooks as use{Resource}: useUsers, useOrder, useProducts12- Place all hooks in src/hooks/13- Use SWR keys as API paths: '/users', '/orders/123'14- Include mutate function in return for cache invalidation15- NEVER create inline fetch calls inside componentsExpected result: Cursor follows consistent SWR conventions when generating data fetching hooks.
Generate a data fetching hook
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.
1// Cursor Chat prompt (Cmd+L):2// @src/lib/fetcher.ts @src/types/user.ts3// Generate a useUser(id) hook using SWR that:4// - Fetches from /users/:id5// - Returns { user, isLoading, isError, mutate }6// - Only fetches when id is provided7// - Uses our typed fetcher89import useSWR from 'swr';10import { fetcher } from '@/lib/fetcher';11import type { User } from '@/types/user';1213export function useUser(id: string | undefined) {14 const { data, error, isLoading, mutate } = useSWR<User>(15 id ? `/users/${id}` : null,16 fetcher17 );1819 return {20 user: data,21 isLoading,22 isError: !!error,23 error,24 mutate,25 };26}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.
Generate a paginated list hook
Generate a paginated list hook
For list endpoints, generate a hook that handles pagination with SWR. Reference your pagination types and the fetcher module.
1// Cursor Chat prompt (Cmd+L):2// @src/lib/fetcher.ts @src/types/api.ts3// Generate a useUsers hook that fetches a paginated list4// from /users?page=N&limit=N. Return users array, total5// count, loading state, error, and a setPage function.6// Use SWR with the page number in the key.78import useSWR from 'swr';9import { useState } from 'react';10import { fetcher } from '@/lib/fetcher';11import type { PaginatedResponse } from '@/types/api';12import type { User } from '@/types/user';1314export function useUsers(limit = 20) {15 const [page, setPage] = useState(1);16 const { data, error, isLoading, mutate } = useSWR<PaginatedResponse<User>>(17 `/users?page=${page}&limit=${limit}`,18 fetcher19 );2021 return {22 users: data?.items ?? [],23 total: data?.total ?? 0,24 page,25 setPage,26 isLoading,27 isError: !!error,28 mutate,29 };30}Expected result: A paginated SWR hook with page state management and typed response handling.
Generate a mutation helper alongside the hook
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.
1// Cursor Chat prompt (Cmd+L):2// @src/hooks/useUser.ts @src/lib/fetcher.ts3// Add a useUpdateUser hook that:4// - Takes userId5// - Returns an updateUser(data) async function6// - Sends PUT /users/:id with the data7// - Calls mutate() from useUser to revalidate the cache8// - Uses optimistic updates910import useSWRMutation from 'swr/mutation';11import type { User } from '@/types/user';12import { config } from '@/config/env';1314async function updateUserFn(15 url: string,16 { arg }: { arg: Partial<User> }17) {18 const res = await fetch(`${config.apiBaseUrl}${url}`, {19 method: 'PUT',20 headers: { 'Content-Type': 'application/json' },21 body: JSON.stringify(arg),22 });23 if (!res.ok) throw new Error('Update failed');24 return res.json();25}2627export function useUpdateUser(id: string) {28 return useSWRMutation(`/users/${id}`, updateUserFn);29}Expected result: A mutation hook that updates the server and revalidates the SWR cache with optimistic updates.
Complete working example
1/**2 * Shared SWR fetcher with authentication and error handling.3 * All useXxx() hooks import this fetcher for consistent behavior.4 */56import { config } from '@/config/env';78export class FetchError extends Error {9 public status: number;10 public info: unknown;1112 constructor(status: number, message: string, info?: unknown) {13 super(message);14 this.name = 'FetchError';15 this.status = status;16 this.info = info;17 }18}1920export async function fetcher<T>(url: string): Promise<T> {21 const token =22 typeof window !== 'undefined'23 ? localStorage.getItem('auth_token')24 : null;2526 const res = await fetch(`${config.apiBaseUrl}${url}`, {27 headers: {28 'Content-Type': 'application/json',29 ...(token ? { Authorization: `Bearer ${token}` } : {}),30 },31 });3233 if (!res.ok) {34 const info = await res.json().catch(() => null);35 throw new FetchError(36 res.status,37 info?.message || `Request failed: ${res.status}`,38 info39 );40 }4142 return res.json() as Promise<T>;43}4445export async function mutationFetcher<T>(46 url: string,47 { arg }: { arg: { method: string; body?: unknown } }48): Promise<T> {49 const token = localStorage.getItem('auth_token');50 const res = await fetch(`${config.apiBaseUrl}${url}`, {51 method: arg.method,52 headers: {53 'Content-Type': 'application/json',54 ...(token ? { Authorization: `Bearer ${token}` } : {}),55 },56 body: arg.body ? JSON.stringify(arg.body) : undefined,57 });5859 if (!res.ok) {60 const info = await res.json().catch(() => null);61 throw new FetchError(res.status, info?.message || 'Mutation failed', info);62 }6364 return res.json() as Promise<T>;65}Common mistakes when generating data fetching code with Cursor
Why it's a problem: Creating inline fetch calls inside components instead of reusable hooks
How to avoid: Add 'NEVER create inline fetch calls inside components' to .cursorrules and always ask for 'a custom hook using SWR.'
Why it's a problem: Forgetting conditional fetching with null keys
How to avoid: In your prompt, specify 'only fetch when id is provided' and Cursor will use the null key pattern.
Why it's a problem: Not typing the SWR generic parameter
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Create a React data fetching pattern using SWR. Generate: 1) A shared fetcher function with auth headers and typed errors. 2) A useUser(id) hook that conditionally fetches from /users/:id. 3) A useUsers(page, limit) hook with pagination. 4) A useUpdateUser mutation hook. All hooks must be fully typed with TypeScript generics.
In Cursor Chat (Cmd+L): @src/lib/fetcher.ts @src/types/user.ts Generate a useUser(id) SWR hook in src/hooks/useUser.ts. Use our fetcher module. Type the response as User. Return { user, isLoading, isError, mutate }. Only fetch when id is provided. Follow the SWR conventions in our .cursorrules.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation