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

Commit 1ca5352

Browse files
authored
Merge pull request #11 from pyreon/docs/update-zero-full
Update Zero docs with API routes, middleware, and testing
2 parents 88e2f02 + 69da1f0 commit 1ca5352

1 file changed

Lines changed: 188 additions & 0 deletions

File tree

content/docs/zero/index.mdx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,124 @@ import { varyEncoding } from "@pyreon/zero"
489489
varyEncoding()
490490
```
491491

492+
## API Routes
493+
494+
API routes are `.ts` files in `src/routes/api/` that export HTTP method handlers. They run on the server and return `Response` objects directly.
495+
496+
```ts title="src/routes/api/posts.ts"
497+
import type { ApiContext } from "@pyreon/zero"
498+
499+
export function GET(ctx: ApiContext) {
500+
return Response.json([
501+
{ id: 1, title: "Hello World" },
502+
])
503+
}
504+
505+
export async function POST(ctx: ApiContext) {
506+
const body = await ctx.request.json()
507+
return Response.json({ id: 2, ...body }, { status: 201 })
508+
}
509+
```
510+
511+
### File path conventions
512+
513+
| File | URL |
514+
|------|-----|
515+
| `src/routes/api/posts.ts` | `/api/posts` |
516+
| `src/routes/api/posts/index.ts` | `/api/posts` |
517+
| `src/routes/api/posts/[id].ts` | `/api/posts/:id` |
518+
| `src/routes/api/[...path].ts` | `/api/*` (catch-all) |
519+
520+
### ApiContext
521+
522+
| Property | Type | Description |
523+
|----------|------|-------------|
524+
| `request` | `Request` | The incoming HTTP request |
525+
| `url` | `URL` | Parsed URL |
526+
| `path` | `string` | URL path |
527+
| `params` | `Record<string, string>` | Dynamic route parameters |
528+
| `headers` | `Headers` | Request headers |
529+
530+
### Wiring API routes
531+
532+
```ts title="src/entry-server.ts"
533+
import { apiRoutes } from "virtual:zero/api-routes"
534+
535+
export default createServer({
536+
routes,
537+
apiRoutes, // API routes run before SSR — matched by URL + HTTP method
538+
middleware: [...],
539+
})
540+
```
541+
542+
Unsupported methods automatically return `405 Method Not Allowed` with an `Allow` header.
543+
544+
### CORS Middleware
545+
546+
```ts
547+
import { corsMiddleware } from "@pyreon/zero/cors"
548+
549+
// Allow any origin
550+
corsMiddleware()
551+
552+
// Specific origins with credentials
553+
corsMiddleware({
554+
origin: ["https://app.com", "https://admin.com"],
555+
credentials: true,
556+
maxAge: 86400,
557+
})
558+
559+
// Dynamic origin matching
560+
corsMiddleware({
561+
origin: (o) => o.endsWith(".example.com"),
562+
})
563+
```
564+
565+
| Option | Type | Default | Description |
566+
|--------|------|---------|-------------|
567+
| `origin` | `string \| string[] \| (origin: string) => boolean` | `"*"` | Allowed origins |
568+
| `methods` | `string[]` | `["GET","POST","PUT","PATCH","DELETE","OPTIONS"]` | Allowed methods |
569+
| `allowedHeaders` | `string[]` | `["Content-Type","Authorization"]` | Allowed request headers |
570+
| `exposedHeaders` | `string[]` | `[]` | Headers exposed to client |
571+
| `credentials` | `boolean` | `false` | Allow credentials |
572+
| `maxAge` | `number` | `86400` | Preflight cache (seconds) |
573+
574+
### Rate Limiting
575+
576+
```ts
577+
import { rateLimitMiddleware } from "@pyreon/zero/rate-limit"
578+
579+
// 100 requests per minute (default)
580+
rateLimitMiddleware()
581+
582+
// Strict API rate limiting
583+
rateLimitMiddleware({
584+
max: 20,
585+
window: 60,
586+
include: ["/api/*"],
587+
})
588+
```
589+
590+
| Option | Type | Default | Description |
591+
|--------|------|---------|-------------|
592+
| `max` | `number` | `100` | Max requests per window |
593+
| `window` | `number` | `60` | Window in seconds |
594+
| `keyFn` | `(ctx) => string` | IP-based | Client identifier function |
595+
| `include` | `string[]` | all paths | URL patterns to rate limit |
596+
| `exclude` | `string[]` | `[]` | URL patterns to skip |
597+
598+
Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers. Returns `429 Too Many Requests` with `Retry-After` when exceeded.
599+
600+
### Compression
601+
602+
```ts
603+
import { compressionMiddleware } from "@pyreon/zero/compression"
604+
605+
compressionMiddleware({ threshold: 1024, encodings: ["gzip"] })
606+
```
607+
608+
Compresses text-based responses (HTML, JSON, JS, CSS, XML, SVG) using the native `CompressionStream` API. Skips binary content and responses below the threshold.
609+
492610
## Server Actions
493611

494612
Define server-side mutations that are callable from the client. Actions receive parsed JSON or FormData and are mounted at `/_zero/actions/*`.
@@ -888,6 +1006,60 @@ startClient({
8881006

8891007
The client automatically detects whether to hydrate (if SSR-rendered HTML is present) or mount fresh (SPA mode).
8901008

1009+
## Testing Utilities
1010+
1011+
Test helpers for middleware and API routes, imported from `@pyreon/zero/testing`.
1012+
1013+
### Testing Middleware
1014+
1015+
```ts
1016+
import { testMiddleware } from "@pyreon/zero/testing"
1017+
import { corsMiddleware } from "@pyreon/zero/cors"
1018+
1019+
const { response, headers } = await testMiddleware(
1020+
corsMiddleware({ origin: "*" }),
1021+
"/api/posts"
1022+
)
1023+
expect(headers.get("Access-Control-Allow-Origin")).toBe("*")
1024+
```
1025+
1026+
### Testing API Routes
1027+
1028+
```ts
1029+
import { createTestApiServer } from "@pyreon/zero/testing"
1030+
1031+
const server = createTestApiServer([
1032+
{ pattern: "/api/posts", module: { GET: () => Response.json([]) } },
1033+
])
1034+
1035+
const res = await server.request("/api/posts")
1036+
expect(res.status).toBe(200)
1037+
1038+
const res2 = await server.request("/api/posts", {
1039+
method: "POST",
1040+
body: { title: "Hello" },
1041+
})
1042+
expect(res2.status).toBe(201)
1043+
```
1044+
1045+
### Mock Handlers
1046+
1047+
```ts
1048+
import { createMockHandler } from "@pyreon/zero/testing"
1049+
1050+
const handler = createMockHandler({ status: 200, body: { ok: true } })
1051+
// ... use in API route module
1052+
expect(handler.calls).toHaveLength(1)
1053+
expect(handler.calls[0].params).toEqual({ id: "123" })
1054+
```
1055+
1056+
| Export | Description |
1057+
|--------|-------------|
1058+
| `createTestContext(path, options)` | Create mock `MiddlewareContext` |
1059+
| `testMiddleware(mw, path, options)` | Run middleware, return response + headers |
1060+
| `createTestApiServer(routes)` | Test API routes via `server.request()` |
1061+
| `createMockHandler(config)` | Mock handler that records calls |
1062+
8911063
## Exports Summary
8921064

8931065
| Export | Signature | Description |
@@ -927,6 +1099,10 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
9271099
| `staticAdapter` | `() => Adapter` | Static output adapter |
9281100
| `defineAction` | `(handler: ActionHandler) => Action` | Define a server action |
9291101
| `createActionMiddleware` | `() => Middleware` | Mount action handler at `/_zero/actions/*` |
1102+
| `createApiMiddleware` | `(routes: ApiRouteEntry[]) => Middleware` | Mount API route handler |
1103+
| `corsMiddleware` | `(config?: CorsConfig) => Middleware` | CORS middleware |
1104+
| `rateLimitMiddleware` | `(config?: RateLimitConfig) => Middleware` | Rate limiting middleware |
1105+
| `compressionMiddleware` | `(config?: CompressionConfig) => Middleware` | Compression middleware |
9301106

9311107
## Subpath Exports
9321108

@@ -944,6 +1120,11 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
9441120
| `@pyreon/zero/theme` | Theme signals and `ThemeToggle` component |
9451121
| `@pyreon/zero/image-plugin` | `imagePlugin` Vite plugin |
9461122
| `@pyreon/zero/actions` | `defineAction`, `createActionMiddleware` |
1123+
| `@pyreon/zero/api-routes` | API route utilities and `createApiMiddleware` |
1124+
| `@pyreon/zero/cors` | `corsMiddleware` |
1125+
| `@pyreon/zero/rate-limit` | `rateLimitMiddleware` |
1126+
| `@pyreon/zero/compression` | `compressionMiddleware`, `compressResponse` |
1127+
| `@pyreon/zero/testing` | Test helpers for middleware and API routes |
9471128

9481129
## Type Exports
9491130

@@ -975,3 +1156,10 @@ The client automatically detects whether to hydrate (if SSR-rendered HTML is pre
9751156
| `Action` | Client-callable action returned by `defineAction` |
9761157
| `ActionHandler` | Server action handler function type |
9771158
| `RouteMiddlewareEntry` | Maps URL pattern to route middleware |
1159+
| `ApiContext` | Context passed to API route handlers |
1160+
| `ApiRouteEntry` | Maps URL pattern to API route module |
1161+
| `ApiRouteModule` | API route module with HTTP method handlers |
1162+
| `HttpMethod` | `"GET" \| "POST" \| "PUT" \| "PATCH" \| "DELETE" \| "HEAD" \| "OPTIONS"` |
1163+
| `CorsConfig` | Configuration for `corsMiddleware` |
1164+
| `RateLimitConfig` | Configuration for `rateLimitMiddleware` |
1165+
| `CompressionConfig` | Configuration for `compressionMiddleware` |

0 commit comments

Comments
 (0)