Server actions

An action is a Go struct that handles a client-triggered operation such as a form submission or an RPC call. The struct embeds piko.ActionMetadata and implements a Call method with typed parameters and a typed return value. The generator registers every action struct it finds and mounts it at a conventional URL. This page describes the types and surface. For task recipes see the how-to guides on forms and streaming with SSE.

About the action protocol walks through the dispatch sequence, including CSRF, parameter binding, rate limiting, and the fork between Call and StreamProgress.

Action struct

An action is any exported struct that embeds piko.ActionMetadata:

package actions

import "piko.sh/piko"

type ContactSubmitAction struct {
    piko.ActionMetadata
}

func (a ContactSubmitAction) Call(name string, email string, message string) (ContactResponse, error) {
    return ContactResponse{OK: true}, nil
}

The generated dispatch code binds incoming fields to Call arguments, validates them against the parameter types, and returns the response value as JSON.

The generator detects the dispatch method by the exact name Call. A struct that defines Handle or Run instead compiles but is not mounted.

ActionMetadata

Embedded field. Exposes three methods that operate on the current request.

MethodTypePurpose
a.Ctx()context.ContextRequest context. Cancellation propagates to any downstream IO.
a.Request()*piko.RequestMetadataURL, method, headers, query parameters, form data, session, cookies.
a.Response()*piko.ResponseWriterSet cookies, attach headers, add client-side helpers, queue field errors.

The dispatch layer validates CSRF tokens before Call runs.

Call signature

Two shapes are valid and interchangeable:

Individual parameters:

func (a *CustomerUpsertAction) Call(id *int64, name string, email string) (UpsertResponse, error) {
    return UpsertResponse{}, nil
}

Input struct (idiomatic for actions with three or more fields or struct-level validation):

type SignupInput struct {
    Username string  `json:"username" validate:"required,min=3"`
    Email    string  `json:"email"    validate:"required,email"`
    Password string  `json:"password" validate:"required,min=10"`
    Age      int     `json:"age"`
    Website  *string `json:"website"`
}

func (a *SignupAction) Call(input SignupInput) (SignupResponse, error) {
    return SignupResponse{UserID: 42}, nil
}

The two are equivalent from the dispatch layer's perspective. Whether the generator sees Call(name string, email string) or Call(input SignupInput), it emits a TypeScript call shape on the client that mirrors the Go parameters. For guidance on when each form fits best, see about the action protocol.

The generator maps every Go parameter to a client argument position by one-for-one correspondence. Call signatures cannot include context.Context. A context.Context parameter would shift the index of every other argument relative to the generated client. The embedded piko.ActionMetadata exposes the request context through a.Ctx().

The dispatch layer parses the request body and maps fields to parameters or struct fields by their JSON tag (or by Go field name, for unnamed parameters). It applies validate rules and rejects requests whose fields fail conversion or validation before calling the method body.

Pointer parameters (*int64, *string) mark a value as optional. A value type with a zero value is still considered set.

Typed responses

The first return value is the action's own response struct, serialised to JSON. There is no generic ActionResponse wrapper.

type SignupResponse struct {
    UserID int    `json:"user_id"`
    Name   string `json:"name"`
}

The second return value, error, signals failure. See Error handling.

Response() helpers

a.Response() returns the action's *piko.ResponseWriter, which queues client-side effects onto the response:

MethodArgumentsEffect
AddHelper(name, args...)helper name and its argumentsQueues a helper call the frontend runtime dispatches.
SetCookie(cookie)*http.CookieAttaches a Set-Cookie header.
AddHeader(key, value)header name and valueAppends a response header (allows duplicates).
SetHeader(key, value)header name and valueReplaces an existing response header.

Built-in helpers

HelperArgumentsEffectRequires
showToastmessage, level, [durationMs]Displays a toast notification (success, error, warning, info)piko.ModuleToasts
closeModalnoneCloses the current modalpiko.ModuleModals
reloadPartialalias or selectorRefetches and re-renders a partialpiko.ModuleModals
redirecturlClient-side navigationalways enabled

Enable modules at bootstrap:

