# ovr Progressive HTML Rendering ## Introduction Designed to optimize [Time-To-First-Byte](https://web.dev/articles/ttfb#what_is_ttfb), ovr evaluates components in parallel and streams HTML in order by producing an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) of HTML that feeds directly into the streamed response. For the following component: ```tsx function Component() { return

hello world

; } ``` ovr generates three `Chunk`s of HTML: ```ts "

"; // streamed immediately "hello world"; // next "

"; // last ``` ## Asynchronous streaming While this streaming is trivial for a paragraph, consider when a component is asynchronous: ```tsx async function Username() { const user = await getUser(); // slow... return {user.name}; } function Component() { return (

hello

); } ``` Instead of waiting for `Username` to resolve before sending the entire `Component`, ovr will send what it has immediately and stream the rest as it becomes available. ```ts "

"; "hello "; // streamed immediately // for await (const chunk of Username()) { ... ""; "username"; ""; "

"; ``` ## Render how browsers read Web browsers are [built for streaming](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/How_browsers_work#parsing), they parse and paint HTML as it arrives. [Most critically, the head](https://web.dev/learn/performance/understanding-the-critical-path#what_resources_are_on_the_critical_rendering_path) of the document can be sent immediately to start the requests for linked assets (JavaScript, CSS, etc.) and start parsing before the HTML has finished streaming. ovr's architecture gives you streaming server-side rendering out of the box. No hydration bundle, no buffering---just HTML delivered _in order_, as soon as it's ready. # Get Started Getting started with ovr ## Installation Install the `ovr` package from npm using your preferred package manager. ```bash npm i ovr ``` Alternatively, you can setup ovr with a pre-configured template using [Vite with domco](https://domco.robino.dev). This includes live reload and options for Tailwind, deployment adapters, and more. domco will create a server entry point in `src/server/+app.tsx` with ovr pre-configured. ```bash npx create-domco@latest --framework=ovr ``` ## JSX To utilize JSX, add the following options to your `tsconfig.json` to enable the JSX transform. TypeScript, Vite, or esbuild will pickup the option from this file. ```json { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "ovr" } } ``` Or you can use a comment if you are using ovr in conjunction with another framework to specify the import source for a specific module where you are using ovr. ```tsx /** @jsx jsx */ /** @jsxImportSource ovr */ ``` ## Compatibility ovr can be used in any Fetch API compatible runtime via [`app.fetch`](/03-app#fetch). Here are a few ways to create a Fetch based HTTP server in various JavaScript runtimes. - [domco](https://domco.robino.dev) - run `npm create domco` and select `ovr` framework - [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plugin/get-started/) + [vite-ssr-components](https://github.com/yusukebe/vite-ssr-components) - [Node + srvx](https://srvx.h3.dev/) - [Bun HTTP server](https://bun.sh/docs/api/http) - [Deno HTTP server](https://docs.deno.com/runtime/fundamentals/http_server/) For example, using `srvx` you can plug `app.fetch` into the `serve` [options](https://srvx.h3.dev/guide/handler). ```tsx // src/index.tsx import { App } from "ovr"; import { serve } from "srvx"; const app = new App(); app.get("/", () =>

