Skip to content
Backlit
Esc
navigateopen⌘Jpreview
On this page

Action

Validate an operation and return its next interface state.

An action owns one operation on a resource. It validates every payload before its handler runs. A form, an agent, or another client can call the same action and get the same validation and response.

Register an action once and keep its definition. Forms use the definition as their typed submission target.

API

resource.action(name, schema, handler): ActionDefinition

resource.action(
  name,
  params: string[],
  schema,
  handler
): ActionDefinition

The schema can use any Standard Schema validator. These examples use Zod.

Definition member Result
resource Returns the resource name.
name Returns the action name.
params Returns the ordered parameter names.
schema Returns the validation schema.
getAddress(args?) Creates a complete action address.
validate(input) Returns the schema result without running the handler.
execute(ctx) Validates and runs the handler. Expected failures throw.
attempt(ctx) Validates and runs the handler. Expected failures become protocol responses.

The Backlit app calls attempt through its request pipeline. Application code usually registers the action and uses the handler context.

Basic action

Return nothing from the handler for a successful operation with no additional interface change.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'create',
  z.object({ customerName: z.string() }),
  () => {}
)

export default orders
{
  "kind": "success",
  "status": 200
}

Validated data

The handler reads the schema output from ctx.data. It does not read the raw payload. This example trims the submitted name before the handler uses it.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'create',
  z.object({
    customerName: z
      .string()
      .transform((name) => name.trim()),
  }),
  (ctx) => ctx.notify(`Created ${ctx.data.customerName}.`)
)

export default orders
{
  "kind": "success",
  "status": 200,
  "notifications": [
    {
      "text": "Created Ada."
    }
  ]
}

Parameters

Declare address parameters in order. Read their values from ctx.paramValues. The payload remains available separately through ctx.data.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'update',
  ['id'],
  z.object({ customerName: z.string() }),
  (ctx) =>
    ctx.notify(`Updated order ${ctx.paramValues.id}.`)
)

export default orders
{
  "kind": "success",
  "status": 200,
  "notifications": [
    {
      "text": "Updated order 42."
    }
  ]
}

Call action.getAddress(args) to create the complete resource, action, string arguments, and key. A missing argument causes an error.

Notifications

Call ctx.notify to report success without a navigation change. Notifications stay in call order. A notification can use text alone or add a title and tone.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'save',
  z.object({ note: z.string() }),
  (ctx) =>
    ctx.notify('Draft saved.').notify({
      text: 'The note is visible to the team.',
      tone: 'info',
    })
)

export default orders
{
  "kind": "success",
  "status": 200,
  "notifications": [
    {
      "text": "Draft saved."
    },
    {
      "text": "The note is visible to the team.",
      "tone": "info"
    }
  ]
}

Redirect

Call ctx.redirectTo with the target view definition. For a view with parameters, add one argument for each parameter in declaration order. TypeScript checks the target and argument count. A redirect can also carry notifications.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])
const orderDetail = orders.view('detail', ['id'], () => [])

orders.action(
  'create',
  z.object({ customerName: z.string() }),
  (ctx) =>
    ctx
      .redirectTo(orderDetail, [42])
      .notify('Order created.')
)

export default orders
{
  "kind": "redirect",
  "status": 302,
  "target": {
    "kind": "view",
    "resource": "orders",
    "view": "detail",
    "args": [
      "42"
    ]
  },
  "notifications": [
    {
      "text": "Order created."
    }
  ]
}

Refresh the page

Call ctx.refresh() to request a refresh of the current page.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'archive',
  z.object({ orderId: z.string() }),
  (ctx) => ctx.refresh()
)

export default orders
{
  "kind": "refresh",
  "status": 200
}

Refresh zones

Pass an array of zone names to ctx.refreshZones to refresh only those parts of the current page. A refresh can also carry notifications.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'recalculate',
  z.object({ orderId: z.string() }),
  (ctx) =>
    ctx
      .refreshZones(['basket', 'totals'])
      .notify('Order totals recalculated.')
)

export default orders
{
  "kind": "refresh",
  "status": 200,
  "zones": [
    "basket",
    "totals"
  ],
  "notifications": [
    {
      "text": "Order totals recalculated."
    }
  ]
}

Schema failure

If the schema rejects the payload, Backlit does not run the handler. It returns each issue as a validation error. Nested schema paths become dot-separated protocol paths.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'create',
  z.object({
    customerName: z
      .string()
      .min(1, 'Customer name is required.'),
  }),
  () => {}
)

export default orders
{
  "kind": "error",
  "status": 422,
  "code": "E_ACTION_VALIDATION_FAILED",
  "errors": [
    {
      "path": "customerName",
      "message": "Customer name is required."
    }
  ]
}

Field failure

Call ctx.fail with a key-to-message object when the handler rejects one or more submitted fields. Backlit returns an action failure with field paths.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'create',
  z.object({ email: z.email() }),
  (ctx) =>
    ctx.fail({
      email: 'An account uses this email.',
    })
)

export default orders
{
  "kind": "error",
  "status": 422,
  "code": "E_ACTION_FAILED",
  "errors": [
    {
      "path": "email",
      "message": "An account uses this email."
    }
  ]
}

Payload failure

Call ctx.fail with text when the failure applies to the complete payload. Use the optional code for a stable application error code.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'pay',
  z.object({ card: z.string() }),
  (ctx) =>
    ctx.fail('Payment was declined.', {
      code: 'E_PAYMENT_DECLINED',
    })
)

export default orders
{
  "kind": "error",
  "status": 422,
  "code": "E_PAYMENT_DECLINED",
  "errors": [
    {
      "message": "Payment was declined."
    }
  ]
}

Expected request error

Call ctx.error when the request cannot continue for a reason that is not a validation failure. Set the HTTP status and a stable error code. The response can include errors and notifications.

import { defineResource } from '@backlit/sdk'
import { z } from 'zod'

const orders = defineResource('orders', [])

orders.action(
  'cancel',
  z.object({ orderId: z.string() }),
  (ctx) =>
    ctx
      .error(403, 'E_ORDER_LOCKED')
      .setErrors([
        {
          message: 'This order can no longer be cancelled.',
        },
      ])
      .notify({
        text: 'No changes were made.',
        tone: 'warning',
      })
)

export default orders
{
  "kind": "error",
  "status": 403,
  "code": "E_ORDER_LOCKED",
  "errors": [
    {
      "message": "This order can no longer be cancelled."
    }
  ],
  "notifications": [
    {
      "text": "No changes were made.",
      "tone": "warning"
    }
  ]
}

An unexpected exception continues to throw. Backlit does not convert it into an action failure.

Was this page helpful?