App and areas
Register resources, navigation, and request handlers.
Use defineApp to register resources in named areas. Each area requires label,
resources, and navigation. An area can also have an icon.
Resource names must be unique across all areas. A request uses the resource
name, view or action name, and ordered arguments. It does not use the area name.
import { defineApp } from '@backlit/sdk'
export default defineApp({
preferences: { title: 'Operations' },
areas: {
sales: {
label: 'Sales',
icon: 'shopping-cart',
resources: { orders: () => import('./orders.ts') },
navigation: [
{ label: 'Orders', resource: 'orders', view: 'list' },
{
label: 'Reports',
items: [
{
label: 'Summary',
resource: 'orders',
view: 'summary',
icon: 'chart-line',
},
],
},
],
},
},
})
Keep resource module imports in loader functions. The app loads a resource when
a request needs it. Use the same key in the resource map and defineResource.
A navigation entry must name a registered resource. A group has a label and an
array of entries. Groups cannot contain other groups.
Manifest and resource access
app.toManifest() returns preferences, areas, navigation, and extension identities.
app.resourceNames() returns registered names. Neither method loads resources.
await app.loadResource(name) loads one resource and returns its default export.
preferences accepts title, logo, logoDark, logoVariant, and logoHeight. See the generated
AppConfig type for the complete configuration.
Requests
These methods return protocol responses:
| Method | Arguments after the method name |
|---|---|
renderView |
resource, view, args?, transport?, search? |
renderZone |
resource, view, zone, args?, transport?, search? |
handleAction |
resource, action, payload, args?, transport? |
handleLookup |
resource, lookup, searchQuery, args?, transport? |
Arguments are strings in parameter order. The argument count must match the
definition exactly. An unknown resource or endpoint returns a 404 response.
A missing zone returns E_ZONE_NOT_FOUND.
The app does not start an HTTP server. A host adapter maps requests to these
methods. Use Request handling for middleware and transport.
Use Search state for the search argument.
Extensions
Pass server extensions in extensions. They run before resource requests.
Pass lazy middleware imports in middleware. Middleware runs in registration
order for view, zone, action, and lookup requests.
See Extensions and Agent tools.
Complete app example
This example has a view, a zone, a lookup, an action, and middleware. The action returns a notification and does not write to a database. The test suite calls the app methods for valid requests, validation failures, and missing addresses.
import {
callout,
defineResource,
fields,
zone,
} from '@backlit/sdk'
import { z } from 'zod'
const orders = defineResource('orders', [
fields.text('id', { label: 'ID' }),
fields.text('title', { label: 'Title' }),
])
const rows = [{ id: '42', title: 'Notebook' }]
orders.view('list', () =>
zone('summary', () => callout(`${rows.length} order`))
)
orders
.view('detail', ['id'], (ctx) =>
callout(`Order ${ctx.paramValues.id}`)
)
.setAgentOptions({ description: 'Open an order by ID.' })
orders
.lookup('search', (ctx, lookup) =>
lookup
.setFields(orders.pick('id', 'title'))
.withData(
rows.filter((row) =>
row.title.includes(ctx.searchQuery)
)
)
)
.setAgentOptions({
description: 'Find orders.',
search: true,
})
orders.action(
'rename',
['id'],
z.object({ title: z.string().min(1) }),
(ctx) => {
if (ctx.paramValues.id !== '42')
return ctx.error(404, 'E_ORDER_NOT_FOUND')
return ctx.notify(`Accepted: ${ctx.data.title}`)
}
)
export default ordersimport { defineApp } from '@backlit/sdk'
export const app = defineApp({
preferences: { title: 'Orders' },
middleware: [() => import('./maintenance.ts')],
areas: {
sales: {
label: 'Sales',
resources: { orders: () => import('./orders.ts') },
navigation: [
{
label: 'Orders',
resource: 'orders',
view: 'list',
},
],
},
},
})import type { Middleware } from '@backlit/sdk/types'
const maintenance: Middleware = async (
ctx,
request,
next
) => {
if (request?.headers()['x-maintenance'] === 'true') {
return ctx.error(503, 'E_MAINTENANCE')
}
await next()
return undefined
}
export default maintenance{
"kind": "success",
"status": 200,
"node": {
"kind": "view",
"resource": "orders",
"name": "list",
"slots": {
"content": [
{
"kind": "zone",
"name": "summary",
"resource": "orders",
"view": "list",
"slots": {
"content": [
{
"kind": "callout",
"text": "1 order"
}
]
}
}
]
}
}
}