Quick Start

The fastest way to start an Effex project is with create-effex. It scaffolds a working app with routing, reactive state, and all the tooling configured.

pnpm create effex my-app

You can also use npm, yarn, or bun:

npx create-effex my-app
yarn create effex my-app
bunx create-effex my-app

The CLI will ask you to pick a template:

  • SPA — Client-side only, no server required
  • SSR — Server-side rendering with client hydration
  • SSG — Static site generation, pre-rendered at build time

Once it’s done, start the dev server:

cd my-app
pnpm dev

Open http://localhost:3000 and you’re running.

The rest of this guide walks through what each template gives you and when to pick one over another.


SPA (Single Page Application)

pnpm create effex my-app --spa

The simplest setup. Everything runs in the browser — no server, no build-time rendering. Good for dashboards, internal tools, or anything that doesn’t need SEO.

What you get

my-app/
├── src/
│   ├── main.ts        # Mounts the app
│   ├── App.ts         # Root component with nav + Outlet
│   └── routes.ts      # Route definitions
├── public/
│   └── styles.css     # Base styles
├── index.html
├── vite.config.ts
└── tsconfig.json

Entry point

src/main.ts mounts the app and provides the router’s Navigation layer:

import { Effect } from "effect";
import { mount, runApp } from "@effex/dom";
import { Navigation } from "@effex/router";

import { App } from "./App.js";
import { router } from "./routes.js";

const root = document.getElementById("root")!;

runApp(
  Effect.gen(function* () {
    const app = yield* App();
    yield* mount(app, root);
  }).pipe(Effect.provide(Navigation.layer(router))),
);

Defining routes

Routes are plain functions that return Effects:

import { Route, Router } from "@effex/router";
import { $, collect, Signal } from "@effex/dom";

const Home = Route.make("/").pipe(
  Route.render(() =>
    Effect.gen(function* () {
      const count = yield* Signal.make(0);
      return yield* $.div(
        {},
        collect(
          $.h1({}, $.of("Welcome to Effex")),
          $.button(
            { onClick: () => count.update((n) => n + 1) },
            count,
          ),
        ),
      );
    }),
  ),
);

export const router = Router.empty.pipe(
  Router.concat(Home),
);

Scripts

Command What it does
pnpm dev Start Vite dev server
pnpm build Production build
pnpm preview Preview the production build

SSR (Server-Side Rendering)

pnpm create effex my-app --ssr

Full-stack rendering. The server renders HTML on each request using Effect’s HTTP platform, then the client hydrates it. Use this when you need SEO, fast initial page loads, or server-side data loading.

What you get

my-app/
├── src/
│   ├── app.ts           # Root component
│   ├── routes.ts        # Route definitions
│   ├── server.ts        # Production Node.js server
│   ├── client.ts        # Client hydration entry
│   └── vite-entry.ts    # Dev server SSR entry
├── public/
│   └── styles.css
├── vite.config.ts
└── tsconfig.json

Server entry

src/server.ts is a Node.js server built on @effect/platform. It serves static assets and delegates everything else to Effex’s SSR:

import { Effect } from "effect";
import { HttpRouter, HttpServer } from "@effect/platform";
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
import { Platform } from "@effex/platform";

import { App } from "./app.js";
import { router } from "./routes.js";

const effexRoutes = Platform.toHttpRoutes(router, {
  app: App,
  document: {
    title: "My App",
    scripts: ["/client.js"],
    styles: ["/styles.css"],
  },
});

// Compose with other routes if needed
const httpApp = HttpRouter.empty.pipe(
  HttpRouter.concat(effexRoutes),
);

Client hydration

src/client.ts hydrates the server-rendered HTML:

import { hydrate } from "@effex/dom";
import { Platform } from "@effex/platform";

import { App } from "./app.js";
import { router } from "./routes.js";

hydrate(App(), document.getElementById("root")!, {
  layers: Platform.makeClientLayer(router),
});

After hydration, navigation is client-side. Data for new pages is fetched as JSON (via ?_data=1 requests) without full page reloads.

Vite plugin

The SSR template uses @effex/vite-plugin to handle dev-time SSR:

import { defineConfig } from "vite";
import { effexPlatform } from "@effex/vite-plugin";

export default defineConfig({
  plugins: [
    effexPlatform({ entry: "src/vite-entry.ts" }),
  ],
});

Scripts

Command What it does
pnpm dev Vite dev server with SSR
pnpm build Build client and server bundles
pnpm start Run the production server

SSG (Static Site Generation)

pnpm create effex my-app --ssg

Pages are pre-rendered to HTML at build time. No server at runtime — just static files you can deploy anywhere. Use this for docs sites, blogs, marketing pages, or anything where the content is known ahead of time.

What you get

my-app/
├── src/
│   ├── App.ts         # Root component
│   ├── routes.ts      # Route definitions with static paths
│   ├── entry.ts       # Build-time entry point
│   └── client.ts      # Client hydration entry
├── public/
│   └── styles.css
├── vite.config.ts
└── tsconfig.json

Static routes

SSG routes use Route.static() to declare which paths to generate and how to load data for each:

import { Effect } from "effect";
import { Route, Router } from "@effex/router";
import { $ } from "@effex/dom";

const DocsRoute = Route.make("/docs/:slug").pipe(
  Route.static({
    // Which paths to generate
    paths: () =>
      Effect.succeed([
        { slug: "getting-started" },
        { slug: "routing" },
      ]),

    // Load data for each path (runs at build time)
    load: ({ params }) =>
      Effect.succeed({
        title: params.slug,
        content: `Content for ${params.slug}`,
      }),

    // Render with loaded data
    render: (data) =>
      $.article(
        {},
        $.of(data.content),
      ),
  }),
);

Build-time entry

src/entry.ts exports everything the SSG builder needs:

import { App } from "./App.js";
import { router } from "./routes.js";

export { router, App };

export const document = {
  title: "My Site",
  scripts: ["/client.js"],
  styles: ["/styles.css"],
};

Client hydration

Like SSR, the client hydrates after the static HTML loads. But since there’s no server at runtime, Platform.makeClientLayer isn’t needed — route data is embedded in the HTML:

import { hydrate } from "@effex/dom";
import { App } from "./App.js";

hydrate(App(), document.getElementById("root")!);

Scripts

Command What it does
pnpm dev Vite dev server with SSR rendering
pnpm build Generate static HTML + client bundle
pnpm preview Preview the static site

Which template should I use?

SPA SSR SSG
SEO No Yes Yes
Initial load speed Slower (JS must execute) Fast (HTML from server) Fastest (pre-built HTML)
Dynamic data Client-side fetching Server loaders Build-time only
Hosting Any static host Node.js server Any static host
Best for Dashboards, internal tools Apps with auth, real-time data Docs, blogs, marketing

You can always change later — the component model is the same across all three. The main difference is the entry point and how data gets loaded.