Skip to main content

Quick Start

Create A Project

npx @evjs/create-app my-app
cd my-app
npm install
npm run dev

The development server prints the selected browser and server URLs.

Define The Application

Create ev.config.ts and choose the output mode:

import { defineConfig } from "@evjs/ev";

export default defineConfig({
routing: {
mode: "spa",
},
});

Create two Page routes:

src/pages/
├── page.tsx # /
└── about/
└── page.tsx # /about
// src/pages/page.tsx
import { Link } from "@evjs/ev/navigation";

export default function HomePage() {
return (
<main>
<h1>Home</h1>
<Link to="/about">About</Link>
</main>
);
}
// src/pages/about/page.tsx
export default function AboutPage() {
return <h1>About</h1>;
}

page.* is the Page and Route anchor. Its relative directory determines the URL, so there is no separate route declaration.

When a Page needs build-time capabilities, add page.config.ts beside it:

import { definePageConfig } from "@evjs/ev";

export default definePageConfig({
title: "About",
meta: {
description: "About this application",
keywords: "evjs,about",
viewport: "width=device-width, initial-scale=1",
"theme-color": "#ffffff",
},
render: "csr",
});

title and meta are static core Page metadata. meta emits only <meta name="..." content="..."> entries. Installed Page-aware plugins use their canonical ids under plugins; those values are not automatically sent to browser runtime.

Page-Private Code

Keep components, hooks, models, services, tests, styles, and assets inside the Page directory:

src/pages/about/
├── page.tsx
├── page.config.ts
├── index.ts
├── model.ts
├── use-about.ts
└── components/
└── Team.tsx

Only page.* creates a Page and Route. Every other file, including index.*, is ordinary private source and needs no _ prefix.

Add A Dynamic Route

Use a $param directory:

src/pages/
└── users/
└── $userId/
└── page.tsx # /users/:userId
// src/pages/users/$userId/page.tsx
import { usePageParams } from "@evjs/ev/route";

export default function UserDetailPage() {
const { userId } = usePageParams();
return <h1>User {userId}</h1>;
}

Static directories create static URL segments. A terminal $...splat directory creates a catch-all, and (group) organizes routes without adding a URL segment.

Switch To MPA

The Page tree does not move. Change only the materialization mode:

export default defineConfig({
routing: {
mode: "mpa",
},
});

SPA materializes the tree as browser Client Routes, normally under one shared Document. MPA starts from the same semantic Pages and Routes and materializes Page-owned Documents. A Page-local index.html can provide its MPA Document template. MPA accepts only static Page paths: $param, terminal $...splat, and router-only boundaries fail during inspect/build. Layouts compose in both modes.

Add A Server Function

Server functions can live beside the Page that calls them:

// src/pages/get-message.server.ts
"use server";

export async function getMessage() {
return "Hello from the server";
}
// src/pages/page.tsx
import { useQuery } from "@evjs/ev/query";
import { getMessage } from "./get-message.server";

export default function HomePage() {
const { data } = useQuery(getMessage);
return <h1>{data}</h1>;
}

Add A Server Route

Server request Routes use a separate positive api.* anchor under src/apis:

// src/apis/api/health/api.ts
export function GET() {
return Response.json({ ok: true });
}

The containing api/health directory creates /api/health. Client page.* routes and server request Routes are separate systems with symmetric positive anchors.

Build

npm run build

By default:

  • client output goes to dist/client;
  • server output goes to dist/server;
  • framework-generated IR lives under .ev.

Treat .ev, dist, src/route-types.d.ts, src/plugin-types.d.ts, and other generated artifacts as outputs. Do not edit them or copy them into templates.

Core Packages

PackagePurpose
@evjs/cliev dev, ev build, ev inspect, and related commands
@evjs/evConfig, plugins, build graph, deployment helpers, and app-facing subpaths
@evjs/ev/routePage params, search, and loader-data helpers
@evjs/ev/navigationLink, navigation, redirects, and outlets
@evjs/ev/queryServer-function query and mutation helpers
@evjs/ev/server-contextRequest-context helpers
@evjs/ev/transportCustom client/server transport helpers
@evjs/clientStandalone browser runtime primitives
@evjs/serverStandalone server runtime primitives

Framework-owned Page applications import from @evjs/ev and its curated subpaths. Use @evjs/client and @evjs/server directly only for intentional standalone/manual runtime composition.

Choose One Route Input

For file-convention routing, put each published Page in the directory for its URL, name the entry page.*, keep Page settings in adjacent page.config.ts, and declare routing.mode: "spa" | "mpa".

Explicit application.routes is a separate SPA-only configuration input. It supports page or component, nested routes, layouts, wrappers, and redirects. Plugin configuration remains Page-owned. It cannot be combined with routing and never selects MPA.

An unrelated src/pages directory alone does not publish client routes. Run ev inspect to verify the normalized Page/Route structure.

Next, read Project Structure, Client Routes, and Configuration.