Overview
createAction from @noverax/courier-next creates a dot-notation client for use in Next.js Server Actions. It works like createCaller, but automatically forwards the incoming request's headers via next/headers: authorization and the other safe ones, the headers the source and the endpoint name, and the user's cookies where they name cookie.
import { createAction } from "@noverax/courier-next"
import { sources } from "@/api"
export const action = createAction(sources, "api")"use server"
import { revalidatePath } from "next/cache"
import { action } from "@/lib/actions"
export async function createUser(data: { name: string; email: string }) {
const user = await action.users.create(data)
revalidatePath("/users")
return user
}API signature
import type { InferClient } from "@noverax/courier"
import type { Sources } from "@noverax/courier/define"
import type { ActionDefaults } from "@noverax/courier-next"
declare function createAction<TSources extends Sources, TKey extends string & keyof TSources>(
sources: TSources,
source: TKey,
defaults?: ActionDefaults,
): InferClient<TSources[TKey]>| Parameter | Description |
|---|---|
sources | Sources map (same object passed to apiRoute) |
source | Source name — key in the sources map |
defaults | Optional configuration |
ActionDefaults
type ActionDefaults = {
// Forward incoming request headers to the API (default: true)
headers?: boolean
}The action's request
createAction reads the action's own request through next/headers. An API that keeps a session gets the user's session from it — the cookies the session names, and the double-submit token a write repeats — and the session cookies the API sets reach the browser with the action's response; see User Sessions. By default the request's headers are forwarded too, through the same allowlist a context goes through; headers: false turns that off, and leaves the session to the API that keeps one.
Basic usage
Create the action client once and use it across your server action files:
import { createAction } from "@noverax/courier-next"
import { sources } from "@/api"
export const action = createAction(sources, "api")"use server"
import { revalidatePath } from "next/cache"
import { action } from "@/lib/actions"
export async function createUser(data: { name: string; email: string }) {
const user = await action.users.create(data)
revalidatePath("/users")
return user
}
export async function updateUser(data: { id: number; name: string; email: string }) {
const user = await action.users.update(data)
revalidatePath("/users")
return user
}
export async function deleteUser(id: number) {
await action.users.remove({ id })
revalidatePath("/users")
revalidatePath("/dashboard")
}ActionState
ActionState is a convenience type exported from @noverax/courier-next for structuring server action return values with useActionState:
type ActionState<T = unknown> = {
data: T | null
error: {
message: string
status?: number
issues?: readonly ValidationIssue[] // Standard Schema validation errors
} | null
}You can use it to wrap your server actions with consistent error handling:
"use server"
import type { ActionState } from "@noverax/courier-next"
import { revalidatePath } from "next/cache"
import { HTTPError } from "@noverax/courier"
import { action } from "@/lib/actions"
export async function createUser(
_prev: ActionState,
formData: FormData,
): Promise<ActionState> {
try {
const data = {
name: formData.get("name") as string,
email: formData.get("email") as string,
}
const user = await action.users.create(data)
revalidatePath("/users")
return { data: user, error: null }
} catch (error) {
if (error instanceof HTTPError) {
return { data: null, error: { message: error.message, status: error.status } }
}
return { data: null, error: { message: "Unknown error" } }
}
}Using with useActionState
"use client"
import { useActionState } from "react"
import { createUser } from "@/app/actions/users"
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(createUser, {
data: null,
error: null,
})
return (
<form action={formAction}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{state.error && <p className="text-red-500">{state.error.message}</p>}
{state.error?.issues && (
<ul>
{state.error.issues.map((issue, i) => (
<li key={i}>{issue.message}</li>
))}
</ul>
)}
{state.data && <p className="text-green-500">Created: {state.data.name}</p>}
</form>
)
}Difference from createCaller
Both createAction and createCaller return a TypedClient with dot-notation access. The difference:
createServerCaller(api)— takes a singleDefinedApi; its calls carry the session of the request Next.js is serving, wherever they are made — a page, a layout, a Server Action, a Route HandlercreateAction(sources, "api")— takesSourcesmap + source name; reads the action's own request, its headers forwarded through the allowlist
// createServerCaller — the session of the request being served
const created = await server.users.create(data)
// createAction — the action's request, its headers forwarded too
const forwarded = await action.users.create(data)Use either in Server Actions. Use createServerCaller in Server Components and Route Handlers.
Server Actions vs useMutation
Both Server Actions and React Query's useMutation can handle mutations. They serve different use cases:
- Server Actions — best for form submissions, progressive enhancement, and when you want Next.js to handle revalidation
- useMutation — best for optimistic updates, cache invalidation via query keys, and when you need React Query's retry/rollback features
"use client"
import { q } from "@/lib/query"
export function CreateUserButton() {
// useMutation calls the proxy endpoint, not a Server Action
// Use this when you want React Query's cache management
const { mutate, isPending } = q.users.useMutation("create", {
onSuccess: (user) => {
console.log("Created:", user.name)
},
})
return (
<button
onClick={() => mutate({ name: "Jane", email: "jane@test.com", role: "user" })}
disabled={isPending}
>
{isPending ? "Creating..." : "Add User"}
</button>
)
}Next Steps
- createCaller — full API reference with defaults and raw data mode
- useMutation — React Query mutations with proxy mode
- Error Handling — HTTPError structure and error callbacks