Skip to content
Oxide
Esc
↑↓navigate↵open⌘Jpreview
On this page

Quickstart

Install the plugin, write a server (and an action), then run a production build.

Install

npm install -D oxidejs
pnpm add -D oxidejs
yarn add -D oxidejs
bun add -D oxidejs
nub add -D oxidejs
aube add -D oxidejs

Extend the TypeScript config:

{ "extends": "oxidejs/tsconfig" }

Add the plugin

Use Vite or Rsbuild. Same plugin, same output tree.

import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide()],
});

Add scripts to package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}
import { import defineConfigdefineConfig } from "@rsbuild/core";
import const oxide: (options?: OxidejsOptions | undefined) => anyoxide from "oxidejs/rsbuild";

export default import defineConfigdefineConfig({
  plugins: any[]plugins: [function oxide(options?: OxidejsOptions | undefined): anyoxide()],
});

Add scripts to package.json:

{
  "scripts": {
    "dev": "rsbuild dev",
    "build": "rsbuild build",
    "preview": "rsbuild preview"
  }
}

Server entry

Handle routes you care about. Return nothing to fall through to static files (and index.html for navigations):

import type { type FetchHandler<Env extends object = { [key: string]: OxidejsJson; }> = (request: Request, env: Env, ctx: ExecutionContext) => FetchResult | Promise<FetchResult>
`src/server.ts` fetch handler. The generated wrapper always calls `fetch(request, env, ctx)` — `env` may be `{}` on Node without the `env` option. Return `undefined` (or bare `return`) to fall through to assets.
FetchHandler
} from "oxidejs";
export const const fetch: (request: Request) => Response | undefinedfetch = ((request: Requestrequest) => { if (new var URL: new (url: string | URL, base?: string | URL) => URL
The **`URL`** interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL. [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL)
URL
(request: Requestrequest.Request.url: string
The **`url`** read-only property of the Request interface contains the URL of the request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url)
url
).URL.pathname: string
The **`pathname`** property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character. [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)
pathname
=== "/api/ok") {
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response
The **`Response`** interface of the Fetch API represents the response to a request. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)
Response
("ok");
} return; }) satisfies type FetchHandler<Env extends object = { [key: string]: OxidejsJson; }> = (request: Request, env: Env, ctx: ExecutionContext) => FetchResult | Promise<FetchResult>
`src/server.ts` fetch handler. The generated wrapper always calls `fetch(request, env, ctx)` — `env` may be `{}` on Node without the `env` option. Return `undefined` (or bare `return`) to fall through to assets.
FetchHandler
;

A typed action

Put server-only code in *.server.ts. Wrap what the browser may call in action(). Import it from the client with the same module path — Oxide replaces the import with Effect RPC. The implementation never ships to the browser:

