Katman
Integrations

React Server Actions

Call Katman procedures as React Server Actions — type-safe [error, data] tuples instead of try/catch.

React Server Actions let you call server-side code directly from React components. Katman wraps your procedures as actions that return [error, data] tuples instead of throwing, making error handling straightforward in your UI code.

This works with Next.js (App Router) and TanStack Start.

Prerequisites

You need a Katman router defined on the server. If you haven't set one up yet, see the Getting Started guide.

createAction

Wraps a single procedure as a server action. Returns [error, data] instead of throwing.

Define the action in a file with "use server":

// app/actions.ts
"use server"
import {  } from "katman/react"
import {  } from "../router"

export const  = (.users.list)
export const  = (.users.create)

Use it in a component:

// app/page.tsx
import { listUsers } from "./actions"

export default async function Page() {
  const [error, users] = await listUsers({ limit: 10 })

  if (error) {
    return <p>Error: {error.message}</p>
  }

  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  )
}

The return type

Every action returns a tuple: [error, data].

  • On success: [null, data]error is null, data is the procedure's return value
  • On failure: [error, undefined]error has code, status, message, and optionally data

This pattern avoids try/catch in your components and makes error states explicit.

createActions

Converts an entire router into actions at once. The result mirrors your router structure:

"use server"
import {  } from "katman/react"
import {  } from "../router"

export const  = ()
// In a component or server function
const [, ] = await actions.users.list({ : 10 })
const [, ] = await actions.users.create({ : "Alice" })

This is convenient when you have many procedures and don't want to export each one individually.

createFormAction

For HTML <form> submissions, createFormAction accepts FormData and converts it into the input object your procedure expects:

// app/actions.ts
"use server"
import {  } from "katman/react"

export const  = (appRouter.users.create)
<form action={submitUser}>
  <input name="name" required />
  <input name="email" type="email" required />
  <button type="submit">Create User</button>
</form>

Nested form data

Bracket notation in field names is automatically parsed into nested objects:

<input name="user[name]" value="Alice" />
<input name="user[email]" value="[email protected]" />

Becomes:

{ "user": { "name": "Alice", "email": "[email protected]" } }

Array indices work too: items[0], items[1], etc.

Custom parsing

If the default FormData parser doesn't match your needs, provide your own:

const  = createFormAction(appRouter.users.create, {
  : () => ({
    : .get("name"),
    : .get("email"),
  }),
})

Framework errors

Katman automatically detects and rethrows framework-specific errors:

  • Next.js redirect() and notFound() (errors with NEXT_ digest)
  • TanStack Router navigation errors (isNotFound: true)
  • Response objects (used for redirects)

These errors pass through untouched — they won't be caught as [error, data] tuples.

This means redirect("/login") inside a procedure works as expected in Next.js. The redirect happens, and the action doesn't return an error tuple.

useServerAction

A React hook that wraps a server action with loading and error state:

import { useServerAction } from "katman/react"

function CreateUserButton() {
  const { execute, data, error, isPending, reset } = useServerAction(createUser)

  return (
    <div>
      <button onClick={() => execute({ name: "Alice" })} disabled={isPending}>
        {isPending ? "Creating..." : "Create User"}
      </button>
      {error && <p>Error: {error.message}</p>}
      {data && <p>Created: {data.name}</p>}
    </div>
  )
}

execute() returns the same [error, data] tuple as the action itself, so you can also handle results inline.

useOptimisticServerAction

Like useServerAction, but applies an optimistic update immediately while the server call is in flight:

import { useOptimisticServerAction } from "katman/react"

function UserName({ user }) {
  const { execute, displayData, isPending } = useOptimisticServerAction(updateUser, {
    optimistic: (input) => ({ ...user, ...input }),
  })

  return (
    <div>
      <span>{displayData?.name ?? user.name}</span>
      <button onClick={() => execute({ name: "Bob" })}>Rename</button>
    </div>
  )
}
  • displayData shows the optimistic value while pending, then switches to the confirmed server response.
  • If the call fails, the optimistic value is rolled back automatically.

What's next?

  • Getting Started — set up a Katman router
  • Typed Errors — how error maps and fail() work, and how they appear in the error tuple
  • TanStack Query — client-side data fetching with caching, as an alternative

On this page