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.