import { function action<Args extends unknown[], Y, R = void>(fn: (...args: Args) => AsyncGenerator<Y, R, unknown>, opts?: ActionOptions): StreamActionHandle<Args, Y, R> (+3 overloads)
Marks a `*.server.ts` export as a remote RPC action. On the server the underlying function runs locally; on the client the build replaces the module with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`). Optional schemas stamp Effect Rpc payload / success / error codecs for the generated actions module. `withSchema` is sugar for `{ payload }`.
action
} from "oxidejs";
export const const greet: ServerActionHandle<[name: string], `hello, ${string}`>greet = action<[name: string], Promise<`hello, ${string}`>>(fn: (name: string) => Promise<`hello, ${string}`>, opts?: ActionOptions): ServerActionHandle<[name: string], `hello, ${string}`> (+3 overloads)
Marks a `*.server.ts` export as a remote RPC action. On the server the underlying function runs locally; on the client the build replaces the module with an `Atom.fn`-shaped RPC handle (`set`, `bind`, `result`). Optional schemas stamp Effect Rpc payload / success / error codecs for the generated actions module. `withSchema` is sugar for `{ payload }`.
action
(async (name: stringname: string) => {
return `hello, ${name: stringname}` as type const = `hello, ${string}`const; });
// @filename: client.ts
import { const greet: ServerActionHandle<[name: string], `hello, ${string}`>greet } from "./hello.server";

const 
const message: `hello, ${string}`
message
= await function greet(...args: [name: string] | [name: string, CallOptions]): Promise<`hello, ${string}`>greet("Ryuz");

Build and run

vite build
node dist/server.js

No index.html → only dist/server.js. With index.html → client assets in dist/client/. Files in public/ land next to those assets.

Common workflows

Cloudflare Workers

Add a root wrangler.jsonc (preset defaults to "worker") and @cloudflare/vite-plugin. Wrap Cloudflare options with withOxide so durable bindings merge.

import { function cloudflare(pluginConfig?: PluginConfig): Plugin[]
Vite plugin that enables a full-featured integration between Vite and the Cloudflare Workers runtime. See the [README](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare#readme) for more details.
@parampluginConfig An optional {@link PluginConfig} object.
cloudflare
} from "@cloudflare/vite-plugin";
import {
const withOxide: <T extends object>(options?: T & {
    root?: string;
}) => Omit<T, "config" | "root" | "viteEnvironment"> & {
    config: CloudflareConfigCustomizer;
    viteEnvironment: ViteEnvironmentOptions;
}
Higher-order options for `@cloudflare/vite-plugin`'s `cloudflare()` — defaults `viteEnvironment.name` to `"ssr"` and runs `mergeDurableBindings` before any user `config` object / function. Pass `root` when the Vite project root is not `process.cwd()` (stripped before options reach Cloudflare). With a root `cloudflare.config.ts` and no wrangler file (or `configPath`), the config `oxide()` converted from it is assigned first. List `oxide()` in Vite `plugins` so it loads before Cloudflare resolves its config. ```ts cloudflare(withOxide()) cloudflare(withOxide({ root: import.meta.dirname })) cloudflare(withOxide({ config: (c) => { … } })) ```
withOxide
} from "oxidejs/wrangler";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite"; import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
} from "vite";
export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)
Type helper to make it easier to use vite.config.ts accepts a direct {@link UserConfig } object, or a function that returns it. The function receives a {@link ConfigEnv } object.
defineConfig
({
UserConfig.plugins?: PluginOption[] | undefined
Array of vite plugins to use.
plugins
: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide(), function cloudflare(pluginConfig?: PluginConfig): Plugin[]
Vite plugin that enables a full-featured integration between Vite and the Cloudflare Workers runtime. See the [README](https://github.com/cloudflare/workers-sdk/tree/main/packages/vite-plugin-cloudflare#readme) for more details.
@parampluginConfig An optional {@link PluginConfig} object.
cloudflare
(
withOxide<object>(options?: (object & {
    root?: string;
}) | undefined): Omit<object, "config" | "root" | "viteEnvironment"> & {
    config: CloudflareConfigCustomizer;
    viteEnvironment: ViteEnvironmentOptions;
}
Higher-order options for `@cloudflare/vite-plugin`'s `cloudflare()` — defaults `viteEnvironment.name` to `"ssr"` and runs `mergeDurableBindings` before any user `config` object / function. Pass `root` when the Vite project root is not `process.cwd()` (stripped before options reach Cloudflare). With a root `cloudflare.config.ts` and no wrangler file (or `configPath`), the config `oxide()` converted from it is assigned first. List `oxide()` in Vite `plugins` so it loads before Cloudflare resolves its config. ```ts cloudflare(withOxide()) cloudflare(withOxide({ root: import.meta.dirname })) cloudflare(withOxide({ config: (c) => { … } })) ```
withOxide
())],
});

Production builds that emit dist/ssr/wrangler.json also write a celld-ready dist/wrangler.json. See preset and Worker.

Server only

Leave out index.html. The plugin emits dist/server.js and does not build a client.

Next steps

Was this page helpful?