ssr := piko.New(
    piko.WithFrontendModule(piko.ModuleToasts),
    piko.WithFrontendModule(piko.ModuleModals),
)
FunctionDescription
piko.SessionCookie(name, value, expires)Secure session cookie: HttpOnly, Secure, SameSite=Lax.
piko.SessionCookieInsecure(name, value, expires)Session cookie without the Secure flag, for local development over HTTP.
piko.SmartSessionCookie(name, value, expires)Session cookie that sets the Secure flag automatically based on the runtime environment (secure in production, insecure in development).
piko.ClearCookie(name)Expires the named cookie.
piko.ClearCookieInsecure(name)Clear-cookie variant without the Secure flag.
piko.SmartClearCookie(name)Clear-cookie that adapts the Secure flag to the runtime environment.
piko.Cookie(name, value string, maxAge time.Duration, opts ...CookieOption)Customisable cookie with functional options.

The piko.Cookie constructor accepts functional options:

OptionEffect
piko.WithPath(path)Sets the cookie path.
piko.WithDomain(domain)Sets the cookie domain.
piko.WithInsecure()Drops the Secure flag.
piko.WithJavaScriptAccess()Drops the HttpOnly flag.
piko.WithSameSiteStrict()Sets SameSite=Strict.
piko.WithSameSiteNone()Sets SameSite=None (implies Secure).

Error handling

Call returns an error. The dispatch layer treats different error shapes differently.

Error shapeHTTP statusBehaviour
nil200Response body is the typed response, JSON-encoded.
piko.ValidationField(field, message)422Field-level validation error rendered into the form.
piko.NewValidationError(fields)422Multi-field validation error.
Any other error500Piko logs the error with full detail, and the client sees a generic failure message.

Internal errors are never exposed to the client verbatim. Wrap them with fmt.Errorf to preserve context for logs.

HTTP method override

func (a CustomerListAction) Method() piko.HTTPMethod { return piko.MethodGet }

The codegen detects the presence of Method() but ignores its return value (internal/annotator/annotator_domain/action_type_resolver.go:266-270). The generated registry always records POST. See How to override an action's HTTP method for the current behaviour and workarounds.

Discovery and routing

The generator scans actions/**/*.go at build time and registers every exported struct that embeds piko.ActionMetadata. The routing convention is:

action package     URL
-----------------  --------------------------------
actions/customer   /_piko/actions/customer.Upsert
actions/auth       /_piko/actions/auth.Login

The package name becomes the URL segment, and the action struct name (without the Action suffix) becomes the method name. Configure the _piko prefix through piko.WithActionServePath(...) in func main. Running the project generator (go run ./cmd/generator/main.go all) regenerates dispatch code after adding or renaming an action.

Client invocation

Generated TypeScript bindings expose every action through the global action namespace. Each action is a builder that takes its arguments and resolves through .call().

const result = await action.customer.Upsert({
  id:    42,
  name:  "Acme",
  email: "[email protected]",
}).call();

The argument object's keys mirror the Go parameter names (or struct field JSON tags). The builder returns a thenable. Awaiting .call() resolves to the typed response or throws on failure. The runtime auto-imports the bindings.

Batch endpoint

The runtime exposes a single batch endpoint at /_piko/actions/_batch (also under the configured ActionServePath) that runs multiple actions in one round-trip. The request body is JSON of shape { "actions": [ { "name": "<pkg>.<Method>", "args": { … } }, … ], "_csrf_ephemeral_token": "…" }. Limits and semantics:

  • Maximum 100 actions per batch; over-limit requests return HTTP 400.
  • The endpoint accepts an empty actions array and returns an empty results list.
  • Strategy is continue-all, report-failures: every action runs even when an earlier one fails.
  • The response is { "results": [ { "name", "data", "error", "code", "status" }, … ], "success": <bool> } where success is true only when every entry has a non-error status.
  • The endpoint validates the ephemeral CSRF token once for the entire batch.

The generated client uses this endpoint internally when callers request batching. Application code should invoke each action through the per-action builder instead of calling _batch directly.

See also

Used in: Scenario 002: contact form, Scenario 010: progress tracker, Scenario 016: cached API, Scenario 017: rate-limited API.

Integration tests: tests/integration/e2e_browser/actions_test.go, tests/integration/cache_rendering/.