Astro integration
Server-first forms with Astro Actions
Let Astro parse and validate the request while Lumen renders the same values, fields, and errors before JavaScript runs.
- Transport
- FormData
- Rendering
- Server
- Fallback
- Native POST
Define a form Action
Use accept: 'form'. The Action owns parsing, schema validation, authorization, and the write. Lumen does not wrap defineAction or require a resolver.
// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
updateProfile: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
name: z.string().min(2)
}),
handler: async (input, context) => {
// Authorize context.user before writing.
await saveProfile(input)
return { updated: true }
}
})
}// src/actions/index.ts
import { defineAction } from 'astro:actions'
import { z } from 'astro:schema'
export const server = {
updateProfile: defineAction({
accept: 'form',
input: z.object({
email: z.string().email(),
name: z.string().min(2)
}),
handler: async (input, context) => {
// Authorize context.user before writing.
await saveProfile(input)
return { updated: true }
}
})
}Render Action errors
Read the submitted result with Astro.getActionResult. normalizeAstroActionErrors turns ActionInputError fields into the shared ErrorSummary and FieldError contract.
---
import { actions, isInputError } from 'astro:actions'
import {
Button,
ErrorSummary,
Field,
FieldError,
Form,
Input,
Label,
normalizeAstroActionErrors
} from '@santi020k/lumen-astro'
const result = Astro.getActionResult(actions.updateProfile)
const errors = result?.error && isInputError(result.error)
? normalizeAstroActionErrors(result.error, {
email: 'profile-email',
name: 'profile-name'
})
: normalizeAstroActionErrors(result?.error)
const emailError = errors.fields.find(error => error.name === 'email')
---
<Form action={actions.updateProfile} method="POST" status={result?.error ? 'error' : 'idle'}>
<ErrorSummary errors={errors} />
<Field controlId="profile-email" describedBy="profile-email-error">
<Label for="profile-email">Email</Label>
<Input
aria-describedby={emailError ? 'profile-email-error' : undefined}
aria-invalid={emailError ? 'true' : undefined}
id="profile-email"
name="email"
required
type="email"
/>
<FieldError id="profile-email-error" message={emailError?.message} />
</Field>
<Button type="submit">Save profile</Button>
</Form>---
import { actions, isInputError } from 'astro:actions'
import {
Button,
ErrorSummary,
Field,
FieldError,
Form,
Input,
Label,
normalizeAstroActionErrors
} from '@santi020k/lumen-astro'
const result = Astro.getActionResult(actions.updateProfile)
const errors = result?.error && isInputError(result.error)
? normalizeAstroActionErrors(result.error, {
email: 'profile-email',
name: 'profile-name'
})
: normalizeAstroActionErrors(result?.error)
const emailError = errors.fields.find(error => error.name === 'email')
---
<Form action={actions.updateProfile} method="POST" status={result?.error ? 'error' : 'idle'}>
<ErrorSummary errors={errors} />
<Field controlId="profile-email" describedBy="profile-email-error">
<Label for="profile-email">Email</Label>
<Input
aria-describedby={emailError ? 'profile-email-error' : undefined}
aria-invalid={emailError ? 'true' : undefined}
id="profile-email"
name="email"
required
type="email"
/>
<FieldError id="profile-email-error" message={emailError?.message} />
</Field>
<Button type="submit">Save profile</Button>
</Form>Files
Keep multipart encoding explicit. Validate file type and size on the server even when the client supplies accept.
<Form
action={actions.uploadAvatar}
enctype="multipart/form-data"
method="POST"
>
<FileUpload accept="image/*" name="avatar" required />
<Button type="submit">Upload avatar</Button>
</Form><Form
action={actions.uploadAvatar}
enctype="multipart/form-data"
method="POST"
>
<FileUpload accept="image/*" name="avatar" required />
<Button type="submit">Upload avatar</Button>
</Form>Production checklist
Authorize inside the Action before reading or writing private data.
Rate-limit sensitive submissions and keep CSRF decisions in the application.
After success, redirect to a GET route to avoid accidental resubmission.
Re-render rejected values and errors on the server; enhancement is optional.
Focus ErrorSummary only after a failed submit, not on every blur.