Chapter
03 · Entity Actions (actions/*.ts)
Server authority over data: Prisma lives here; validate, authorize, and expose get*/mutations.
Mental Model
- Single source of truth for this entity’s data.
- Encapsulates all Prisma access; framework-agnostic.
- Called by Server Pages (reads) and Server Actions (writes).
File Classification
Layer: Entity data authority
Runtime: Server only
Prisma: ✅ Yes (only here)
UI Logic: ❌ Never
Server Actions: ❌ (separate files)
revalidatePath: ❌ (unless intentionally exposed)
Should Do
- Read/write via Prisma.
- Validate inputs; enforce authorization.
- Expose get* reads and mutation helpers for Server Actions to call.
- Remain framework-agnostic (no JSX, no client imports).
Should Not (and where it belongs)
- UI logic → client components.
- Context/hooks → client or provider files.
- revalidatePath calls → server actions or route handlers that orchestrate mutations.
- Internal API fetch → not needed; call Prisma directly here.
Canonical Example
import { prisma } from "@/lib/prisma";
import type Project from "@/types/project";
export async function getProjects(): Promise<Project[]> {
return prisma.project.findMany({ orderBy: { createdAt: "desc" } }); // read
}
export async function getProjectById(id: string): Promise<Project | null> {
return prisma.project.findUnique({ where: { id } }); // guarded read
}
export async function createProject(data: { name: string; ownerId: string; }) {
if (!data.name.trim()) throw new Error("Name required"); // security/validation
return prisma.project.create({ data }); // write
}
Colour-Coded Miniature
import { prisma } from "@/lib/prisma"; // 🟡
export async function getProjectById(id: string) {
return prisma.project.findUnique({ where: { id } }); // 🟡
}Quick Rules
- All Prisma lives here; nowhere else.
- Validate and authorize at the edge of the entity.
- No UI, no hooks, no JSX.
- Server Actions call these for writes; Server Pages call these for reads.