Server Actions with Database
Server Actions with Database
Section titled “Server Actions with Database”Introduction
Section titled “Introduction”Server Actions naturally integrate with databases. Since they run on the server, they can directly use Prisma, Drizzle, or any other ORM. This eliminates the need for separate API endpoints for database mutations.
Direct Database Access
Section titled “Direct Database Access”"use server"
import { db } from '@/lib/db'import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) { const title = formData.get('title') as string const content = formData.get('content') as string
const post = await db.post.create({ data: { title, content, published: false } })
revalidatePath('/posts') return { postId: post.id }}Prisma Integration
Section titled “Prisma Integration”Read Operations
Section titled “Read Operations”// app/posts/page.tsx (Server Component)import { db } from '@/lib/prisma'
export default async function PostsPage() { const posts = await db.post.findMany({ include: { author: true, _count: { select: { comments: true } } }, orderBy: { createdAt: 'desc' }, take: 20, })
return ( <div className="space-y-4"> {posts.map(post => ( <article key={post.id} className="border p-4 rounded"> <h2 className="text-xl font-semibold">{post.title}</h2> <p className="text-gray-500 text-sm"> By {post.author.name} · {post._count.comments} comments </p> </article> ))} </div> )}Write Operations
Section titled “Write Operations”"use server"
import { db } from '@/lib/prisma'import { revalidatePath } from 'next/cache'
export async function publishPost(formData: FormData) { const postId = formData.get('postId') as string
await db.post.update({ where: { id: postId }, data: { published: true, publishedAt: new Date() } })
revalidatePath(`/posts/${postId}`)}
export async function deletePost(formData: FormData) { const postId = formData.get('postId') as string
await db.post.delete({ where: { id: postId } })
revalidatePath('/posts')}Drizzle Integration
Section titled “Drizzle Integration”"use server"
import { db } from '@/lib/drizzle'import { users } from '@/lib/schema'import { eq } from 'drizzle-orm'import { revalidatePath } from 'next/cache'
export async function updateProfile(formData: FormData) { const userId = formData.get('userId') as string const name = formData.get('name') as string const bio = formData.get('bio') as string
await db.update(users) .set({ name, bio, updatedAt: new Date() }) .where(eq(users.id, userId))
revalidatePath(`/profile/${userId}`)}Transaction Patterns
Section titled “Transaction Patterns”For operations involving multiple related changes, use database transactions:
"use server"
export async function transferPoints(formData: FormData) { const fromUser = formData.get('fromUserId') as string const toUser = formData.get('toUserId') as string const amount = Number(formData.get('amount'))
// Both operations succeed or both fail await db.$transaction([ db.user.update({ where: { id: fromUser }, data: { points: { decrement: amount } } }), db.user.update({ where: { id: toUser }, data: { points: { increment: amount } } }), db.transactionLog.create({ data: { fromUser, toUser, amount, type: 'POINT_TRANSFER' } }) ])
revalidatePath('/dashboard')}Optimistic Updates with Server Actions
Section titled “Optimistic Updates with Server Actions”For a snappy UX, combine Server Actions with optimistic UI:
"use client"
import { useOptimistic } from 'react'import { addComment } from './actions'
export default function PostPage({ post, comments }) { const [optimisticComments, setOptimisticComments] = useOptimistic(comments)
async function handleSubmit(formData: FormData) { const content = formData.get('comment') as string
setOptimisticComments(prev => [...prev, { id: 'temp', content, createdAt: new Date().toISOString(), author: { name: 'You' } }])
await addComment(formData) }
return ( <form action={handleSubmit}> {/* ... */} </form> )}File Uploads with Server Actions
Section titled “File Uploads with Server Actions”Server Actions can handle file uploads via FormData:
"use server"
import { writeFile } from 'fs/promises'import { join } from 'path'
export async function uploadAvatar(formData: FormData) { const file = formData.get('avatar') as File
if (!file || file.size > 5 * 1024 * 1024) { return { error: 'File too large (max 5MB)' } }
const bytes = await file.arrayBuffer() const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}` const path = join('public/uploads', filename) await writeFile(path, buffer)
revalidatePath('/settings') return { url: `/uploads/${filename}` }}Common Mistakes
Section titled “Common Mistakes”- No connection pooling — When using serverless, ensure your database connection uses pooling (Prisma Accelerate, PgBouncer, etc.)
- Missing input validation — Always validate and sanitize database inputs. Use Zod to parse before writing.
- N+1 queries in Server Components — Use Prisma’s
includeor Drizzle’swithto eager-load relations. - Not handling concurrent mutations — Use transactions or optimistic locking for critical operations.
Best Practices
Section titled “Best Practices”- Use an ORM (Prisma or Drizzle) for type safety and migrations
- Always validate input before database operations
- Use database transactions for multi-step mutations
- Implement proper error handling — return errors, don’t let them crash the server
- Revalidate affected paths after every mutation
Summary
Section titled “Summary”Server Actions integrate directly with your database layer. Use Prisma or Drizzle for type-safe queries, transactions for atomic operations, and always validate inputs server-side. Combine with optimistic UI for a responsive user experience.