Server Actions

Server actions and mutations provide type-safe server-side functionality with client-side integration using React Query.

Project Structure

server/
├── actions/           # Server actions
│   ├── auth-actions.ts
│   ├── user-actions.ts
│   └── workspace-actions.ts
└── db/
    └── mutations/     # Client-side mutations
        ├── use-user-api.ts
        ├── use-workspace-api.ts
        └── use-member-api.ts

Server Actions

server-only

Server actions are located in server/actions/ and handle server-side operations with built-in error handling and rate limiting.

Example: User Action

Server-side implementation of user creation:

"use server"

export const createUserAction = async ({
  values,
  isFromInvitation,
}: {
  values: z.infer<typeof userSchema>
  isFromInvitation?: boolean
}) => {
  try {
    // 1. Validate input
    const validateValues = userSchema.safeParse(values)
    if (!validateValues.success) {
      throw new ValidationError(validateValues.error.message)
    }

    // 2. Check authentication
    const { user } = await getCurrentUser()
    if (!user) {
      throw new AuthenticationError()
    }

    // 3. Check rate limit
    const identifier = `ratelimit:create-user:${user.id}`
    const { success } = await ratelimit.limit(identifier)
    if (!success) {
      throw new RateLimitError()
    }

    // 4. Perform database operations
    return await dbClient.transaction(async (tx) => {
      // ... implementation
    })
  } catch (error) {
    if (error instanceof ApiError) {
      throw error
    }
    throw new DatabaseError("Failed to create user")
  }
}

Client Mutations

use-hooks

Client mutations are React hooks located in server/db/mutations/ that wrap server actions with React Query for state management.

Example: User Mutation Hook

Client-side mutation hook for user creation:

export const useCreateUser = ({ isFromInvitation }: { isFromInvitation: boolean }) => {
  const router = useRouter()

  const { mutate, isPending } = useMutation({
    mutationFn: createUserAction,
    onSuccess: (data) => {
      toast.success(data.message)
      router.refresh()
      router.push(data.hasWorkspace 
        ? createRoute("onboarding-collaborate").href
        : createRoute("onboarding-workspace").href
      )
    },
    onError: (error: any) => {
      toast.error(error.message || GLOBAL_ERROR_MESSAGE)
    },
  })

  return { isPending, server_createUser: mutate }
}

Usage in Components

Example: Using Mutations

Using mutation hooks in React components:

function CreateUserForm() {
  const { server_createUser, isPending } = useCreateUser({
    isFromInvitation: false
  })

  const onSubmit = (values: FormData) => {
    server_createUser(values)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create User"}
      </Button>
    </form>
  )
}

Key Features

Rate Limiting

Built-in rate limiting using Upstash Redis

Error Handling

Standardized error handling with custom error types

Type Safety

Full TypeScript support with Zod validation

State Management

Automatic cache invalidation with React Query

Best Practices

Server Actions

  • Use 'use server' directive
  • Implement proper validation
  • Handle all error cases
  • Use transactions for related operations

Client Mutations

  • Prefix mutation functions with 'server_'
  • Handle loading states
  • Provide meaningful error messages
  • Update UI optimistically when possible