Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit f1473fa

Browse files
authored
Merge pull request #8 from pyreon/docs/update-zero-docs
Update Zero docs with server actions and per-route middleware
2 parents ccbf5ec + 62dad29 commit f1473fa

1 file changed

Lines changed: 111 additions & 1 deletion

File tree

content/docs/zero/index.mdx

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ description: Full-stack meta-framework for Pyreon applications.
1818
Scaffold a new project with the `create` command:
1919

2020
```bash
21-
bun create zero my-app
21+
bun create @pyreon/zero my-app
2222
cd my-app
2323
bun install
2424
bun run dev
@@ -489,6 +489,108 @@ import { varyEncoding } from "@pyreon/zero"
489489
varyEncoding()
490490
```
491491

492+
## Server Actions
493+
494+
Define server-side mutations that are callable from the client. Actions receive parsed JSON or FormData and are mounted at `/_zero/actions/*`.
495+
496+
```ts title="src/features/posts.ts"
497+
import { defineAction } from "@pyreon/zero/actions"
498+
499+
export const createPost = defineAction(async (ctx) => {
500+
const { title, body } = ctx.json as { title: string; body: string }
501+
const post = await db.posts.create({ title, body })
502+
return { success: true, id: post.id }
503+
})
504+
505+
export const deletePost = defineAction(async (ctx) => {
506+
const { id } = ctx.json as { id: number }
507+
await db.posts.delete(id)
508+
return { success: true }
509+
})
510+
```
511+
512+
Call actions from components — they're just async functions:
513+
514+
```tsx
515+
import { createPost } from "../features/posts"
516+
517+
function NewPostForm() {
518+
const handleSubmit = async (e: Event) => {
519+
e.preventDefault()
520+
const result = await createPost({ title: "Hello", body: "World" })
521+
if (result.success) window.location.href = `/posts/${result.id}`
522+
}
523+
524+
return <form onSubmit={handleSubmit}>...</form>
525+
}
526+
```
527+
528+
### ActionContext
529+
530+
| Property | Type | Description |
531+
|----------|------|-------------|
532+
| `request` | `Request` | The original HTTP request |
533+
| `json` | `unknown` | Parsed JSON body (for `application/json`) |
534+
| `formData` | `FormData \| null` | Parsed form data (for `multipart/form-data`) |
535+
| `headers` | `Headers` | Request headers |
536+
537+
### Action Middleware
538+
539+
Mount the action handler in your server entry:
540+
541+
```ts title="src/entry-server.ts"
542+
import { createActionMiddleware } from "@pyreon/zero/actions"
543+
544+
export default createServer({
545+
routes,
546+
middleware: [
547+
createActionMiddleware(), // handles /_zero/actions/* requests
548+
securityHeaders(),
549+
cacheMiddleware(),
550+
],
551+
})
552+
```
553+
554+
## Per-Route Middleware
555+
556+
Route files can export a `middleware` function that runs on the server before rendering. Middleware uses `@pyreon/server`'s signature:
557+
558+
```tsx title="src/routes/(admin)/dashboard.tsx"
559+
import type { MiddlewareContext } from "@pyreon/server"
560+
561+
// Runs on every request to /dashboard
562+
export const middleware = (ctx: MiddlewareContext) => {
563+
const token = ctx.req.headers.get("authorization")
564+
if (!token) {
565+
return new Response("Unauthorized", { status: 401 })
566+
}
567+
// Return void to continue to rendering
568+
}
569+
```
570+
571+
Wire route middleware in your server entry:
572+
573+
```ts title="src/entry-server.ts"
574+
import { routes } from "virtual:zero/routes"
575+
import { routeMiddleware } from "virtual:zero/route-middleware"
576+
import { createServer } from "@pyreon/zero"
577+
578+
export default createServer({
579+
routes,
580+
routeMiddleware, // per-route middleware dispatched before global middleware
581+
middleware: [securityHeaders(), cacheMiddleware()],
582+
})
583+
```
584+
585+
Add the virtual module type to your `env.d.ts`:
586+
587+
```ts title="env.d.ts"
588+
declare module "virtual:zero/route-middleware" {
589+
import type { RouteMiddlewareEntry } from "@pyreon/zero"
590+
export const routeMiddleware: RouteMiddlewareEntry[]
591+
}
592+
```
593+
492594
## SEO
493595

494596
### Sitemap Generation
@@ -763,6 +865,7 @@ import { createServer } from "@pyreon/zero"
763865

764866
const handler = createServer({
765867
routes,
868+
routeMiddleware, // Per-route middleware from virtual:zero/route-middleware
766869
config: { mode: "ssr" },
767870
middleware: [securityHeaders(), cacheMiddleware()],
768871
template: indexHtml, // HTML template string
@@ -822,6 +925,8 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
822925
| `nodeAdapter` | `() => Adapter` | Node.js adapter |
823926
| `bunAdapter` | `() => Adapter` | Bun adapter |
824927
| `staticAdapter` | `() => Adapter` | Static output adapter |
928+
| `defineAction` | `(handler: ActionHandler) => Action` | Define a server action |
929+
| `createActionMiddleware` | `() => Middleware` | Mount action handler at `/_zero/actions/*` |
825930

826931
## Subpath Exports
827932

@@ -838,6 +943,7 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
838943
| `@pyreon/zero/seo` | `seoPlugin`, `seoMiddleware`, `generateSitemap`, `generateRobots`, `jsonLd` |
839944
| `@pyreon/zero/theme` | Theme signals and `ThemeToggle` component |
840945
| `@pyreon/zero/image-plugin` | `imagePlugin` Vite plugin |
946+
| `@pyreon/zero/actions` | `defineAction`, `createActionMiddleware` |
841947

842948
## Type Exports
843949

@@ -865,3 +971,7 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
865971
| `FormatSource` | Per-format srcset in a `ProcessedImage` |
866972
| `ImageFormat` | `"webp" \| "avif" \| "jpeg" \| "png"` |
867973
| `JsonLdType` | JSON-LD structured data type |
974+
| `ActionContext` | Context passed to server action handlers |
975+
| `Action` | Client-callable action returned by `defineAction` |
976+
| `ActionHandler` | Server action handler function type |
977+
| `RouteMiddlewareEntry` | Maps URL pattern to route middleware |

0 commit comments

Comments
 (0)