Files
kolaytercih/.claude/skills/nextjs-app-architecture/references/queries-actions.md
bilalgursen 820eea7eca refactor: ortak tipler @/types/yokatlas + features/rapor/types'a çıkarıldı, server-only korumaları eklendi (Faz 1)
- Program/PUAN_TURLERI/DilimKey/RankResults/UniturGrubu/ProgramNetSatiri/
  SihirbazFacetleri artık src/types/yokatlas.ts'te; lib/db.ts yalnızca sorgu
  fonksiyonları barındırıyor
- RaporSonuc/RaporKapsam/RaporParams/MaskeliRapor src/features/rapor/types/
  rapor.ts'e taşındı; ölü kalan TercihSchema/RaporSchema silindi
- 11 lib dosyasına import "server-only" eklendi; scripts'in düz node ile
  import ettiği dosyalara (db, appdb, rapor-havuzu, tadimlik-havuzu, ai/client,
  ai/cagri, credits) bilinçli eklenmedi
- Davranış değişikliği yok; pnpm build yeşil

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 01:06:06 +03:00

119 lines
5.5 KiB
Markdown

# Queries and actions
The data layer. Every feature has both: queries to read, actions to write.
This page covers the universal data layer that applies to every Next.js App Router app. When `cacheComponents: true` is enabled, follow `references/cache-components.md`: reusable reads are cached/tagged/lifetimed, and mutations update matching tags.
## Cache identities
When a server read also seeds a browser data cache, follow `references/single-page-applications.md` for the feature-local cache contract and client-library placement.
## Queries
Create `features/<domain>/<domain>-queries.ts`. Mark it `import 'server-only'` — that's the invariant. Default to plain async exports.
```ts
import 'server-only';
export async function getFeed(userId: string) {
return db.post.findMany({ where: { userId } });
}
```
Use [`cache()`](https://react.dev/reference/react/cache) from React only for **request-level deduplication** when the same dynamic query is called multiple times with the same arguments in one render. Highest-value cases: a session/user lookup used by many queries, or a shared expensive read used by metadata + page sections. Don't wrap every query "just in case" — it adds indirection and can hide when data is intentionally dynamic.
`cache()` dedups within a request; `'use cache'` + `cacheTag` (Cache Components) shares results *across* requests. Don't add React `cache()` to a function only because it already uses `'use cache'`; that is double-caching unless you have a separate, proven same-request duplication problem. See `references/cache-components.md`.
## Actions
Create `features/<domain>/<domain>-actions.ts`. Mark with `'use server'` at the top. Always:
1. Verify auth.
2. Validate input with your schema validator.
3. Run the mutation.
4. Invalidate cached data so the next render sees the new state.
5. Return a result (`{ ok }` or `{ error }`).
```tsx
'use server';
import { refresh } from 'next/cache';
export async function createPost(formData: FormData) {
const user = await verifyUser();
const parsed = schema.safeParse({ body: formData.get('body') });
if (!parsed.success) {
return { ok: false as const, error: parsed.error.issues[0].message };
}
await db.post.create({ data: { body: parsed.data.body, userId: user.id } });
refresh();
return { ok: true as const };
}
```
[`refresh()`](https://preview.nextjs.org/docs/app/api-reference/functions/refresh) re-renders the current route for the current user. Use it when the affected read is deliberately dynamic and has no tag. With Cache Components enabled, reusable reads should have matching `cacheTag()` calls, so server actions normally call `updateTag()` for read-your-own-writes. See `references/cache-components.md`.
### Action file naming
Actions for a feature always go in `<folder>-actions.ts`, matching the folder name — even when the mutation operates on a sub-concept. `toggleFavorite` in `features/event/` lives in `event-actions.ts`, not `favorite-actions.ts`. The folder is the source of truth for the name.
## Calling actions from client components
Client components import server actions directly. **Don't** pass an action as a prop just to call it:
```tsx
// Right
'use client';
import { likePost } from '@/features/post/post-actions';
export function LikeButton({ postId }: { postId: string }) {
return <button onClick={() => likePost(postId)}>Like</button>;
}
```
```tsx
// Wrong — adds indirection with no benefit
async function Post({ id }: { id: string }) {
return <LikeButton postId={id} onLike={likePost} />;
}
```
Design components (`<BottomNav>`, `<ToggleGroup>`, `<SubmitButton>`) take this further with the **action-prop pattern**`action` is a callback wrapped in `useTransition` / `useOptimistic` internally. See `references/ux-patterns.md`.
## Form actions vs onClick handlers
Prefer [`<form action={serverAction}>`](https://react.dev/reference/react-dom/components/form#action) for form mutations — React wraps the call in a transition and surfaces pending state automatically.
For one-off buttons, `onClick={() => action(args)}` is fine. Wrap in [`startTransition`](https://react.dev/reference/react/startTransition) if you need pending state.
## Return shape
Return a discriminated union from actions that can fail:
```tsx
export type ActionResult<T = void> = { ok: true; data?: T } | { ok: false; error: string };
```
Toast on `ok: false` from the client. Skip success toasts when an optimistic UI already shows the result.
A shared `ActionResult<T>` is optional — a per-action inline union is just as good, and often clearer when the payload has a natural name: `return { ok: true as const, playlist }` reads better than a generic `data`. What matters is that fallible actions return a discriminated union the client can narrow on, not that every action shares one type.
## Mappers and domain types
If your DB rows have shapes you don't want to leak to components (extra columns, ORM-specific types), write a mapper inside the query:
```ts
export async function getPost(id: string) {
const row = await db.post.findUnique({ where: { id }, include: { author: true } });
if (!row) notFound();
return toPost(row);
}
function toPost(row: PostRow & { author: UserRow }): Post {
return { id: row.id, body: row.body, author: row.author.handle };
}
```
Components see `Post`, not the ORM row. If that type is imported by multiple files in the feature, put it under `features/<domain>/types/` (for example `features/post/types/post.ts`). Promote it to top-level `types/` only when multiple features import it.