newX 1.3: islands to disk, server-mode islands, and an image proxy

Server Functions

Server functions

Call server-side functions from the browser without writing REST endpoints. Server functions live in src/actions/. Import one into an island and call it like a normal function, or call it manually with fetch. Both compile down to the same request.

When the dev server starts, registered server functions show up in the route log:

terminal · x dev
$x dev
[x] resolving routes...
[x] ── actions: greet.greet, subscribe.subscribeUser
[x] dev server running at http://localhost:3000

Defining server functions

Create a file in src/actions/ and export named async functions. There is no "use server" directive; arguments passed from the client arrive as JSON, and the return value is serialized back as JSON.

src/actions/greet.ts
export async function greet(name: string) {  return `Hello, ${name}! The server time is ${new Date().toISOString()}.`;}export async function sendEmail({ to, subject, body }: {  to: string;  subject: string;  body: string;}) {  // send email logic  return { sent: true, to };}

Or register a map in one shot with export const actions, which is handy for grouping several functions under one file. This also works in src/api/ files and page files, so a greet.ts with no page component still registers its actions.

src/actions/greet.ts
export const actions = {  greet: async (name: string) => `Hello, ${name}!`,  ping: async () => ({ pong: true }),};

Action paths can carry [param] segments like page routes: src/actions/posts/[id].ts matches /__x/actions/posts/<any-id>/<fn>. The segment selects the route only — arguments always travel in the request body.

Calling actions directly

Import the function into an island component and call it like any other async function. When you run x build, the bundler swaps the import for a generated fetch client before it reaches the browser, so the real implementation, db calls and all, never gets bundled.

src/components/subscribe-form.tsx
import { useState } from "react";import { subscribeUser } from "../actions/subscribe";export default function SubscribeForm() {  const [status, setStatus] = useState("");  async function handleSubmit(e: React.FormEvent) {    e.preventDefault();    const form = new FormData(e.target as HTMLFormElement);    const email = form.get("email") as string;    await subscribeUser(email);    setStatus("Subscribed!");  }  return (    <form onSubmit={handleSubmit} className="space-y-4">      <input        name="email"        type="email"        placeholder="you@example.com"        className="rounded-xl border border-border bg-card px-4 py-2"      /> >      <button type="submit" className="rounded-xl bg-primary px-4 py-2 text-primary-foreground">        Subscribe      </button>      {status && <p className="text-muted-foreground">{status}</p>}    </form>  );}

x has no "use client" directive. To make the form interactive, register it on the page with export const islands = { SubscribeForm } and render it inside <Island name="SubscribeForm" client="load"> (see Islands). Only files under actionsDir get the fetch-wrapper treatment: import a regular server-only helper into client code and it bundles as-is; if it leaks a secret, the build-time env isolation check catches it instead.

Calling manually with fetch

This is what the direct-import style compiles down to, and it works the same way in dev and in production: a POST request to /__x/actions/<filename>/<functionName>. The arguments are sent as JSON in the request body: an array maps positionally onto the function's parameters, and a single non-array value is wrapped as [value]. Error statuses are part of the contract: 403 for a CSRF origin mismatch, 404 for an unknown function, 400/413 for a bad or oversized body. A thrown action never leaks its error text in production — the response carries an opaque incident id (also set on the x-x-error-id header) you can correlate with server logs.

island component
import { useState } from "react";export default function GreetForm() {  const [message, setMessage] = useState("");  async function handleSubmit(e: React.FormEvent) {    e.preventDefault();    const form = new FormData(e.target as HTMLFormElement);    const name = form.get("name");    const res = await fetch("/__x/actions/greet/greet", {      method: "POST",      headers: { "Content-Type": "application/json" },      body: JSON.stringify([name]),    });    const data = await res.text();    setMessage(data);  }  return (    <form onSubmit={handleSubmit} className="space-y-4">      <input        name="name"        placeholder="Enter your name"        className="rounded-xl border border-border bg-card px-4 py-2"      /> >      <button type="submit" className="rounded-xl bg-primary px-4 py-2 text-primary-foreground">        Greet me      </button>      {message && <p className="text-muted-foreground">{message}</p>}    </form>  );}

Reach for this style directly when you're calling an action from outside an island, or anywhere you'd rather see the request explicitly.

Server functions from loaders

You can also import and call server functions directly in loaders. No HTTP needed, since they share the same process.

src/pages/dashboard.tsx
import type { RouteProps, LoaderArgs } from "@thexjs/core";import { getDashboardData } from "../actions/dashboard";export async function loader({ request }: LoaderArgs) {  const data = await getDashboardData();  return { data };}export default function Dashboard({ loaderData }: RouteProps) {  return <div>...</div>;}

Use cases

Server functions are ideal for form handling, sending emails, database mutations, and any server-side logic that doesn't need a dedicated REST API. They reduce boilerplate and keep your client code simple.