Opinionated rules for building reliable, performant server-side (and hybrid) state layers in TypeScript/JavaScript applications that use frameworks such as Next.js, React Query, Redux Toolkit, Zustand, and Jotai.
Building full-stack applications just got infinitely harder. You're juggling server-side rendering, client hydration, optimistic updates, and state synchronization across multiple environments. One stale cache hit or hydration mismatch and your users are staring at broken UIs and inconsistent data.
You've been there: Your Next.js app renders perfectly on the server, then explodes on the client because of hydration mismatches. Your React Query cache is fighting with your Redux store over who owns the user data. You're manually managing loading states across three different data-fetching libraries, and your error boundaries are catching serialization failures you can't even debug.
The real pain points:
These Cursor Rules implement a proven server-first state architecture that treats your server as the single source of truth while optimizing for modern hybrid rendering patterns. Instead of fighting the complexity, you'll work with it.
Core Philosophy: Keep server state minimal and relevant. Normalize ruthlessly. Hydrate safely. Cache intelligently.
Here's what changes in your development workflow:
// Before: Manual state sync nightmare
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users').then(res => res.json()).then(setUsers);
}, []);
// After: Declarative, cached, typed
const { data: users, isLoading } = useUsersQuery();
// Automatically cached, retried, and synchronized
Eliminate Hydration Mismatches: Built-in version checking prevents server/client state drift
// Auto-generated version headers catch mismatches before they break UI
x-state-version: abc123
End Cache Fighting: Clear separation between request-scoped cache and global state
// Request cache (Next.js)
const userData = cache(() => getUserData());
// Global state (Redux/Zustand)
const { theme, preferences } = useGlobalStore();
Bulletproof Error Handling: Typed error responses with domain-specific error classes
// Network vs validation vs auth errors handled distinctly
throw new ValidationError('Invalid user ID', { field: 'userId' });
Zero-Config Type Safety: Discriminated unions for all async states
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: ErrorResponse };
Before: 45 minutes debugging hydration mismatch between SSR user data and client-side auth state
// Broken: Date objects fail serialization
export async function getServerSideProps() {
const user = await getUser(); // Returns Date objects
return { props: { user } }; // Boom! Hydration mismatch
}
After: 2 minutes setup with automatic serialization
// Works: Safe serialization with superjson
export async function getServerSideProps() {
const user = await getUser();
return {
props: {
user: superjson.serialize(user) // Handles Date, Map, BigInt
}
};
}
Before: 30 minutes implementing rollback logic for failed optimistic updates
After: Built-in optimistic updates with automatic rollback
const updateUser = useUpdateUserMutation({
onMutate: async (userData) => {
// Auto-rollback on failure
const previousUser = queryClient.getQueryData(['user', id]);
queryClient.setQueryData(['user', id], userData);
return { previousUser };
}
});
Before: 20 minutes debugging Node.js APIs breaking in edge runtime
After: Edge-optimized patterns from day one
// Edge-safe: Uses Web APIs only
const data = await kv.getWithMetadata(key);
const response = new Response(JSON.stringify(data));
npm install @tanstack/react-query @reduxjs/toolkit superjson zod
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
state/
├── api/ # RTK Query endpoints
├── users/ # User slice, selectors, types
├── auth/ # Auth slice
└── error-mapper.ts # Centralized error handling
// state/users/slice.ts
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const usersAdapter = createEntityAdapter<User>();
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState({
status: 'idle' as const
}),
reducers: {},
extraReducers: builder => {
builder.addMatcher(
api.endpoints.getUsers.matchFulfilled,
(state, { payload }) => {
usersAdapter.setAll(state, payload);
state.status = 'success';
}
);
},
});
// pages/api/users.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const users = await getUsers();
res.json(superjson.serialize(users));
}
Development Speed: 60% reduction in state-related debugging time
Performance Gains: 40% faster page loads with optimized caching
Code Quality: 80% fewer state-related bugs in production
Team Velocity: Standardized patterns across your entire team
Your days of fighting server state complexity are over. These rules transform server-side state management from a source of bugs and performance problems into a reliable, fast foundation for your applications.
Ready to stop debugging hydration mismatches and start shipping features? Your state architecture transformation starts with your next commit.
You are an expert in the TypeScript / JavaScript ecosystem, specialised in hybrid and server-side state management with Next.js, React Query, Redux Toolkit (RTK Query), Zustand, Jotai, SWR and Edge-Runtime environments (Cloudflare Workers, Vercel Edge, etc.).
Key Principles
- Keep server state minimal and relevant; persist only essential domain data. Never mirror the entire client tree.
- Normalise entities (1-to-many, many-to-many) to remove duplication and enable O(1) updates.
- Split concerns: request-scoped cache ≠ long-lived global store; UI-only client state lives separately.
- Always source-of-truth ⇒ server → hydrate → client. Never mutate hydrated data directly on the client without an invalidation path.
- Prefer declarative, data-fetching hooks (e.g. `useQuery`) over manual `fetch` to gain caching, retries, status flags.
- Serialise data with `superjson` or `devalue` to transfer non-primitive types (Date, Map, BigInt) safely across the wire.
- Use optimistic updates only when latency is a UX issue, guard them with rollback logic.
- Secure sensitive state: encrypt cookies, sign JWTs, never embed secrets in the rendered payload.
- Fail fast: detect mismatches (checksum/version) between server snapshot and client cache and re-hydrate.
TypeScript / JavaScript
- Enable `"strict": true` in `tsconfig.json` – state is never `any`.
- Use discriminated unions for status flags (`{ status: 'idle'|'loading'|'success'|'error'; … }`).
- Export pure utility functions (`export const normalizeUsers = …`); no side-effects outside `async function` bodies.
- Use `as const` for action type strings to keep discriminated unions narrow.
- Prefer `Readonly<Record<…>>` for look-up tables instead of mutable objects.
- Directory convention: `state/feature-name/{index.ts,slice.ts,selectors.ts,types.ts}`.
Error Handling and Validation
- First lines of every resolver/handler: validate input schema with Zod/Yup, else `throw new ValidationError()`.
- Pinpoint error domain: network, validation, auth, application. Wrap each in typed subclasses extending `BaseError`.
- Inside React Query/RTK Query, convert thrown errors into `ErrorResponse` objects `{ message: string; code: KnownErrorCode }`.
- Use early returns to abort on failure; happy-path last.
- Map server errors to toast/notifier in one place (`/state/error-mapper.ts`).
Framework-Specific Rules
Next.js (app dir & pages)
- Always isolate request-level cache with `cache()`/`unstable_cache()` or `headers()['x-request-id']` key.
- Put SSR/SSG queries in `async` server components or `getServerSideProps`; never in client components.
- `cookies().get()` over `document.cookie` for server safety.
- Use `export const dynamic = 'force-dynamic'` when the resource is non-cacheable.
React Query / SWR
- Key pattern: `[scope, id]` (e.g. `["user", userId]`). No anonymous keys.
- Provide a 5-minute staleTime for read-heavy lists; 0 for detail views that need freshness.
- Always return `data`, `error`, `isLoading` from hooks; no derived UI booleans in components.
Redux Toolkit & RTK Query
- Use slices *only* for truly global cross-page state (auth, theme). Localise everything else in component scope.
- Generate endpoints with `createApi`, export auto-generated hooks, disable `refetchOnMount` when you hydrate with SSR.
- Keep slice initialState serialisable; functions & class instances are forbidden.
Zustand / Jotai
- Co-locate store files with feature; name atoms `xxxAtom`, stores `useXxxStore`.
- Always wrap usage in custom hooks (`useAuthStore()`), never inline `useStore()` in components.
Edge Functions (Vercel/Cloudflare)
- Use `kv.getWithMetadata` to fetch plus ETag for validation.
- Keep each edge request under 50 ms; preload cold data in `globalThis.bootstrap`.
- Avoid Node-only APIs (`fs`, `crypto.pbkdf2Sync`).
Additional Sections
1. Data Serialization & Hydration
- Prefer `superjson.stringify()` in `getServerSideProps` and `superjson.parse()` on client.
- Attach a version header (`x-state-version`) and embed the same version in the HTML payload to guard mismatches.
2. Testing
- Unit-test normalisers, selectors, and reducers with pure inputs/outputs.
- Integration: use `msw` to mock API and assert hydration compatibility via `@testing-library/react`.
- E2E: use Playwright to record initial HTML and compare against rehydrated DOM snapshot.
3. Performance
- Memoise expensive selectors with `createSelector` or `proxy-compare` (for Jotai).
- Use `unstable_batchedUpdates` or React 18 concurrent features to batch state changes.
- Instrument with `ReactDevTools` profiler + `why-did-you-render` extension; fix repeated renders > 2.
4. Security & Compliance
- Encrypt JWT in `HttpOnly; Secure; SameSite=Lax` cookies.
- Redact PII from server-sent state (`delete user.email`) before hydration when not required on client.
- Validate CSRF token for non-GET mutations.
5. Deployment & Ops
- Log state store version, cache hits/misses per request (use `console.time` groups).
- Roll out migrations with feature flags; keep two versions of normaliser until >95 % of clients updated.
6. Common Pitfalls Checklist
- ❌ Mutating fetched objects before storing ⇒ always clone (`structuredClone`).
- ❌ Mixing client-only and server-only code inside the same file.
- ❌ Relying on `localStorage` for critical shared state.
Example: Normalised Users Slice with RTK Query (TypeScript)
```ts
// state/users/slice.ts
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { api } from '../api';
const usersAdapter = createEntityAdapter<User>({ selectId: u => u.id });
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState({ status: 'idle' as const }),
reducers: {},
extraReducers: builder => {
builder.addMatcher(api.endpoints.getUsers.matchFulfilled, (state, { payload }) => {
usersAdapter.setAll(state, payload);
state.status = 'success';
});
},
});
export default usersSlice.reducer;
export const usersSelectors = usersAdapter.getSelectors((s: RootState) => s.users);
```
Follow these rules to keep your server-side state predictable, resilient, and blazing fast.