Server Actions
Logging in Next.js Server Actions for form handling and mutations.
Overview
vestigAction() wraps your Server Actions to provide:
- Automatic logging — Start/end of action execution
- Correlation context — Links to the originating request
- Error handling — Automatic error capture and logging
- Timing — Action duration tracking
Basic Usage
typescript
1// app/actions/user.ts2'use server'3 4import { vestigAction } from '@vestig/next'5 6export const createUser = vestigAction(7 async (formData: FormData, { log, ctx }) => {8 log.info('Creating user', { requestId: ctx.requestId })9 10 const name = formData.get('name') as string11 const email = formData.get('email') as string12 13 const user = await db.users.create({14 data: { name, email }15 })16 17 log.info('User created', { userId: user.id })18 19 return { success: true, userId: user.id }20 },21 { namespace: 'actions:createUser' }22)Using in Components
Form Component
typescript
1// app/components/create-user-form.tsx2'use client'3 4import { createUser } from '@/app/actions/user'5 6export function CreateUserForm() {7 return (8 <form action={createUser}>9 <input name="name" placeholder="Name" required />10 <input name="email" type="email" placeholder="Email" required />11 <button type="submit">Create User</button>12 </form>13 )14}With useFormState
typescript
1'use client'2 3import { useFormState } from 'react-dom'4import { createUser } from '@/app/actions/user'5 6export function CreateUserForm() {7 const [state, formAction] = useFormState(createUser, null)8 9 return (10 <form action={formAction}>11 <input name="name" required />12 <input name="email" type="email" required />13 <button type="submit">Create</button>14 {state?.error && <p>{state.error}</p>}15 {state?.success && <p>User created!</p>}16 </form>17 )18}Action Context
The second argument provides:
typescript
1interface ActionContext {2 log: Logger // Namespaced logger3 ctx: CorrelationContext // requestId, traceId, spanId4}Using Context
typescript
1export const updateUser = vestigAction(2 async (formData: FormData, { log, ctx }) => {3 const userId = formData.get('userId') as string4 5 log.info('Updating user', {6 userId,7 requestId: ctx.requestId,8 traceId: ctx.traceId9 })10 11 // Update logic...12 },13 { namespace: 'actions:updateUser' }14)Configuration Options
typescript
1interface VestigActionOptions {2 // Logger namespace3 namespace?: string4 5 // Log the action input6 logInput?: boolean7 8 // Log the action output9 logOutput?: boolean10}With Options
typescript
1export const sensitiveAction = vestigAction(2 async (data, { log }) => {3 // ...4 },5 {6 namespace: 'actions:sensitive',7 logInput: false, // Don't log input (sensitive data)8 logOutput: true // Log the result9 }10)Input Types
FormData
typescript
1export const submitForm = vestigAction(2 async (formData: FormData, { log }) => {3 const name = formData.get('name')4 const email = formData.get('email')5 6 log.debug('Form data received', { name, email })7 8 // Process form...9 }10)Object Input
typescript
1export const createPost = vestigAction(2 async (data: { title: string; content: string }, { log }) => {3 log.info('Creating post', { title: data.title })4 5 const post = await db.posts.create({ data })6 7 return { success: true, postId: post.id }8 }9)Bound Arguments
typescript
1// Server Component2export default async function Page({ params }) {3 const createPostForUser = createPost.bind(null, params.userId)4 5 return <form action={createPostForUser}>...</form>6}7 8// Action9export const createPost = vestigAction(10 async (userId: string, formData: FormData, { log }) => {11 log.info('Creating post for user', { userId })12 // ...13 }14)Error Handling
Errors are automatically logged:
typescript
1export const riskyAction = vestigAction(2 async (data, { log }) => {3 // If this throws, error is logged automatically4 await riskyOperation()5 6 return { success: true }7 }8)Manual Error Handling
typescript
1export const createUser = vestigAction(2 async (formData: FormData, { log }) => {3 try {4 const user = await db.users.create({5 data: {6 name: formData.get('name'),7 email: formData.get('email')8 }9 })10 11 log.info('User created', { userId: user.id })12 13 return { success: true, userId: user.id }14 } catch (error) {15 if (error.code === 'P2002') {16 log.warn('Duplicate email', { email: formData.get('email') })17 return { success: false, error: 'Email already exists' }18 }19 20 log.error('Failed to create user', error)21 return { success: false, error: 'Something went wrong' }22 }23 }24)Validation
Log validation failures:
typescript
1import { z } from 'zod'2 3const userSchema = z.object({4 name: z.string().min(2),5 email: z.string().email()6})7 8export const createUser = vestigAction(9 async (formData: FormData, { log }) => {10 const data = {11 name: formData.get('name'),12 email: formData.get('email')13 }14 15 const result = userSchema.safeParse(data)16 17 if (!result.success) {18 log.warn('Validation failed', {19 errors: result.error.flatten()20 })21 22 return {23 success: false,24 errors: result.error.flatten().fieldErrors25 }26 }27 28 // Create user...29 }30)Revalidation
Log revalidation:
typescript
1import { revalidatePath, revalidateTag } from 'next/cache'2 3export const updatePost = vestigAction(4 async (formData: FormData, { log }) => {5 const postId = formData.get('postId') as string6 7 await db.posts.update({8 where: { id: postId },9 data: { title: formData.get('title') }10 })11 12 log.info('Post updated, revalidating', { postId })13 14 revalidatePath('/posts')15 revalidateTag('posts')16 17 return { success: true }18 }19)Redirect
Log before redirects:
typescript
1import { redirect } from 'next/navigation'2 3export const createUser = vestigAction(4 async (formData: FormData, { log }) => {5 const user = await db.users.create({6 data: { name: formData.get('name') }7 })8 9 log.info('User created, redirecting', { userId: user.id })10 11 redirect(`/users/${user.id}`)12 }13)Timing
Action duration is automatically logged:
json
1{2 "level": "info",3 "message": "Action completed",4 "namespace": "actions:createUser",5 "metadata": {6 "duration": "125.45ms",7 "success": true8 }9}