Form action
A form that adds a Pokémon to a team of up to six. The server side is one Elysia 2 app behind the Rust server; the form is a native component.
1Turn actions on
Actions are opt-in. Install the two pinned packages and add an [actions] table; every key in it is optional.
$ bun add [email protected] @elysia/[email protected][actions]2Write the action
The entry default-exports an Elysia app built with the brust() adapter. In Elysia 2 the argument order is (path, hook, handler): the schema comes before the handler. A body that fails the schema answers 422; a returned problem(409, ...) is typed on the client.
import { brust } from '@brust/core/elysia'
import { Elysia, problem, t } from 'elysia'
const team: number[] = []
const MAX = 6
const app = new Elysia({ adapter: brust() })
.get('/team', () => ({ team, max: MAX }))
.post('/team', { body: t.Object({ id: t.Integer({ minimum: 1 }) }) }, ({ body }) => {
if (team.length >= MAX) return problem(409, { title: 'TeamFull', max: MAX })
if (!team.includes(body.id)) team.push(body.id)
return { team, max: MAX }
})
export default app
export type App = typeof app3Call it from a form
client<App>() is Eden Treaty wired to the actions mount. The import type is erased, so the browser bundle carries no Elysia. This form has state and handlers but nothing the native tier cannot express, so it compiles to a native component.
import { client } from '@brust/core/client'
import type { App } from '../actions'
export const api = client<App>()import { useState } from 'react'
import { api } from '../lib/api'
export default function AddToTeam() {
const [id, setId] = useState('25')
const [msg, setMsg] = useState('')
const submit = async (e: { preventDefault(): void }) => {
e.preventDefault()
const r = await api.team.post({ id: Number(id) })
if (r.error) setMsg(r.error.status === 409 ? 'The team is full' : `Failed with ${r.error.status}`)
else setMsg(`Team: ${r.data.team.length}/${r.data.max}`)
}
return (
<form onSubmit={submit}>
<input name="id" value={id} onChange={(e) => setId(e.target.value)} />
<button type="submit">Add to team</button>
<p>{msg}</p>
</form>
)
}4Try it
The same endpoint with curl: a valid body, then one that fails validation.
$ curl -si -X POST http://127.0.0.1:1337/_brust/action/team -H 'content-type: application/json' -d '{"id":25}'
HTTP/1.1 200 OK
{"team":[25],"max":6}
$ curl -si -X POST http://127.0.0.1:1337/_brust/action/team -H 'content-type: application/json' -d '{"id":0}'
HTTP/1.1 422 Unprocessable Entity
content-type: application/problem+json
{"type":"validation","code":"validation","title":"Validation Error","status":422,"on":"body","property":"/id"}Actions are public endpoints. brust rejects cross-site non-GET requests by default (403), and nothing more: authentication is your app’s job. The team array above lives in one worker’s memory, so keep real state in a database. See Actions.