Docs/Routing & Data/Form Actions

Routing & Data

Form Actions

Form Actions — Routing & Data documentation for BCP Framework.

Last updated: 2026-08-29T01:11:41

Form Actions and Server Mutations

BCP form actions provide a route-scoped server mutation primitive for application forms without requiring an API route for every create, update or delete operation.

This feature is intentionally separate from future BCP Server Actions. In this release, client pages do not import a server function directly. A page submits a named action such as "saveUser", and BCP resolves that name from the sibling actions.ts / actions.tsx file on the server.

Route convention

text
app/users/[id]/
├─ guard.ts        # optional
├─ loader.ts       # optional read path
├─ actions.ts      # server-only mutation path
└─ page.tsx

Only one of actions.ts or actions.tsx may exist next to a page.

Defining an action

TypeScript
// app/users/[id]/actions.ts
import "bcp/server-only";

import {
    cookies,
    redirect,
    type PageActionContext,
} from "bcp/server";

import {
    revalidatePath,
    revalidateTag,
} from "bcp/cache";

export async function saveUser(
    formData: FormData,
    context: PageActionContext
) {
    const name =
        String(
            formData.get("name") ?? ""
        ).trim();

    // Perform the database mutation here.

    const cookieStore =
        await cookies();

    cookieStore.set(
        "last-user",
        context.params.id,
        {
            httpOnly: true,
            sameSite: "lax",
            path: "/",
        }
    );

    revalidatePath("/users");
    revalidateTag("users");

    return {
        ok: true,
        id: context.params.id,
        name,
    };
}

export async function removeUser(
    _formData: FormData,
    context: PageActionContext
) {
    // Delete from the database.

    revalidatePath("/users");

    return redirect(
        "/users",
        303
    );
}

An action receives:

  • formData — the submitted browser FormData.
  • context.params — matched dynamic route parameters.
  • context.searchParams — a fresh URLSearchParams for the target page URL.
  • context.guardData — merged data from route guards that ran before the action.
  • context.method — the semantic mutation method: POST, PUT, PATCH, or DELETE.

The normal bcp/server request context is active while the action runs, so cookies(), headers(), requestUrl(), requestMethod(), requestId(), getSession() and other server helpers work directly. requestMethod() reflects the semantic action method rather than the internal POST transport.

Rendering a form

TypeScript
"use client";

import {
    Form,
    useActionData,
    useActionError,
    useFormStatus,
} from "bcp";

interface SaveResult {
    ok: boolean;
    id: string;
    name: string;
}

export default function UserEditor() {
    return (
        <Form
            action="saveUser"
            method="patch"
        >
            <input
                name="name"
                required
            />

            <SubmitButton />
            <Result />
        </Form>
    );
}

function SubmitButton() {
    const status =
        useFormStatus();

    return (
        <button
            type="submit"
            disabled={status.pending}
        >
            {status.pending
                ? "Saving..."
                : "Save"}
        </button>
    );
}

function Result() {
    const data =
        useActionData<SaveResult>();
    const error =
        useActionError();

    if (error) {
        return (
            <p role="alert">
                {error.message}
            </p>
        );
    }

    if (!data) {
        return null;
    }

    return (
        <p>
            Saved {data.name}
        </p>
    );
}

Supported mutation methods

<Form> supports:

TypeScript
<Form action="createUser" method="post" />
<Form action="replaceUser" method="put" />
<Form action="updateUser" method="patch" />
<Form action="deleteUser" method="delete" />

Browsers only support GET and POST as native HTML form methods. BCP therefore uses POST as the wire transport for progressive enhancement and preserves the intended method separately. The action context and requestMethod() still report PUT, PATCH, or DELETE as requested.

GET is deliberately not an action method. Reads belong in page loaders or API GET handlers.

Action return values

A mutation may return either:

  1. JSON-safe action data.
  2. undefined, which BCP normalizes to null.
  3. A Web Response, commonly redirect().

Serializable action data follows the same safety model as loader data: primitives, arrays and plain objects are allowed. Functions, symbols, BigInt, non-finite numbers, Date/Map/Set/class instances and circular references are rejected before transport.

For validation-style UI in this release, return a serializable object such as:

TypeScript
return {
    ok: false,
    fieldErrors: {
        email: "Email is required",
    },
};

Typed validation helpers and CSRF-specific APIs are planned as a separate security/validation milestone.

Pending, data, and transport errors

Inside a <Form> subtree:

  • useFormStatus() exposes pending, action, method, and the current transport/server error.
  • useActionData<T>() exposes the most recent serializable action result.
  • useActionError() exposes the most recent thrown transport/runtime error.

Submitting the form again clears the previous action data before the new request begins.

Redirects

Actions can return a normal BCP redirect:

TypeScript
return redirect(
    "/users",
    303
);

With JavaScript enabled, BCP converts the redirect into an action transport payload and continues with client navigation. Without JavaScript, the browser receives the normal HTTP redirect.

Cookies set before the redirect are preserved in both paths.

Revalidation and refreshing loaders

revalidatePath() and revalidateTag() can be called directly inside an action to invalidate BCP data cache entries:

TypeScript
revalidatePath("/users");
revalidateTag("users");

If the current page should immediately rerun its loader after a successful data-returning action, enable the form's refresh option:

TypeScript
<Form
    action="saveUser"
    method="patch"
    refresh
>
    ...
</Form>

refresh uses the existing BCP router refresh path after the action result is received. Redirecting actions do not need refresh.

Progressive enhancement

The rendered HTML form posts to BCP's internal action endpoint using a normal browser POST. The target page is recovered from the same-origin referrer when JavaScript is unavailable.

When an action returns serializable data and no explicit Response, the no-JavaScript path uses POST/Redirect/GET with HTTP 303 back to the original target page. This prevents duplicate browser resubmission on refresh.

With JavaScript enabled, <Form> intercepts the submit and sends the same FormData through the action transport without reloading the document.

Middleware, guards, and request order

For a mutation, the effective server order is:

text
Security gateway
    ↓
Project middleware
    ↓
Resolve target page
    ↓
Route guards (root → child)
    ↓
Named action
    ↓
Response cookies / redirect / data

A guard redirect prevents the action from running. Guard data is passed into context.guardData, so an authorization lookup does not need to be repeated in the action.

Why action names are strings

This release intentionally uses:

TypeScript
<Form action="saveUser">

and not:

TypeScript
<Form action={saveUser}>

Passing a server function through a client bundle requires a compiler-generated server reference, serialization protocol and action identifier system. That belongs to the later BCP Server Actions milestone. Keeping named route actions separate now prevents server/database modules from being pulled into hydrated client graphs.

When to use an API route instead

Use a form action when the mutation belongs to a BCP page UI.

Keep an API route when the caller is an external client such as:

  • a mobile application,
  • another backend service,
  • a webhook provider,
  • Postman/API consumers,
  • a public or versioned HTTP API.

A project can use both patterns: loaders for page reads, guards for authorization, form actions for page-owned mutations, and API routes for external HTTP contracts.