Hello World

); serve({ fetch: app.fetch }); ``` Then can compile `tsx` into `js` with TypeScript, and run the server with Node. ```bash tsc && node dist/index.js ``` # Components Async generator JSX While components in ovr are authored like components in frameworks like [React](https://react.dev/), there two main distinctions between ovr and other frameworks: 1. ovr only runs on the _server_, there is no client runtime. 2. JSX evaluates to an `AsyncGenerator` that yields escaped `Chunk`s of HTML. ## Basic If you aren't familiar with components, they are functions that return JSX elements. You can use them to declaratively describe and reuse parts of your HTML. Define a `props` object as a parameter for a component to pass arguments to it. ```tsx import type { JSX } from "ovr"; function Component(props: { children?: JSX.Element; color: string }) { return
{props.children}
; } ``` Now the component can be used as it's own tag within other components. ```tsx function Page() { return (

Hello world

Children
); } ``` Props are passed in as attributes, while `children` is a special prop that is used to reference the element(s) in between the opening and closing tags. ovr uses aligns with the standard (all lowercase) HTML attributes---attributes will be rendered exactly as they are written. If you're coming from React, use `class` and `for` instead of `className` and `htmlFor` respectively. There is also no need to provide a `key` attribute in when rendering lists. ## Async Components can be asynchronous, for example you can `fetch` directly in a component. ```tsx async function Data() { const res = await fetch("..."); const data = await res.text(); return
{data}
; } ``` ## Generators Components can also be generators for more fine grained control and [memory optimization](/demo/memory). When utilizing generators `yield` values instead of `return`. ```tsx async function* Generator() { yield

start

; // streamed immediately await promise; yield

after

; } ``` ## Parallelization These three components await in [parallel](/demo/parallel) when this component is evaluated. Then they will stream in order as soon as they are ready. ```tsx function Page() { return (
); } ``` The order of your components does not affect when they are evaluated, but it does impact when they will display. If `Username` is the slowest component, `Generator` and `Data` will be queued but only streamed after `Username` completes. ## Return Types You can `return` or `yield` most data types from a component, they will be rendered as you might expect: ```tsx function* DataTypes() { yield null; // "" yield undefined; // "" yield false; // "" yield true; // "" yield "string"; // "string" yield 0; // "0"; yield BigInt(9007199254740991); // "9007199254740991" yield

jsx

; // "

jsx

" yield ["any-", "iterable", 1, null]; // "any-iterable1" yield () => "function"; // "function" yield async () => "async"; // "async" } ``` > Check out the [source code](https://github.com/rossrobino/ovr/blob/main/packages/ovr/src/jsx/index.ts) for the `toGenerator` function to understand how ovr evaluates each data type. ## Raw HTML To render HTML directly without escaping, create a new `Chunk` with the second argument `safe` set to `true`. ```tsx import { Chunk } from "ovr"; const html = "

Safe to render

"; function Component() { return
{new Chunk(html, true)}
; } ``` ## Running components To evaluate components (for example, if you aren't using `App` or need to call them separately), you can use these functions. ### toGenerator Convert any `JSX.Element` into `AsyncGenerator` with `toGenerator`. ```tsx import { toGenerator } from "ovr"; const Component = () =>

element

; const gen = toGenerator(Component); for await (const chunk of gen) { // ... } ``` ### toStream Turn a `JSX.Element` into a `ReadableStream`, this pipes the result of `toGenerator` into a `ReadableStream`. ```tsx import { toStream } from "ovr"; const Component = () =>

element

; const stream = toStream(Component); const response = new Response(stream, { "Content-Type": "text/html; charset=utf-8", }); ``` ### toString Convert any `JSX.Element` into a `string` of HTML with `toString`. This runs `toGenerator` joins the results into a single string. ```tsx import { toString } from "ovr"; const Component = () =>

element

; const str = await toString(Component); ``` # App Creating an application with ovr. To create a web server with ovr, initialize a new `App` instance: ```ts import { App } from "ovr"; const app = new App(); ``` The `App` API is inspired by and works similar to frameworks such as [Hono](https://hono.dev/) and [Express](https://expressjs.com/). ## Configuration The following values can be customized after creating the `App`. You can also configure most of these per route within middleware by modifying the value on the `Context`. ### Trailing Slash ovr handles [trailing slash](https://bjornlu.com/blog/trailing-slash-for-frameworks) redirects automatically, you can customize the redirect preference. ```ts app.trailingSlash = "never"; ``` ### Not Found Customize the not found response handler. ```ts app.notFound = (c) => c.html("Not found", 404); ``` ### Error Handler Add an error handler, by default errors are thrown. ```ts app.error = (c, error) => { console.error(error); c.html("An error occurred", 500); }; ``` ### Base HTML Change the base HTML to inject elements into with the [`Context.head` and `Context.page`](/05-context#page-builders) methods, this is the default. ```ts app.base = ''; ``` ## Response At the most basic level, you can create a route and return a `Response` from the middleware to handle a request. ```ts app.get("/", () => new Response("Hello world")); ``` You can also return a `ReadableStream` to use as the `Response.body`. ## JSX Returning JSX from middleware will generate an HTML streamed response. ```tsx app.get("/", () =>

Hello world

); ``` The element will be injected into the `` element of the [`base`](/03-app#base-html) HTML. ## HTTP methods `app.get` and `app.post` create handlers for the HTTP methods respectively. You can add other or custom methods with `app.on`. ```ts // Other or custom methods app.on("METHOD", "/pattern", () => { // ... }); ``` ## Multiple patterns Add the same middleware to multiple patterns. ```ts app.get(["/multi/:param", "/pattern/:another"], (c) => { c.params; // { param: string } | { another: string } }); ``` ## Middleware When multiple middleware handlers are added to a route, the first middleware will be called, and the `next` middleware can be dispatched within the first by using `await next()`. Middleware is based on [koa-compose](https://github.com/koajs/compose). ```ts app.get( "/multi", async (c, next) => { console.log("1"); await next(); // dispatches the next middleware below console.log("3"); }, (c) => { console.log("2"); }, ); ``` The same [`Context`](/04-context) is passed into each middleware. After all the middleware have been run, the `Context` will `build` and return the final `Response`. ### Global Middleware Add global middleware that runs in front of every request with `app.use`. ```ts app.use(async (c, next) => { // ... await next(); // ... }); ``` ## Fetch Use the `fetch` method to create a `Response`, this is the `Request` handler for your application. ```ts const response = await app.fetch(new Request("https://example.com/")); ``` # Helpers ovr route helpers. ovr provides helpers to encapsulate a route, allowing you to easily create a route in a separate module from `App`. Helpers are the best way to create pages, API endpoints, links, and forms in an ovr application. ## Get `Get` creates a [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET) route and corresponding `Anchor`, `Button`, and `Form` components for it. This ensures if you change the route's pattern, you don't need to update all of the links to it throughout your application. Anytime you need to generate a link to a page use the `Anchor` component from the `Get` helper. ```tsx import { Get } from "ovr"; const page = new Get("/", () => { return (
{/* */} Home {/*
); }); ``` ## Post There is also a `Post` helper that will create a [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST) handler and corresponding `Form` and `Button` elements. Anytime you need to handle a form submission, use the generated `Form` component from the `Post` helper. For `Post`, ovr will automatically generate a unique pathname for the route based on a hash of the middleware provided. ```tsx import { Get, Post } from "ovr"; const login = new Post(async (c) => { const data = await c.req.formData(); // ... c.redirect("/", 303); }); const page = new Get("/", () => { return (
{/* */} ... {/*
); }); ``` You can set the pattern manually if you need a stable pattern or if you are using parameters. ```tsx const custom = new Post("/custom/:pattern", (c) => { // ... }); ``` ## Props Components created via helpers have the following props available: - `params` - if the route's pattern has parameters, they must be passed as a prop to properly construct the URL. - `search` - [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams) to append to the URL. - If `true` the current request's `url.search` will be used (this option can only be used in the context of a request). - Other values are passed into `URLSearchParams` constructor to create the query string. - `hash` - [fragment hash](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) appended with a `#` at the end of the URL. ```tsx import { Get } from "ovr"; const page = new Get("/hello/:name", () => { return ( // ... ); }); ``` ## Add Use the `add` method to register a helper to your app. ```tsx app.add(page); // single app.add(page, login); // multiple app.add({ page, login }); // object app.add([page, login]); // array // any combination of these also works ``` This makes it easy to create a module of helpers, ```tsx // home.tsx import { Get, Post } from "ovr"; export const page = new Get("/", (c) => { // ... }); export const login = new Post((c) => { // ... }); ``` and then add them all at once: ```tsx // app.tsx import * as home from "./home"; app.add(home); // adds all exports ``` ## Properties Given the following `Get` helper, a variety of other properties are available to use in addition to the components. ```tsx const page = new Get("/hello/:name", (c) =>

Hello {c.params.name}

); ``` ### Middleware All `Middleware` added to the route. ```ts page.middleware; // Middleware[] ``` ### Params `Params` is a type helper to get the specific params of the route based on the pattern. ```ts typeof page.Params; // { name: string } ``` ### Pattern The route pattern. ```ts page.pattern; // "/hello/:name" ``` ### Pathname The `pathname` method inserts params into the pattern. It provides type safety to ensure you always pass the correct params (or no params) to create the pathname. In this case, given the pattern `/hello/:name`, the `name` property must be passed in on the `params` object. ```ts page.pathname({ name: "world" }); // `/hello/${string}` page.pathname({ name: "world" } as const); // "/hello/world" ``` Using incorrect params results in a type error: ```ts page.pathname({ id: "world" }); // Error: 'id' does not exist in type '{ name: string; }' ``` You can create a list of exact pathnames for given params. ```ts const params = [ { name: "world" }, { name: "ross" }, ] as const satisfies (typeof page.Params)[]; const pathnames = params.map((p) => page.pathname(p)); // ("/hello/world" | "/hello/ross")[] ``` ### Relative URL The `url` method creates a _relative_ URL (without the `origin`) for the route. This method is similar to `pathname`, but also provides the ability to also pass [`search`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) and [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) options to create the URL. ```ts // /hello/world?search=param#hash const relativeUrl = page.url({ params: { name: "world" }, search: { search: "param" }, hash: "hash", }); const absoluteUrl = new URL(relativeUrl, "https://example.com"); absoluteUrl.href; // https://example.com/hello/world?search=param#hash ``` # Context Understanding the ovr request context. `Context` contains context for the current request and helpers to build a `Response`. ## Request information Access information about the current request such as the [`url`](https://developer.mozilla.org/en-US/docs/Web/API/URL) or [`params`](/06-routing#parameters). ```ts app.get("/api/:id", (c) => { c.req; // original `Request` c.url; // parsed web `URL` c.params; // type-safe route parameters { id: "123" } c.route; // matched `Route` (contains `pattern`, `store`) }); ``` ## Response builders The context contains methods to build your final `Response`. ### Content types Easily set the response with common [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type) headers. ```tsx app.get("/api/:id", (c) => { c.html(body, status); // HTML c.text(body, status); // plain text c.json(data, status); // JSON c.redirect(location, status); // redirect c.res(body, init); // generic response (like `new Response()`) }); ``` ### Page builders There are also JSX page building methods which leverage streaming JSX. ```tsx app.get("/api/:id", (c) => { // inject elements into c.head(); // wrap page content with layout components c.layout(Layout); // stream JSX page (same as returning JSX) c.page(); }); ``` ## Utilities ### Memo Memoize a function to dedupe async operations and cache the results. See [memoization](/07-memo) for more details. ```tsx app.get("/api/:id", (c) => { c.memo(fn); }); ``` ### Entity tag Generates an [entity tag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) from a hash of the string provided. If the tag matches, the response will be set to [`304: Not Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/304) and the funciton will return `true`. ```tsx app.get("/api/:id", (c) => { const html = `

Content

`; const matched = c.etag(html); if (matched) { // 304: Not Modified return; } return c.html(html); }); ``` ## Get Context can be acquired from anywhere within the scope of a request handler with the `Context.get` method. `get` uses [`AsyncLocalStorage`](https://blog.robino.dev/posts/async-local-storage) to accomplish this and ensure the correct context is only accessed in the scope of the current request. This prevents you from having to prop drill the context to each component from the handler. ```tsx import { Context } from "ovr"; function Component() { const c = Context.get(); // current request context } ``` # Routing Trie-based routing in ovr. ovr's router offers efficient and fast route matching, supporting static paths, parameters, and wildcards. Utilizing a [trie](https://en.wikipedia.org/wiki/Radix_tree) structure means that performance does not degrade as you add more routes. The router is forked and adapted from [memoirist](https://github.com/SaltyAom/memoirist) and [@medley/router](https://github.com/medleyjs/router). ## Parameters Create a parameter for a route using the colon before a path segment. `/api/:id` will create a `params` object on the `Context` with a property of `id` containing the actual path segment requested. ```ts app.post("/api/:id", (c) => { // matches "/api/123" c.params; // { id: "123" } }); ``` ## Wildcard Use an asterisk `*` to match all remaining segments in the route. ```ts app.get("/files/*", (c) => { c.params["*"]; // matched wildcard path (ex: "images/logo.png") }); ``` ## Prioritization Routes are prioritized in this order: **Static > Parametric > Wildcard** Given three routes are added in any order: ```ts trie.add(new Route("/hello/world", "store")); trie.add(new Route("/hello/:name", "store")); trie.add(new Route("/hello/*", "store")); ``` More specific matches are prioritized. The following pathnames would match the corresponding patterns: | Pathname | Pattern | | ------------------- | -------------- | | `/hello/world` | `/hello/world` | | `/hello/john` | `/hello/:name` | | `/hello/john/smith` | `/hello/*` | ## Create your own router `App` is built using the `Trie` and `Route` classes. You don't need to access these if you are using `App`, but you can build your own router using the them. ```ts import { Route, Trie } from "ovr"; // specify the type of the store in the generic const trie = new Trie(); const route = new Route("/hello/:name", "store"); trie.add(route); const match = trie.find("/hello/world"); // { route, params: { name: "world" } } ``` # Memoization How to deduplicate and cache function calls in ovr. ## Context cache If you need to display a component in multiple locations with the same dynamic information, you need to ensure you aren't fetching the same data multiple times. ovr provides built in memoization on the request context you can utilize on any function to memoize it for the request. ```tsx import { Context } from "ovr"; import { db } from "@/lib/db"; async function Data(props: { id: number }) { // acquire the context const c = Context.get(); // automatically deduped and cached for the current request const data = await c.memo(db.query)(props.id); return {data}; } ``` This will deduplicate multiple calls to the same function with the with the same arguments and cache the result. Every time `` is called with the same `id`, the result will be reused. ## Create your own cache The `Memo` class can also be utilized outside of the application context if you need to cache across requests. It's generally safer to cache per request using `Context.memo`---especially for user specific or sensitive information. But if you have a long running server and need to cache public data, you can create a `Memo` outside of the app. ```ts import { Memo } from "ovr"; const memo = new Memo(); const add = memo.use((a: number, b: number) => a + b); add(1, 2); // runs add(1, 2); // cached add(2, 3); // runs again, saves the new result separately ``` # Middleware Built in middleware helpers. ## CSRF Basic [cross-site request forgery (CSRF)](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF) protection that checks the request's `method` and `Origin` header. For more robust protection you'll need a stateful server or a database to store [CSRF tokens](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF#csrf_tokens). ```ts import { csrf } from "ovr"; app.use(csrf({ origin: "https://example.com" })); ``` # Libraries Libraries that work well with ovr. Here are some libraries that work nicely alongside ovr. Have a library you'd recommend with ovr? Feel free to [submit a PR](https://github.com/rossrobino/ovr/edit/main/apps/docs/src/server/docs/09-libraries.md). --- ## Data - Database services - [Convex](https://www.convex.dev/) - [PlanetScale](https://planetscale.com/) - [Prisma Postgres](https://www.prisma.io/postgres) - [Supabase](https://supabase.com/) - ORMs - [drizzle](https://orm.drizzle.team/) - [Prisma](https://www.prisma.io/) ## Interactivity - [Adobe Spectrum Web Components](https://opensource.adobe.com/spectrum-web-components/index.html) - full featured web component library based on Adobe's Spectrum design system - [drab](https://drab.robino.dev) - SSR first custom elements - [htmx](https://htmx.org/) - server based interactivity using HTML attributes - [Lit](https://lit.dev/) - framework for building your own web components - [Kelp](https://kelpui.com/) - HTML web components and CSS ## Styling - [Pico](https://picocss.com/) - opinionated base styles for common elements - [tailwindcss](https://tailwindcss.com) - prebuilt utility classes that compile away - [daisyUI](https://daisyui.com/) - Tailwind plugin that includes themes and CSS components - [uico](https://uico.robino.dev) - base styles for common elements ## Utilities - [cookie-es](https://github.com/unjs/cookie-es) - `Cookie` and `Set-Cookie` parser and serializer ## Validation - [Valibot](https://valibot.dev/) - [Zod](https://zod.dev/) # Basic Create a page and POST request handler with ovr. A basic page created with the [`Get` helper](/04-helpers#get). ```tsx import { Get } from "ovr"; export const page = new Get("/demo/basic", (c) => { c.head(Basic); return

Basic

; }); ``` # Chat Learn how to build a basic chat interface with ovr. Here's a chat example with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-js/). The response is streamed _without_ client-side JavaScript using the async generator `Poet` component. ```tsx async function* Poet(props: { message: string }) { const agent = new Agent({ name: "Poet", instructions: "You turn messages into poems.", model: "gpt-4.1-nano", }); const result = await run(agent, props.message, { stream: true }); for await (const event of result) { if ( event.type === "raw_model_stream_event" && event.data.type === "output_text_delta" ) { yield event.data.delta; } } } ``` # Form Create a page and POST request handler with ovr. This is a page and post route created with ovr's [`Get` and `Post` helpers](/04-helpers). The generated `` can be used directly within the `Get` handler. ```tsx import { Get, Post } from "ovr"; import * as z from "zod"; export const page = new Get("/demo/form", (c) => { c.head(Form); return (
); }); export const post = new Post(async (c) => { const data = await c.req.formData(); const name = z.string().parse(data.get("name")); name; // text input string return c.redirect("/", 303); }); ``` # Layout Create a layout to reuse on multiple pages. Use the [`Context.layout`](/05-context#page-builders) method to add a layout to a route. To use the same layout for all pages, create a middleware that sets the layout and apply it globally with [`app.use`](/03-app#global-middleware). ```tsx import { App, type JSX } from "ovr"; const app = new App(); const Layout = (props: { children?: JSX.Element }) => { return ( <>
...
{props.children}
...
); }; app.use((c, next) => { c.layout(Layout); return next(); }); ``` # Memory Optimization Demo of using sync generators to optimize memory usage. Using generators can reduce memory consumption which can be useful if you are rendering a large HTML page. Instead of creating the entire component in memory by mapping through a large array: ```tsx function Numbers() { return Array.from({ length: 5_000 }).map(() =>
); // creates an array of 5000 divs } ``` You can use a generator to `yield` elements as you iterate through the array. This allows the server to send the result as it iterates through the generator, users also see the start of the content faster. ```tsx function* Numbers() { let i = 0; while (i++ < 5_000) yield
; } ``` --- # Parallelization Understand how ovr processes async components in parallel. With ovr, every component is streamed independently allowing you to read this content immediately instead of waiting for the last component to render. The delay does not waterfall since components are generated in parallel with [`Promise.race`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race). ```tsx const Delay = async ({ ms }: { ms: number }) => { await new Promise((res) => setTimeout(res, ms)); return
{ms}ms
; }; const Delays = () => { const delays = Array.from({ length: 6 }, (_, i) => i * 100); return delays.map((ms) => ); }; ``` --- # Todo A basic todo app built with ovr. A server driven todo app that stores data in the URL. ```tsx import { Chunk, Context, Get, Post } from "ovr"; import * as z from "zod"; export const add = new Post(async () => { const todos = getTodos(); const { text } = await data(); todos.push({ id: (todos.at(-1)?.id ?? 0) + 1, text, done: false }); redirect(todos); }); export const toggle = new Post(async () => { const todos = getTodos(); const { id } = await data(); const current = todos.find((t) => t.id === id); if (current) current.done = !current.done; redirect(todos); }); export const remove = new Post(async () => { const todos = getTodos(); const { id } = await data(); redirect(todos.filter((t) => t.id !== id)); }); export const todo = new Get("/demo/todo", (c) => { return (
    {getTodos().map((t) => (
  • {t.done ? "done" : "todo"} {t.text}
    x
  • ))}
Reset
); }); const TodoSchema = z.object({ done: z.boolean().optional(), id: z.coerce.number(), text: z.coerce.string(), }); const redirect = (todos: z.infer<(typeof TodoSchema)[]>) => { const c = Context.get(); const location = new URL(todo.pathname(), c.url); location.searchParams.set("todos", JSON.stringify(todos)); c.redirect(location, 303); }; const getTodos = () => { const todos = Context.get().url.searchParams.get("todos"); if (!todos) return [{ done: false, id: 0, text: "Build a todo app" }]; return z.array(TodoSchema).parse(JSON.parse(todos)); }; const data = async () => { const data = await Context.get().req.formData(); return TodoSchema.parse({ id: data.get("id"), text: data.get("text") }); }; ```